blob: dc3fa2a048f615e4abf16b8924ddcf8b752278ad [file] [log] [blame]
/*
* Copyright (C) 2020 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 com.android.server.people.data;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.LocusId;
import android.content.LocusIdProto;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutInfo.ShortcutFlags;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.Preconditions;
import com.android.server.people.ConversationInfoProto;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/**
* Represents a conversation that is provided by the app based on {@link ShortcutInfo}.
*/
public class ConversationInfo {
private static final String TAG = ConversationInfo.class.getSimpleName();
private static final int FLAG_IMPORTANT = 1;
private static final int FLAG_NOTIFICATION_SILENCED = 1 << 1;
private static final int FLAG_BUBBLED = 1 << 2;
private static final int FLAG_PERSON_IMPORTANT = 1 << 3;
private static final int FLAG_PERSON_BOT = 1 << 4;
private static final int FLAG_CONTACT_STARRED = 1 << 5;
private static final int FLAG_DEMOTED = 1 << 6;
@IntDef(flag = true, prefix = {"FLAG_"}, value = {
FLAG_IMPORTANT,
FLAG_NOTIFICATION_SILENCED,
FLAG_BUBBLED,
FLAG_PERSON_IMPORTANT,
FLAG_PERSON_BOT,
FLAG_CONTACT_STARRED,
FLAG_DEMOTED,
})
@Retention(RetentionPolicy.SOURCE)
private @interface ConversationFlags {
}
@NonNull
private String mShortcutId;
@Nullable
private LocusId mLocusId;
@Nullable
private Uri mContactUri;
@Nullable
private String mContactPhoneNumber;
@Nullable
private String mNotificationChannelId;
@ShortcutFlags
private int mShortcutFlags;
@ConversationFlags
private int mConversationFlags;
private ConversationInfo(Builder builder) {
mShortcutId = builder.mShortcutId;
mLocusId = builder.mLocusId;
mContactUri = builder.mContactUri;
mContactPhoneNumber = builder.mContactPhoneNumber;
mNotificationChannelId = builder.mNotificationChannelId;
mShortcutFlags = builder.mShortcutFlags;
mConversationFlags = builder.mConversationFlags;
}
@NonNull
public String getShortcutId() {
return mShortcutId;
}
@Nullable
LocusId getLocusId() {
return mLocusId;
}
/** The URI to look up the entry in the contacts data provider. */
@Nullable
Uri getContactUri() {
return mContactUri;
}
/** The phone number of the associated contact. */
@Nullable
String getContactPhoneNumber() {
return mContactPhoneNumber;
}
/**
* ID of the {@link android.app.NotificationChannel} where the notifications for this
* conversation are posted.
*/
@Nullable
String getNotificationChannelId() {
return mNotificationChannelId;
}
/** Whether the shortcut for this conversation is set long-lived by the app. */
public boolean isShortcutLongLived() {
return hasShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED);
}
/** Whether the shortcut for this conversation is cached in Shortcut Service. */
public boolean isShortcutCached() {
return hasShortcutFlags(ShortcutInfo.FLAG_CACHED);
}
/** Whether this conversation is marked as important by the user. */
public boolean isImportant() {
return hasConversationFlags(FLAG_IMPORTANT);
}
/** Whether the notifications for this conversation should be silenced. */
public boolean isNotificationSilenced() {
return hasConversationFlags(FLAG_NOTIFICATION_SILENCED);
}
/** Whether the notifications for this conversation should show in bubbles. */
public boolean isBubbled() {
return hasConversationFlags(FLAG_BUBBLED);
}
/**
* Whether this conversation is demoted by the user. New notifications for the demoted
* conversation will not show in the conversation space.
*/
public boolean isDemoted() {
return hasConversationFlags(FLAG_DEMOTED);
}
/** Whether the associated person is marked as important by the app. */
public boolean isPersonImportant() {
return hasConversationFlags(FLAG_PERSON_IMPORTANT);
}
/** Whether the associated person is marked as a bot by the app. */
public boolean isPersonBot() {
return hasConversationFlags(FLAG_PERSON_BOT);
}
/** Whether the associated contact is marked as starred by the user. */
public boolean isContactStarred() {
return hasConversationFlags(FLAG_CONTACT_STARRED);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ConversationInfo)) {
return false;
}
ConversationInfo other = (ConversationInfo) obj;
return Objects.equals(mShortcutId, other.mShortcutId)
&& Objects.equals(mLocusId, other.mLocusId)
&& Objects.equals(mContactUri, other.mContactUri)
&& Objects.equals(mContactPhoneNumber, other.mContactPhoneNumber)
&& Objects.equals(mNotificationChannelId, other.mNotificationChannelId)
&& mShortcutFlags == other.mShortcutFlags
&& mConversationFlags == other.mConversationFlags;
}
@Override
public int hashCode() {
return Objects.hash(mShortcutId, mLocusId, mContactUri, mContactPhoneNumber,
mNotificationChannelId, mShortcutFlags, mConversationFlags);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ConversationInfo {");
sb.append("shortcutId=").append(mShortcutId);
sb.append(", locusId=").append(mLocusId);
sb.append(", contactUri=").append(mContactUri);
sb.append(", phoneNumber=").append(mContactPhoneNumber);
sb.append(", notificationChannelId=").append(mNotificationChannelId);
sb.append(", shortcutFlags=0x").append(Integer.toHexString(mShortcutFlags));
sb.append(" [");
if (isShortcutLongLived()) {
sb.append("Liv");
}
if (isShortcutCached()) {
sb.append("Cac");
}
sb.append("]");
sb.append(", conversationFlags=0x").append(Integer.toHexString(mConversationFlags));
sb.append(" [");
if (isImportant()) {
sb.append("Imp");
}
if (isNotificationSilenced()) {
sb.append("Sil");
}
if (isBubbled()) {
sb.append("Bub");
}
if (isDemoted()) {
sb.append("Dem");
}
if (isPersonImportant()) {
sb.append("PIm");
}
if (isPersonBot()) {
sb.append("Bot");
}
if (isContactStarred()) {
sb.append("Sta");
}
sb.append("]}");
return sb.toString();
}
private boolean hasShortcutFlags(@ShortcutFlags int flags) {
return (mShortcutFlags & flags) == flags;
}
private boolean hasConversationFlags(@ConversationFlags int flags) {
return (mConversationFlags & flags) == flags;
}
/** Writes field members to {@link ProtoOutputStream}. */
void writeToProto(@NonNull ProtoOutputStream protoOutputStream) {
protoOutputStream.write(ConversationInfoProto.SHORTCUT_ID, mShortcutId);
if (mLocusId != null) {
long locusIdToken = protoOutputStream.start(ConversationInfoProto.LOCUS_ID_PROTO);
protoOutputStream.write(LocusIdProto.LOCUS_ID, mLocusId.getId());
protoOutputStream.end(locusIdToken);
}
if (mContactUri != null) {
protoOutputStream.write(ConversationInfoProto.CONTACT_URI, mContactUri.toString());
}
if (mNotificationChannelId != null) {
protoOutputStream.write(ConversationInfoProto.NOTIFICATION_CHANNEL_ID,
mNotificationChannelId);
}
protoOutputStream.write(ConversationInfoProto.SHORTCUT_FLAGS, mShortcutFlags);
protoOutputStream.write(ConversationInfoProto.CONVERSATION_FLAGS, mConversationFlags);
if (mContactPhoneNumber != null) {
protoOutputStream.write(ConversationInfoProto.CONTACT_PHONE_NUMBER,
mContactPhoneNumber);
}
}
@Nullable
byte[] getBackupPayload() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(baos);
try {
out.writeUTF(mShortcutId);
out.writeUTF(mLocusId != null ? mLocusId.getId() : "");
out.writeUTF(mContactUri != null ? mContactUri.toString() : "");
out.writeUTF(mNotificationChannelId != null ? mNotificationChannelId : "");
out.writeInt(mShortcutFlags);
out.writeInt(mConversationFlags);
out.writeUTF(mContactPhoneNumber != null ? mContactPhoneNumber : "");
} catch (IOException e) {
Slog.e(TAG, "Failed to write fields to backup payload.", e);
return null;
}
return baos.toByteArray();
}
/** Reads from {@link ProtoInputStream} and constructs a {@link ConversationInfo}. */
@NonNull
static ConversationInfo readFromProto(@NonNull ProtoInputStream protoInputStream)
throws IOException {
ConversationInfo.Builder builder = new ConversationInfo.Builder();
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (protoInputStream.getFieldNumber()) {
case (int) ConversationInfoProto.SHORTCUT_ID:
builder.setShortcutId(
protoInputStream.readString(ConversationInfoProto.SHORTCUT_ID));
break;
case (int) ConversationInfoProto.LOCUS_ID_PROTO:
long locusIdToken = protoInputStream.start(
ConversationInfoProto.LOCUS_ID_PROTO);
while (protoInputStream.nextField()
!= ProtoInputStream.NO_MORE_FIELDS) {
if (protoInputStream.getFieldNumber() == (int) LocusIdProto.LOCUS_ID) {
builder.setLocusId(new LocusId(
protoInputStream.readString(LocusIdProto.LOCUS_ID)));
}
}
protoInputStream.end(locusIdToken);
break;
case (int) ConversationInfoProto.CONTACT_URI:
builder.setContactUri(Uri.parse(protoInputStream.readString(
ConversationInfoProto.CONTACT_URI)));
break;
case (int) ConversationInfoProto.NOTIFICATION_CHANNEL_ID:
builder.setNotificationChannelId(protoInputStream.readString(
ConversationInfoProto.NOTIFICATION_CHANNEL_ID));
break;
case (int) ConversationInfoProto.SHORTCUT_FLAGS:
builder.setShortcutFlags(protoInputStream.readInt(
ConversationInfoProto.SHORTCUT_FLAGS));
break;
case (int) ConversationInfoProto.CONVERSATION_FLAGS:
builder.setConversationFlags(protoInputStream.readInt(
ConversationInfoProto.CONVERSATION_FLAGS));
break;
case (int) ConversationInfoProto.CONTACT_PHONE_NUMBER:
builder.setContactPhoneNumber(protoInputStream.readString(
ConversationInfoProto.CONTACT_PHONE_NUMBER));
break;
default:
Slog.w(TAG, "Could not read undefined field: "
+ protoInputStream.getFieldNumber());
}
}
return builder.build();
}
@Nullable
static ConversationInfo readFromBackupPayload(@NonNull byte[] payload) {
ConversationInfo.Builder builder = new ConversationInfo.Builder();
DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
try {
builder.setShortcutId(in.readUTF());
String locusId = in.readUTF();
if (!TextUtils.isEmpty(locusId)) {
builder.setLocusId(new LocusId(locusId));
}
String contactUri = in.readUTF();
if (!TextUtils.isEmpty(contactUri)) {
builder.setContactUri(Uri.parse(contactUri));
}
String notificationChannelId = in.readUTF();
if (!TextUtils.isEmpty(notificationChannelId)) {
builder.setNotificationChannelId(notificationChannelId);
}
builder.setShortcutFlags(in.readInt());
builder.setConversationFlags(in.readInt());
String contactPhoneNumber = in.readUTF();
if (!TextUtils.isEmpty(contactPhoneNumber)) {
builder.setContactPhoneNumber(contactPhoneNumber);
}
} catch (IOException e) {
Slog.e(TAG, "Failed to read conversation info fields from backup payload.", e);
return null;
}
return builder.build();
}
/**
* Builder class for {@link ConversationInfo} objects.
*/
static class Builder {
private String mShortcutId;
@Nullable
private LocusId mLocusId;
@Nullable
private Uri mContactUri;
@Nullable
private String mContactPhoneNumber;
@Nullable
private String mNotificationChannelId;
@ShortcutFlags
private int mShortcutFlags;
@ConversationFlags
private int mConversationFlags;
Builder() {
}
Builder(@NonNull ConversationInfo conversationInfo) {
if (mShortcutId == null) {
mShortcutId = conversationInfo.mShortcutId;
} else {
Preconditions.checkArgument(mShortcutId.equals(conversationInfo.mShortcutId));
}
mLocusId = conversationInfo.mLocusId;
mContactUri = conversationInfo.mContactUri;
mContactPhoneNumber = conversationInfo.mContactPhoneNumber;
mNotificationChannelId = conversationInfo.mNotificationChannelId;
mShortcutFlags = conversationInfo.mShortcutFlags;
mConversationFlags = conversationInfo.mConversationFlags;
}
Builder setShortcutId(@NonNull String shortcutId) {
mShortcutId = shortcutId;
return this;
}
Builder setLocusId(LocusId locusId) {
mLocusId = locusId;
return this;
}
Builder setContactUri(Uri contactUri) {
mContactUri = contactUri;
return this;
}
Builder setContactPhoneNumber(String phoneNumber) {
mContactPhoneNumber = phoneNumber;
return this;
}
Builder setNotificationChannelId(String notificationChannelId) {
mNotificationChannelId = notificationChannelId;
return this;
}
Builder setShortcutFlags(@ShortcutFlags int shortcutFlags) {
mShortcutFlags = shortcutFlags;
return this;
}
Builder setConversationFlags(@ConversationFlags int conversationFlags) {
mConversationFlags = conversationFlags;
return this;
}
Builder setImportant(boolean value) {
return setConversationFlag(FLAG_IMPORTANT, value);
}
Builder setNotificationSilenced(boolean value) {
return setConversationFlag(FLAG_NOTIFICATION_SILENCED, value);
}
Builder setBubbled(boolean value) {
return setConversationFlag(FLAG_BUBBLED, value);
}
Builder setDemoted(boolean value) {
return setConversationFlag(FLAG_DEMOTED, value);
}
Builder setPersonImportant(boolean value) {
return setConversationFlag(FLAG_PERSON_IMPORTANT, value);
}
Builder setPersonBot(boolean value) {
return setConversationFlag(FLAG_PERSON_BOT, value);
}
Builder setContactStarred(boolean value) {
return setConversationFlag(FLAG_CONTACT_STARRED, value);
}
private Builder setConversationFlag(@ConversationFlags int flags, boolean value) {
if (value) {
return addConversationFlags(flags);
} else {
return removeConversationFlags(flags);
}
}
private Builder addConversationFlags(@ConversationFlags int flags) {
mConversationFlags |= flags;
return this;
}
private Builder removeConversationFlags(@ConversationFlags int flags) {
mConversationFlags &= ~flags;
return this;
}
ConversationInfo build() {
Objects.requireNonNull(mShortcutId);
return new ConversationInfo(this);
}
}
}