CarMessage.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.car.app.messaging.model;

import static java.util.Objects.requireNonNull;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.annotations.CarProtocol;
import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.annotations.KeepFields;
import androidx.car.app.annotations.RequiresCarApi;
import androidx.car.app.model.CarText;
import androidx.core.app.Person;

import java.util.Objects;

/** Represents a single message in a {@link ConversationItem} */
@ExperimentalCarApi
@CarProtocol
@RequiresCarApi(6)
@KeepFields
public class CarMessage {
    @NonNull
    private final Bundle mSender;
    @NonNull
    private final CarText mBody;
    private final long mReceivedTimeEpochMillis;
    private final boolean mIsRead;

    @Override
    public int hashCode() {
        return Objects.hash(
                getPersonHashCode(getSender()),
                mBody,
                mReceivedTimeEpochMillis,
                mIsRead
        );
    }

    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof CarMessage)) {
            return false;
        }
        CarMessage otherCarMessage = (CarMessage) other;

        return
                arePeopleEqual(getSender(), otherCarMessage.getSender())
                        && Objects.equals(mBody, otherCarMessage.mBody)
                        && mReceivedTimeEpochMillis == otherCarMessage.mReceivedTimeEpochMillis
                        && mIsRead == otherCarMessage.mIsRead
                ;
    }

    // TODO(b/266877597): Move to androidx.core.app.Person
    private static boolean arePeopleEqual(Person person1, Person person2) {
        // If a unique ID was provided, use it
        String key1 = person1.getKey();
        String key2 = person2.getKey();
        if (key1 != null || key2 != null) {
            return Objects.equals(key1, key2);
        }

        // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
        String name1 = Objects.toString(person1.getName());
        String name2 = Objects.toString(person2.getName());

        // Fallback: Compare field-by-field
        return
                Objects.equals(name1, name2)
                        && Objects.equals(person1.getUri(), person2.getUri())
                        && Objects.equals(person1.isBot(), person2.isBot())
                        && Objects.equals(person1.isImportant(), person2.isImportant());
    }

    // TODO(b/266877597): Move to androidx.core.app.Person
    private static int getPersonHashCode(Person person) {
        // If a unique ID was provided, use it
        String key = person.getKey();
        if (key != null) {
            return key.hashCode();
        }

        // Fallback: Use hash code for individual fields
        return Objects.hash(
                person.getName(),
                person.getUri(),
                person.isBot(),
                person.isImportant()
        );
    }

    CarMessage(@NonNull Builder builder) {
        this.mSender = requireNonNull(builder.mSender).toBundle();
        this.mBody = requireNonNull(builder.mBody);
        this.mReceivedTimeEpochMillis = builder.mReceivedTimeEpochMillis;
        this.mIsRead = builder.mIsRead;
    }

    /** Default constructor for serialization. */
    private CarMessage() {
        this.mSender = new Person.Builder().setName("").build().toBundle();
        this.mBody = new CarText.Builder("").build();
        this.mReceivedTimeEpochMillis = 0;
        this.mIsRead = false;
    }


    /** Returns a {@link Person} representing the message sender */
    @NonNull
    public Person getSender() {
        return Person.fromBundle(mSender);
    }

    /** Returns a {@link CarText} representing the message body */
    @NonNull
    public CarText getBody() {
        return mBody;
    }

    /** Returns a {@code long} representing the message timestamp (in epoch millis) */
    public long getReceivedTimeEpochMillis() {
        return mReceivedTimeEpochMillis;
    }

    /** Returns a {@code boolean}, indicating whether the message has been read */
    public boolean isRead() {
        return mIsRead;
    }

    /** A builder for {@link CarMessage} */
    public static final class Builder {
        @Nullable
        Person mSender;
        @Nullable
        CarText mBody;
        long mReceivedTimeEpochMillis;
        boolean mIsRead;

        /** Sets a {@link Person} representing the message sender */
        public @NonNull Builder setSender(@NonNull Person sender) {
            mSender = sender;
            return this;
        }

        /** Sets a {@link CarText} representing the message body */
        public @NonNull Builder setBody(@NonNull CarText body) {
            mBody = body;
            return this;
        }

        /** Sets a {@code long} representing the message timestamp (in epoch millis) */
        public @NonNull Builder setReceivedTimeEpochMillis(long receivedTimeEpochMillis) {
            mReceivedTimeEpochMillis = receivedTimeEpochMillis;
            return this;
        }

        /** Sets a {@code boolean}, indicating whether the message has been read */
        public @NonNull Builder setRead(boolean isRead) {
            mIsRead = isRead;
            return this;
        }

        /** Returns a new {@link CarMessage} instance defined by this builder */
        public @NonNull CarMessage build() {
            return new CarMessage(this);
        }
    }
}