1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.people.data;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.LocusId;
23 import android.content.LocusIdProto;
24 import android.content.pm.ShortcutInfo;
25 import android.content.pm.ShortcutInfo.ShortcutFlags;
26 import android.net.Uri;
27 import android.text.TextUtils;
28 import android.util.Slog;
29 import android.util.proto.ProtoInputStream;
30 import android.util.proto.ProtoOutputStream;
31 
32 import com.android.internal.util.Preconditions;
33 import com.android.server.people.ConversationInfoProto;
34 
35 import java.io.ByteArrayInputStream;
36 import java.io.ByteArrayOutputStream;
37 import java.io.DataInputStream;
38 import java.io.DataOutputStream;
39 import java.io.IOException;
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.Objects;
43 
44 /**
45  * Represents a conversation that is provided by the app based on {@link ShortcutInfo}.
46  */
47 public class ConversationInfo {
48 
49     private static final String TAG = ConversationInfo.class.getSimpleName();
50 
51     private static final int FLAG_IMPORTANT = 1;
52 
53     private static final int FLAG_NOTIFICATION_SILENCED = 1 << 1;
54 
55     private static final int FLAG_BUBBLED = 1 << 2;
56 
57     private static final int FLAG_PERSON_IMPORTANT = 1 << 3;
58 
59     private static final int FLAG_PERSON_BOT = 1 << 4;
60 
61     private static final int FLAG_CONTACT_STARRED = 1 << 5;
62 
63     private static final int FLAG_DEMOTED = 1 << 6;
64 
65     @IntDef(flag = true, prefix = {"FLAG_"}, value = {
66             FLAG_IMPORTANT,
67             FLAG_NOTIFICATION_SILENCED,
68             FLAG_BUBBLED,
69             FLAG_PERSON_IMPORTANT,
70             FLAG_PERSON_BOT,
71             FLAG_CONTACT_STARRED,
72             FLAG_DEMOTED,
73     })
74     @Retention(RetentionPolicy.SOURCE)
75     private @interface ConversationFlags {
76     }
77 
78     @NonNull
79     private String mShortcutId;
80 
81     @Nullable
82     private LocusId mLocusId;
83 
84     @Nullable
85     private Uri mContactUri;
86 
87     @Nullable
88     private String mContactPhoneNumber;
89 
90     @Nullable
91     private String mNotificationChannelId;
92 
93     @ShortcutFlags
94     private int mShortcutFlags;
95 
96     @ConversationFlags
97     private int mConversationFlags;
98 
ConversationInfo(Builder builder)99     private ConversationInfo(Builder builder) {
100         mShortcutId = builder.mShortcutId;
101         mLocusId = builder.mLocusId;
102         mContactUri = builder.mContactUri;
103         mContactPhoneNumber = builder.mContactPhoneNumber;
104         mNotificationChannelId = builder.mNotificationChannelId;
105         mShortcutFlags = builder.mShortcutFlags;
106         mConversationFlags = builder.mConversationFlags;
107     }
108 
109     @NonNull
getShortcutId()110     public String getShortcutId() {
111         return mShortcutId;
112     }
113 
114     @Nullable
getLocusId()115     LocusId getLocusId() {
116         return mLocusId;
117     }
118 
119     /** The URI to look up the entry in the contacts data provider. */
120     @Nullable
getContactUri()121     Uri getContactUri() {
122         return mContactUri;
123     }
124 
125     /** The phone number of the associated contact. */
126     @Nullable
getContactPhoneNumber()127     String getContactPhoneNumber() {
128         return mContactPhoneNumber;
129     }
130 
131     /**
132      * ID of the {@link android.app.NotificationChannel} where the notifications for this
133      * conversation are posted.
134      */
135     @Nullable
getNotificationChannelId()136     String getNotificationChannelId() {
137         return mNotificationChannelId;
138     }
139 
140     /** Whether the shortcut for this conversation is set long-lived by the app. */
isShortcutLongLived()141     public boolean isShortcutLongLived() {
142         return hasShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED);
143     }
144 
145     /**
146      * Whether the shortcut for this conversation is cached in Shortcut Service, with cache owner
147      * set as notifications.
148      */
isShortcutCachedForNotification()149     public boolean isShortcutCachedForNotification() {
150         return hasShortcutFlags(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
151     }
152 
153     /** Whether this conversation is marked as important by the user. */
isImportant()154     public boolean isImportant() {
155         return hasConversationFlags(FLAG_IMPORTANT);
156     }
157 
158     /** Whether the notifications for this conversation should be silenced. */
isNotificationSilenced()159     public boolean isNotificationSilenced() {
160         return hasConversationFlags(FLAG_NOTIFICATION_SILENCED);
161     }
162 
163     /** Whether the notifications for this conversation should show in bubbles. */
isBubbled()164     public boolean isBubbled() {
165         return hasConversationFlags(FLAG_BUBBLED);
166     }
167 
168     /**
169      * Whether this conversation is demoted by the user. New notifications for the demoted
170      * conversation will not show in the conversation space.
171      */
isDemoted()172     public boolean isDemoted() {
173         return hasConversationFlags(FLAG_DEMOTED);
174     }
175 
176     /** Whether the associated person is marked as important by the app. */
isPersonImportant()177     public boolean isPersonImportant() {
178         return hasConversationFlags(FLAG_PERSON_IMPORTANT);
179     }
180 
181     /** Whether the associated person is marked as a bot by the app. */
isPersonBot()182     public boolean isPersonBot() {
183         return hasConversationFlags(FLAG_PERSON_BOT);
184     }
185 
186     /** Whether the associated contact is marked as starred by the user. */
isContactStarred()187     public boolean isContactStarred() {
188         return hasConversationFlags(FLAG_CONTACT_STARRED);
189     }
190 
191     @Override
equals(Object obj)192     public boolean equals(Object obj) {
193         if (this == obj) {
194             return true;
195         }
196         if (!(obj instanceof ConversationInfo)) {
197             return false;
198         }
199         ConversationInfo other = (ConversationInfo) obj;
200         return Objects.equals(mShortcutId, other.mShortcutId)
201                 && Objects.equals(mLocusId, other.mLocusId)
202                 && Objects.equals(mContactUri, other.mContactUri)
203                 && Objects.equals(mContactPhoneNumber, other.mContactPhoneNumber)
204                 && Objects.equals(mNotificationChannelId, other.mNotificationChannelId)
205                 && mShortcutFlags == other.mShortcutFlags
206                 && mConversationFlags == other.mConversationFlags;
207     }
208 
209     @Override
hashCode()210     public int hashCode() {
211         return Objects.hash(mShortcutId, mLocusId, mContactUri, mContactPhoneNumber,
212                 mNotificationChannelId, mShortcutFlags, mConversationFlags);
213     }
214 
215     @Override
toString()216     public String toString() {
217         StringBuilder sb = new StringBuilder();
218         sb.append("ConversationInfo {");
219         sb.append("shortcutId=").append(mShortcutId);
220         sb.append(", locusId=").append(mLocusId);
221         sb.append(", contactUri=").append(mContactUri);
222         sb.append(", phoneNumber=").append(mContactPhoneNumber);
223         sb.append(", notificationChannelId=").append(mNotificationChannelId);
224         sb.append(", shortcutFlags=0x").append(Integer.toHexString(mShortcutFlags));
225         sb.append(" [");
226         if (isShortcutLongLived()) {
227             sb.append("Liv");
228         }
229         if (isShortcutCachedForNotification()) {
230             sb.append("Cac");
231         }
232         sb.append("]");
233         sb.append(", conversationFlags=0x").append(Integer.toHexString(mConversationFlags));
234         sb.append(" [");
235         if (isImportant()) {
236             sb.append("Imp");
237         }
238         if (isNotificationSilenced()) {
239             sb.append("Sil");
240         }
241         if (isBubbled()) {
242             sb.append("Bub");
243         }
244         if (isDemoted()) {
245             sb.append("Dem");
246         }
247         if (isPersonImportant()) {
248             sb.append("PIm");
249         }
250         if (isPersonBot()) {
251             sb.append("Bot");
252         }
253         if (isContactStarred()) {
254             sb.append("Sta");
255         }
256         sb.append("]}");
257         return sb.toString();
258     }
259 
hasShortcutFlags(@hortcutFlags int flags)260     private boolean hasShortcutFlags(@ShortcutFlags int flags) {
261         return (mShortcutFlags & flags) == flags;
262     }
263 
hasConversationFlags(@onversationFlags int flags)264     private boolean hasConversationFlags(@ConversationFlags int flags) {
265         return (mConversationFlags & flags) == flags;
266     }
267 
268     /** Writes field members to {@link ProtoOutputStream}. */
writeToProto(@onNull ProtoOutputStream protoOutputStream)269     void writeToProto(@NonNull ProtoOutputStream protoOutputStream) {
270         protoOutputStream.write(ConversationInfoProto.SHORTCUT_ID, mShortcutId);
271         if (mLocusId != null) {
272             long locusIdToken = protoOutputStream.start(ConversationInfoProto.LOCUS_ID_PROTO);
273             protoOutputStream.write(LocusIdProto.LOCUS_ID, mLocusId.getId());
274             protoOutputStream.end(locusIdToken);
275         }
276         if (mContactUri != null) {
277             protoOutputStream.write(ConversationInfoProto.CONTACT_URI, mContactUri.toString());
278         }
279         if (mNotificationChannelId != null) {
280             protoOutputStream.write(ConversationInfoProto.NOTIFICATION_CHANNEL_ID,
281                     mNotificationChannelId);
282         }
283         protoOutputStream.write(ConversationInfoProto.SHORTCUT_FLAGS, mShortcutFlags);
284         protoOutputStream.write(ConversationInfoProto.CONVERSATION_FLAGS, mConversationFlags);
285         if (mContactPhoneNumber != null) {
286             protoOutputStream.write(ConversationInfoProto.CONTACT_PHONE_NUMBER,
287                     mContactPhoneNumber);
288         }
289     }
290 
291     @Nullable
getBackupPayload()292     byte[] getBackupPayload() {
293         ByteArrayOutputStream baos = new ByteArrayOutputStream();
294         DataOutputStream out = new DataOutputStream(baos);
295         try {
296             out.writeUTF(mShortcutId);
297             out.writeUTF(mLocusId != null ? mLocusId.getId() : "");
298             out.writeUTF(mContactUri != null ? mContactUri.toString() : "");
299             out.writeUTF(mNotificationChannelId != null ? mNotificationChannelId : "");
300             out.writeInt(mShortcutFlags);
301             out.writeInt(mConversationFlags);
302             out.writeUTF(mContactPhoneNumber != null ? mContactPhoneNumber : "");
303         } catch (IOException e) {
304             Slog.e(TAG, "Failed to write fields to backup payload.", e);
305             return null;
306         }
307         return baos.toByteArray();
308     }
309 
310     /** Reads from {@link ProtoInputStream} and constructs a {@link ConversationInfo}. */
311     @NonNull
readFromProto(@onNull ProtoInputStream protoInputStream)312     static ConversationInfo readFromProto(@NonNull ProtoInputStream protoInputStream)
313             throws IOException {
314         ConversationInfo.Builder builder = new ConversationInfo.Builder();
315         while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
316             switch (protoInputStream.getFieldNumber()) {
317                 case (int) ConversationInfoProto.SHORTCUT_ID:
318                     builder.setShortcutId(
319                             protoInputStream.readString(ConversationInfoProto.SHORTCUT_ID));
320                     break;
321                 case (int) ConversationInfoProto.LOCUS_ID_PROTO:
322                     long locusIdToken = protoInputStream.start(
323                             ConversationInfoProto.LOCUS_ID_PROTO);
324                     while (protoInputStream.nextField()
325                             != ProtoInputStream.NO_MORE_FIELDS) {
326                         if (protoInputStream.getFieldNumber() == (int) LocusIdProto.LOCUS_ID) {
327                             builder.setLocusId(new LocusId(
328                                     protoInputStream.readString(LocusIdProto.LOCUS_ID)));
329                         }
330                     }
331                     protoInputStream.end(locusIdToken);
332                     break;
333                 case (int) ConversationInfoProto.CONTACT_URI:
334                     builder.setContactUri(Uri.parse(protoInputStream.readString(
335                             ConversationInfoProto.CONTACT_URI)));
336                     break;
337                 case (int) ConversationInfoProto.NOTIFICATION_CHANNEL_ID:
338                     builder.setNotificationChannelId(protoInputStream.readString(
339                             ConversationInfoProto.NOTIFICATION_CHANNEL_ID));
340                     break;
341                 case (int) ConversationInfoProto.SHORTCUT_FLAGS:
342                     builder.setShortcutFlags(protoInputStream.readInt(
343                             ConversationInfoProto.SHORTCUT_FLAGS));
344                     break;
345                 case (int) ConversationInfoProto.CONVERSATION_FLAGS:
346                     builder.setConversationFlags(protoInputStream.readInt(
347                             ConversationInfoProto.CONVERSATION_FLAGS));
348                     break;
349                 case (int) ConversationInfoProto.CONTACT_PHONE_NUMBER:
350                     builder.setContactPhoneNumber(protoInputStream.readString(
351                             ConversationInfoProto.CONTACT_PHONE_NUMBER));
352                     break;
353                 default:
354                     Slog.w(TAG, "Could not read undefined field: "
355                             + protoInputStream.getFieldNumber());
356             }
357         }
358         return builder.build();
359     }
360 
361     @Nullable
readFromBackupPayload(@onNull byte[] payload)362     static ConversationInfo readFromBackupPayload(@NonNull byte[] payload) {
363         ConversationInfo.Builder builder = new ConversationInfo.Builder();
364         DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
365         try {
366             builder.setShortcutId(in.readUTF());
367             String locusId = in.readUTF();
368             if (!TextUtils.isEmpty(locusId)) {
369                 builder.setLocusId(new LocusId(locusId));
370             }
371             String contactUri = in.readUTF();
372             if (!TextUtils.isEmpty(contactUri)) {
373                 builder.setContactUri(Uri.parse(contactUri));
374             }
375             String notificationChannelId = in.readUTF();
376             if (!TextUtils.isEmpty(notificationChannelId)) {
377                 builder.setNotificationChannelId(notificationChannelId);
378             }
379             builder.setShortcutFlags(in.readInt());
380             builder.setConversationFlags(in.readInt());
381             String contactPhoneNumber = in.readUTF();
382             if (!TextUtils.isEmpty(contactPhoneNumber)) {
383                 builder.setContactPhoneNumber(contactPhoneNumber);
384             }
385         } catch (IOException e) {
386             Slog.e(TAG, "Failed to read conversation info fields from backup payload.", e);
387             return null;
388         }
389         return builder.build();
390     }
391 
392     /**
393      * Builder class for {@link ConversationInfo} objects.
394      */
395     static class Builder {
396 
397         private String mShortcutId;
398 
399         @Nullable
400         private LocusId mLocusId;
401 
402         @Nullable
403         private Uri mContactUri;
404 
405         @Nullable
406         private String mContactPhoneNumber;
407 
408         @Nullable
409         private String mNotificationChannelId;
410 
411         @ShortcutFlags
412         private int mShortcutFlags;
413 
414         @ConversationFlags
415         private int mConversationFlags;
416 
Builder()417         Builder() {
418         }
419 
Builder(@onNull ConversationInfo conversationInfo)420         Builder(@NonNull ConversationInfo conversationInfo) {
421             if (mShortcutId == null) {
422                 mShortcutId = conversationInfo.mShortcutId;
423             } else {
424                 Preconditions.checkArgument(mShortcutId.equals(conversationInfo.mShortcutId));
425             }
426             mLocusId = conversationInfo.mLocusId;
427             mContactUri = conversationInfo.mContactUri;
428             mContactPhoneNumber = conversationInfo.mContactPhoneNumber;
429             mNotificationChannelId = conversationInfo.mNotificationChannelId;
430             mShortcutFlags = conversationInfo.mShortcutFlags;
431             mConversationFlags = conversationInfo.mConversationFlags;
432         }
433 
setShortcutId(@onNull String shortcutId)434         Builder setShortcutId(@NonNull String shortcutId) {
435             mShortcutId = shortcutId;
436             return this;
437         }
438 
setLocusId(LocusId locusId)439         Builder setLocusId(LocusId locusId) {
440             mLocusId = locusId;
441             return this;
442         }
443 
setContactUri(Uri contactUri)444         Builder setContactUri(Uri contactUri) {
445             mContactUri = contactUri;
446             return this;
447         }
448 
setContactPhoneNumber(String phoneNumber)449         Builder setContactPhoneNumber(String phoneNumber) {
450             mContactPhoneNumber = phoneNumber;
451             return this;
452         }
453 
setNotificationChannelId(String notificationChannelId)454         Builder setNotificationChannelId(String notificationChannelId) {
455             mNotificationChannelId = notificationChannelId;
456             return this;
457         }
458 
setShortcutFlags(@hortcutFlags int shortcutFlags)459         Builder setShortcutFlags(@ShortcutFlags int shortcutFlags) {
460             mShortcutFlags = shortcutFlags;
461             return this;
462         }
463 
setConversationFlags(@onversationFlags int conversationFlags)464         Builder setConversationFlags(@ConversationFlags int conversationFlags) {
465             mConversationFlags = conversationFlags;
466             return this;
467         }
468 
setImportant(boolean value)469         Builder setImportant(boolean value) {
470             return setConversationFlag(FLAG_IMPORTANT, value);
471         }
472 
setNotificationSilenced(boolean value)473         Builder setNotificationSilenced(boolean value) {
474             return setConversationFlag(FLAG_NOTIFICATION_SILENCED, value);
475         }
476 
setBubbled(boolean value)477         Builder setBubbled(boolean value) {
478             return setConversationFlag(FLAG_BUBBLED, value);
479         }
480 
setDemoted(boolean value)481         Builder setDemoted(boolean value) {
482             return setConversationFlag(FLAG_DEMOTED, value);
483         }
484 
setPersonImportant(boolean value)485         Builder setPersonImportant(boolean value) {
486             return setConversationFlag(FLAG_PERSON_IMPORTANT, value);
487         }
488 
setPersonBot(boolean value)489         Builder setPersonBot(boolean value) {
490             return setConversationFlag(FLAG_PERSON_BOT, value);
491         }
492 
setContactStarred(boolean value)493         Builder setContactStarred(boolean value) {
494             return setConversationFlag(FLAG_CONTACT_STARRED, value);
495         }
496 
setConversationFlag(@onversationFlags int flags, boolean value)497         private Builder setConversationFlag(@ConversationFlags int flags, boolean value) {
498             if (value) {
499                 return addConversationFlags(flags);
500             } else {
501                 return removeConversationFlags(flags);
502             }
503         }
504 
addConversationFlags(@onversationFlags int flags)505         private Builder addConversationFlags(@ConversationFlags int flags) {
506             mConversationFlags |= flags;
507             return this;
508         }
509 
removeConversationFlags(@onversationFlags int flags)510         private Builder removeConversationFlags(@ConversationFlags int flags) {
511             mConversationFlags &= ~flags;
512             return this;
513         }
514 
build()515         ConversationInfo build() {
516             Objects.requireNonNull(mShortcutId);
517             return new ConversationInfo(this);
518         }
519     }
520 }
521