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.app.people.ConversationStatus;
23 import android.content.LocusId;
24 import android.content.LocusIdProto;
25 import android.content.pm.ShortcutInfo;
26 import android.content.pm.ShortcutInfo.ShortcutFlags;
27 import android.net.Uri;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.util.Slog;
31 import android.util.proto.ProtoInputStream;
32 import android.util.proto.ProtoOutputStream;
33 
34 import com.android.internal.util.Preconditions;
35 import com.android.server.people.ConversationInfoProto;
36 
37 import java.io.ByteArrayInputStream;
38 import java.io.ByteArrayOutputStream;
39 import java.io.DataInputStream;
40 import java.io.DataOutputStream;
41 import java.io.EOFException;
42 import java.io.IOException;
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 import java.util.Collection;
46 import java.util.HashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Objects;
50 
51 /**
52  * Represents a conversation that is provided by the app based on {@link ShortcutInfo}.
53  */
54 public class ConversationInfo {
55     private static final boolean DEBUG = false;
56 
57     // Schema version for the backup payload. Must be incremented whenever fields are added in
58     // backup payload.
59     private static final int VERSION = 1;
60 
61     private static final String TAG = ConversationInfo.class.getSimpleName();
62 
63     private static final int FLAG_IMPORTANT = 1;
64 
65     private static final int FLAG_NOTIFICATION_SILENCED = 1 << 1;
66 
67     private static final int FLAG_BUBBLED = 1 << 2;
68 
69     private static final int FLAG_PERSON_IMPORTANT = 1 << 3;
70 
71     private static final int FLAG_PERSON_BOT = 1 << 4;
72 
73     private static final int FLAG_CONTACT_STARRED = 1 << 5;
74 
75     private static final int FLAG_DEMOTED = 1 << 6;
76 
77     @IntDef(flag = true, prefix = {"FLAG_"}, value = {
78             FLAG_IMPORTANT,
79             FLAG_NOTIFICATION_SILENCED,
80             FLAG_BUBBLED,
81             FLAG_PERSON_IMPORTANT,
82             FLAG_PERSON_BOT,
83             FLAG_CONTACT_STARRED,
84             FLAG_DEMOTED,
85     })
86     @Retention(RetentionPolicy.SOURCE)
87     private @interface ConversationFlags {
88     }
89 
90     @NonNull
91     private String mShortcutId;
92 
93     @Nullable
94     private LocusId mLocusId;
95 
96     @Nullable
97     private Uri mContactUri;
98 
99     @Nullable
100     private String mContactPhoneNumber;
101 
102     @Nullable
103     private String mNotificationChannelId;
104 
105     @Nullable
106     private String mParentNotificationChannelId;
107 
108     private long mLastEventTimestamp;
109 
110     private long mCreationTimestamp;
111 
112     @ShortcutFlags
113     private int mShortcutFlags;
114 
115     @ConversationFlags
116     private int mConversationFlags;
117 
118     private Map<String, ConversationStatus> mCurrStatuses;
119 
ConversationInfo(Builder builder)120     private ConversationInfo(Builder builder) {
121         mShortcutId = builder.mShortcutId;
122         mLocusId = builder.mLocusId;
123         mContactUri = builder.mContactUri;
124         mContactPhoneNumber = builder.mContactPhoneNumber;
125         mNotificationChannelId = builder.mNotificationChannelId;
126         mParentNotificationChannelId = builder.mParentNotificationChannelId;
127         mLastEventTimestamp = builder.mLastEventTimestamp;
128         mCreationTimestamp = builder.mCreationTimestamp;
129         mShortcutFlags = builder.mShortcutFlags;
130         mConversationFlags = builder.mConversationFlags;
131         mCurrStatuses = builder.mCurrStatuses;
132     }
133 
134     @NonNull
getShortcutId()135     public String getShortcutId() {
136         return mShortcutId;
137     }
138 
139     @Nullable
getLocusId()140     LocusId getLocusId() {
141         return mLocusId;
142     }
143 
144     /** The URI to look up the entry in the contacts data provider. */
145     @Nullable
getContactUri()146     Uri getContactUri() {
147         return mContactUri;
148     }
149 
150     /** The phone number of the associated contact. */
151     @Nullable
getContactPhoneNumber()152     String getContactPhoneNumber() {
153         return mContactPhoneNumber;
154     }
155 
156     /**
157      * ID of the conversation-specific {@link android.app.NotificationChannel} where the
158      * notifications for this conversation are posted.
159      */
160     @Nullable
getNotificationChannelId()161     String getNotificationChannelId() {
162         return mNotificationChannelId;
163     }
164 
165     /**
166      * ID of the parent {@link android.app.NotificationChannel} for this conversation. This is the
167      * notification channel where the notifications are posted before this conversation is
168      * customized by the user.
169      */
170     @Nullable
getParentNotificationChannelId()171     String getParentNotificationChannelId() {
172         return mParentNotificationChannelId;
173     }
174 
175     /**
176      * Timestamp of the last event, {@code 0L} if there are no events. This timestamp is for
177      * identifying and sorting the recent conversations. It may only count a subset of event types.
178      */
getLastEventTimestamp()179     long getLastEventTimestamp() {
180         return mLastEventTimestamp;
181     }
182 
183     /**
184      * Timestamp of the creation of the conversationInfo.
185      */
getCreationTimestamp()186     long getCreationTimestamp() {
187         return mCreationTimestamp;
188     }
189 
190     /** Whether the shortcut for this conversation is set long-lived by the app. */
isShortcutLongLived()191     public boolean isShortcutLongLived() {
192         return hasShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED);
193     }
194 
195     /**
196      * Whether the shortcut for this conversation is cached in Shortcut Service, with cache owner
197      * set as notifications.
198      */
isShortcutCachedForNotification()199     public boolean isShortcutCachedForNotification() {
200         return hasShortcutFlags(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
201     }
202 
203     /** Whether this conversation is marked as important by the user. */
isImportant()204     public boolean isImportant() {
205         return hasConversationFlags(FLAG_IMPORTANT);
206     }
207 
208     /** Whether the notifications for this conversation should be silenced. */
isNotificationSilenced()209     public boolean isNotificationSilenced() {
210         return hasConversationFlags(FLAG_NOTIFICATION_SILENCED);
211     }
212 
213     /** Whether the notifications for this conversation should show in bubbles. */
isBubbled()214     public boolean isBubbled() {
215         return hasConversationFlags(FLAG_BUBBLED);
216     }
217 
218     /**
219      * Whether this conversation is demoted by the user. New notifications for the demoted
220      * conversation will not show in the conversation space.
221      */
isDemoted()222     public boolean isDemoted() {
223         return hasConversationFlags(FLAG_DEMOTED);
224     }
225 
226     /** Whether the associated person is marked as important by the app. */
isPersonImportant()227     public boolean isPersonImportant() {
228         return hasConversationFlags(FLAG_PERSON_IMPORTANT);
229     }
230 
231     /** Whether the associated person is marked as a bot by the app. */
isPersonBot()232     public boolean isPersonBot() {
233         return hasConversationFlags(FLAG_PERSON_BOT);
234     }
235 
236     /** Whether the associated contact is marked as starred by the user. */
isContactStarred()237     public boolean isContactStarred() {
238         return hasConversationFlags(FLAG_CONTACT_STARRED);
239     }
240 
getStatuses()241     public Collection<ConversationStatus> getStatuses() {
242         return mCurrStatuses.values();
243     }
244 
245     @Override
equals(Object obj)246     public boolean equals(Object obj) {
247         if (this == obj) {
248             return true;
249         }
250         if (!(obj instanceof ConversationInfo)) {
251             return false;
252         }
253         ConversationInfo other = (ConversationInfo) obj;
254         return Objects.equals(mShortcutId, other.mShortcutId)
255                 && Objects.equals(mLocusId, other.mLocusId)
256                 && Objects.equals(mContactUri, other.mContactUri)
257                 && Objects.equals(mContactPhoneNumber, other.mContactPhoneNumber)
258                 && Objects.equals(mNotificationChannelId, other.mNotificationChannelId)
259                 && Objects.equals(mParentNotificationChannelId, other.mParentNotificationChannelId)
260                 && Objects.equals(mLastEventTimestamp, other.mLastEventTimestamp)
261                 && mCreationTimestamp == other.mCreationTimestamp
262                 && mShortcutFlags == other.mShortcutFlags
263                 && mConversationFlags == other.mConversationFlags
264                 && Objects.equals(mCurrStatuses, other.mCurrStatuses);
265     }
266 
267     @Override
hashCode()268     public int hashCode() {
269         return Objects.hash(mShortcutId, mLocusId, mContactUri, mContactPhoneNumber,
270                 mNotificationChannelId, mParentNotificationChannelId, mLastEventTimestamp,
271                 mCreationTimestamp, mShortcutFlags, mConversationFlags, mCurrStatuses);
272     }
273 
274     @Override
toString()275     public String toString() {
276         StringBuilder sb = new StringBuilder();
277         sb.append("ConversationInfo {");
278         sb.append("shortcutId=").append(mShortcutId);
279         sb.append(", locusId=").append(mLocusId);
280         sb.append(", contactUri=").append(mContactUri);
281         sb.append(", phoneNumber=").append(mContactPhoneNumber);
282         sb.append(", notificationChannelId=").append(mNotificationChannelId);
283         sb.append(", parentNotificationChannelId=").append(mParentNotificationChannelId);
284         sb.append(", lastEventTimestamp=").append(mLastEventTimestamp);
285         sb.append(", creationTimestamp=").append(mCreationTimestamp);
286         sb.append(", statuses=").append(mCurrStatuses);
287         sb.append(", shortcutFlags=0x").append(Integer.toHexString(mShortcutFlags));
288         sb.append(" [");
289         if (isShortcutLongLived()) {
290             sb.append("Liv");
291         }
292         if (isShortcutCachedForNotification()) {
293             sb.append("Cac");
294         }
295         sb.append("]");
296         sb.append(", conversationFlags=0x").append(Integer.toHexString(mConversationFlags));
297         sb.append(" [");
298         if (isImportant()) {
299             sb.append("Imp");
300         }
301         if (isNotificationSilenced()) {
302             sb.append("Sil");
303         }
304         if (isBubbled()) {
305             sb.append("Bub");
306         }
307         if (isDemoted()) {
308             sb.append("Dem");
309         }
310         if (isPersonImportant()) {
311             sb.append("PIm");
312         }
313         if (isPersonBot()) {
314             sb.append("Bot");
315         }
316         if (isContactStarred()) {
317             sb.append("Sta");
318         }
319         sb.append("]}");
320         return sb.toString();
321     }
322 
hasShortcutFlags(@hortcutFlags int flags)323     private boolean hasShortcutFlags(@ShortcutFlags int flags) {
324         return (mShortcutFlags & flags) == flags;
325     }
326 
hasConversationFlags(@onversationFlags int flags)327     private boolean hasConversationFlags(@ConversationFlags int flags) {
328         return (mConversationFlags & flags) == flags;
329     }
330 
331     /** Writes field members to {@link ProtoOutputStream}. */
writeToProto(@onNull ProtoOutputStream protoOutputStream)332     void writeToProto(@NonNull ProtoOutputStream protoOutputStream) {
333         protoOutputStream.write(ConversationInfoProto.SHORTCUT_ID, mShortcutId);
334         if (mLocusId != null) {
335             long locusIdToken = protoOutputStream.start(ConversationInfoProto.LOCUS_ID_PROTO);
336             protoOutputStream.write(LocusIdProto.LOCUS_ID, mLocusId.getId());
337             protoOutputStream.end(locusIdToken);
338         }
339         if (mContactUri != null) {
340             protoOutputStream.write(ConversationInfoProto.CONTACT_URI, mContactUri.toString());
341         }
342         if (mNotificationChannelId != null) {
343             protoOutputStream.write(ConversationInfoProto.NOTIFICATION_CHANNEL_ID,
344                     mNotificationChannelId);
345         }
346         if (mParentNotificationChannelId != null) {
347             protoOutputStream.write(ConversationInfoProto.PARENT_NOTIFICATION_CHANNEL_ID,
348                     mParentNotificationChannelId);
349         }
350         protoOutputStream.write(ConversationInfoProto.LAST_EVENT_TIMESTAMP, mLastEventTimestamp);
351         protoOutputStream.write(ConversationInfoProto.CREATION_TIMESTAMP, mCreationTimestamp);
352         protoOutputStream.write(ConversationInfoProto.SHORTCUT_FLAGS, mShortcutFlags);
353         protoOutputStream.write(ConversationInfoProto.CONVERSATION_FLAGS, mConversationFlags);
354         if (mContactPhoneNumber != null) {
355             protoOutputStream.write(ConversationInfoProto.CONTACT_PHONE_NUMBER,
356                     mContactPhoneNumber);
357         }
358         // ConversationStatus is a transient object and not persisted
359     }
360 
361     @Nullable
getBackupPayload()362     byte[] getBackupPayload() {
363         ByteArrayOutputStream baos = new ByteArrayOutputStream();
364         DataOutputStream out = new DataOutputStream(baos);
365         try {
366             out.writeUTF(mShortcutId);
367             out.writeUTF(mLocusId != null ? mLocusId.getId() : "");
368             out.writeUTF(mContactUri != null ? mContactUri.toString() : "");
369             out.writeUTF(mNotificationChannelId != null ? mNotificationChannelId : "");
370             out.writeInt(mShortcutFlags);
371             out.writeInt(mConversationFlags);
372             out.writeUTF(mContactPhoneNumber != null ? mContactPhoneNumber : "");
373             out.writeUTF(mParentNotificationChannelId != null ? mParentNotificationChannelId : "");
374             out.writeLong(mLastEventTimestamp);
375             out.writeInt(VERSION);
376             out.writeLong(mCreationTimestamp);
377             // ConversationStatus is a transient object and not persisted
378         } catch (IOException e) {
379             Slog.e(TAG, "Failed to write fields to backup payload.", e);
380             return null;
381         }
382         return baos.toByteArray();
383     }
384 
385     /** Reads from {@link ProtoInputStream} and constructs a {@link ConversationInfo}. */
386     @NonNull
readFromProto(@onNull ProtoInputStream protoInputStream)387     static ConversationInfo readFromProto(@NonNull ProtoInputStream protoInputStream)
388             throws IOException {
389         ConversationInfo.Builder builder = new ConversationInfo.Builder();
390         while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
391             switch (protoInputStream.getFieldNumber()) {
392                 case (int) ConversationInfoProto.SHORTCUT_ID:
393                     builder.setShortcutId(
394                             protoInputStream.readString(ConversationInfoProto.SHORTCUT_ID));
395                     break;
396                 case (int) ConversationInfoProto.LOCUS_ID_PROTO:
397                     long locusIdToken = protoInputStream.start(
398                             ConversationInfoProto.LOCUS_ID_PROTO);
399                     while (protoInputStream.nextField()
400                             != ProtoInputStream.NO_MORE_FIELDS) {
401                         if (protoInputStream.getFieldNumber() == (int) LocusIdProto.LOCUS_ID) {
402                             builder.setLocusId(new LocusId(
403                                     protoInputStream.readString(LocusIdProto.LOCUS_ID)));
404                         }
405                     }
406                     protoInputStream.end(locusIdToken);
407                     break;
408                 case (int) ConversationInfoProto.CONTACT_URI:
409                     builder.setContactUri(Uri.parse(protoInputStream.readString(
410                             ConversationInfoProto.CONTACT_URI)));
411                     break;
412                 case (int) ConversationInfoProto.NOTIFICATION_CHANNEL_ID:
413                     builder.setNotificationChannelId(protoInputStream.readString(
414                             ConversationInfoProto.NOTIFICATION_CHANNEL_ID));
415                     break;
416                 case (int) ConversationInfoProto.PARENT_NOTIFICATION_CHANNEL_ID:
417                     builder.setParentNotificationChannelId(protoInputStream.readString(
418                             ConversationInfoProto.PARENT_NOTIFICATION_CHANNEL_ID));
419                     break;
420                 case (int) ConversationInfoProto.LAST_EVENT_TIMESTAMP:
421                     builder.setLastEventTimestamp(protoInputStream.readLong(
422                             ConversationInfoProto.LAST_EVENT_TIMESTAMP));
423                     break;
424                 case (int) ConversationInfoProto.CREATION_TIMESTAMP:
425                     builder.setCreationTimestamp(protoInputStream.readLong(
426                             ConversationInfoProto.CREATION_TIMESTAMP));
427                     break;
428                 case (int) ConversationInfoProto.SHORTCUT_FLAGS:
429                     builder.setShortcutFlags(protoInputStream.readInt(
430                             ConversationInfoProto.SHORTCUT_FLAGS));
431                     break;
432                 case (int) ConversationInfoProto.CONVERSATION_FLAGS:
433                     builder.setConversationFlags(protoInputStream.readInt(
434                             ConversationInfoProto.CONVERSATION_FLAGS));
435                     break;
436                 case (int) ConversationInfoProto.CONTACT_PHONE_NUMBER:
437                     builder.setContactPhoneNumber(protoInputStream.readString(
438                             ConversationInfoProto.CONTACT_PHONE_NUMBER));
439                     break;
440                 default:
441                     Slog.w(TAG, "Could not read undefined field: "
442                             + protoInputStream.getFieldNumber());
443             }
444         }
445         return builder.build();
446     }
447 
448     @Nullable
readFromBackupPayload(@onNull byte[] payload)449     static ConversationInfo readFromBackupPayload(@NonNull byte[] payload) {
450         ConversationInfo.Builder builder = new ConversationInfo.Builder();
451         DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
452         try {
453             builder.setShortcutId(in.readUTF());
454             String locusId = in.readUTF();
455             if (!TextUtils.isEmpty(locusId)) {
456                 builder.setLocusId(new LocusId(locusId));
457             }
458             String contactUri = in.readUTF();
459             if (!TextUtils.isEmpty(contactUri)) {
460                 builder.setContactUri(Uri.parse(contactUri));
461             }
462             String notificationChannelId = in.readUTF();
463             if (!TextUtils.isEmpty(notificationChannelId)) {
464                 builder.setNotificationChannelId(notificationChannelId);
465             }
466             builder.setShortcutFlags(in.readInt());
467             builder.setConversationFlags(in.readInt());
468             String contactPhoneNumber = in.readUTF();
469             if (!TextUtils.isEmpty(contactPhoneNumber)) {
470                 builder.setContactPhoneNumber(contactPhoneNumber);
471             }
472             String parentNotificationChannelId = in.readUTF();
473             if (!TextUtils.isEmpty(parentNotificationChannelId)) {
474                 builder.setParentNotificationChannelId(parentNotificationChannelId);
475             }
476             builder.setLastEventTimestamp(in.readLong());
477             int payloadVersion = maybeReadVersion(in);
478             if (payloadVersion == 1) {
479                 builder.setCreationTimestamp(in.readLong());
480             }
481         } catch (IOException e) {
482             Slog.e(TAG, "Failed to read conversation info fields from backup payload.", e);
483             return null;
484         }
485         return builder.build();
486     }
487 
maybeReadVersion(DataInputStream in)488     private static int maybeReadVersion(DataInputStream in) throws IOException {
489         try {
490             return in.readInt();
491         } catch (EOFException eofException) {
492             // EOF is expected if using old backup payload protocol.
493             if (DEBUG) Log.d(TAG, "Eof reached for data stream, missing version number");
494             return 0;
495         }
496     }
497 
498     /**
499      * Builder class for {@link ConversationInfo} objects.
500      */
501     static class Builder {
502 
503         private String mShortcutId;
504 
505         @Nullable
506         private LocusId mLocusId;
507 
508         @Nullable
509         private Uri mContactUri;
510 
511         @Nullable
512         private String mContactPhoneNumber;
513 
514         @Nullable
515         private String mNotificationChannelId;
516 
517         @Nullable
518         private String mParentNotificationChannelId;
519 
520         private long mLastEventTimestamp;
521 
522         private long mCreationTimestamp;
523 
524         @ShortcutFlags
525         private int mShortcutFlags;
526 
527         @ConversationFlags
528         private int mConversationFlags;
529 
530         private Map<String, ConversationStatus> mCurrStatuses = new HashMap<>();
531 
Builder()532         Builder() {
533         }
534 
Builder(@onNull ConversationInfo conversationInfo)535         Builder(@NonNull ConversationInfo conversationInfo) {
536             if (mShortcutId == null) {
537                 mShortcutId = conversationInfo.mShortcutId;
538             } else {
539                 Preconditions.checkArgument(mShortcutId.equals(conversationInfo.mShortcutId));
540             }
541             mLocusId = conversationInfo.mLocusId;
542             mContactUri = conversationInfo.mContactUri;
543             mContactPhoneNumber = conversationInfo.mContactPhoneNumber;
544             mNotificationChannelId = conversationInfo.mNotificationChannelId;
545             mParentNotificationChannelId = conversationInfo.mParentNotificationChannelId;
546             mLastEventTimestamp = conversationInfo.mLastEventTimestamp;
547             mCreationTimestamp = conversationInfo.mCreationTimestamp;
548             mShortcutFlags = conversationInfo.mShortcutFlags;
549             mConversationFlags = conversationInfo.mConversationFlags;
550             mCurrStatuses = conversationInfo.mCurrStatuses;
551         }
552 
setShortcutId(@onNull String shortcutId)553         Builder setShortcutId(@NonNull String shortcutId) {
554             mShortcutId = shortcutId;
555             return this;
556         }
557 
setLocusId(LocusId locusId)558         Builder setLocusId(LocusId locusId) {
559             mLocusId = locusId;
560             return this;
561         }
562 
setContactUri(Uri contactUri)563         Builder setContactUri(Uri contactUri) {
564             mContactUri = contactUri;
565             return this;
566         }
567 
setContactPhoneNumber(String phoneNumber)568         Builder setContactPhoneNumber(String phoneNumber) {
569             mContactPhoneNumber = phoneNumber;
570             return this;
571         }
572 
setNotificationChannelId(String notificationChannelId)573         Builder setNotificationChannelId(String notificationChannelId) {
574             mNotificationChannelId = notificationChannelId;
575             return this;
576         }
577 
setParentNotificationChannelId(String parentNotificationChannelId)578         Builder setParentNotificationChannelId(String parentNotificationChannelId) {
579             mParentNotificationChannelId = parentNotificationChannelId;
580             return this;
581         }
582 
setLastEventTimestamp(long lastEventTimestamp)583         Builder setLastEventTimestamp(long lastEventTimestamp) {
584             mLastEventTimestamp = lastEventTimestamp;
585             return this;
586         }
587 
setCreationTimestamp(long creationTimestamp)588         Builder setCreationTimestamp(long creationTimestamp) {
589             mCreationTimestamp = creationTimestamp;
590             return this;
591         }
592 
setShortcutFlags(@hortcutFlags int shortcutFlags)593         Builder setShortcutFlags(@ShortcutFlags int shortcutFlags) {
594             mShortcutFlags = shortcutFlags;
595             return this;
596         }
597 
setConversationFlags(@onversationFlags int conversationFlags)598         Builder setConversationFlags(@ConversationFlags int conversationFlags) {
599             mConversationFlags = conversationFlags;
600             return this;
601         }
602 
setImportant(boolean value)603         Builder setImportant(boolean value) {
604             return setConversationFlag(FLAG_IMPORTANT, value);
605         }
606 
setNotificationSilenced(boolean value)607         Builder setNotificationSilenced(boolean value) {
608             return setConversationFlag(FLAG_NOTIFICATION_SILENCED, value);
609         }
610 
setBubbled(boolean value)611         Builder setBubbled(boolean value) {
612             return setConversationFlag(FLAG_BUBBLED, value);
613         }
614 
setDemoted(boolean value)615         Builder setDemoted(boolean value) {
616             return setConversationFlag(FLAG_DEMOTED, value);
617         }
618 
setPersonImportant(boolean value)619         Builder setPersonImportant(boolean value) {
620             return setConversationFlag(FLAG_PERSON_IMPORTANT, value);
621         }
622 
setPersonBot(boolean value)623         Builder setPersonBot(boolean value) {
624             return setConversationFlag(FLAG_PERSON_BOT, value);
625         }
626 
setContactStarred(boolean value)627         Builder setContactStarred(boolean value) {
628             return setConversationFlag(FLAG_CONTACT_STARRED, value);
629         }
630 
setConversationFlag(@onversationFlags int flags, boolean value)631         private Builder setConversationFlag(@ConversationFlags int flags, boolean value) {
632             if (value) {
633                 return addConversationFlags(flags);
634             } else {
635                 return removeConversationFlags(flags);
636             }
637         }
638 
addConversationFlags(@onversationFlags int flags)639         private Builder addConversationFlags(@ConversationFlags int flags) {
640             mConversationFlags |= flags;
641             return this;
642         }
643 
removeConversationFlags(@onversationFlags int flags)644         private Builder removeConversationFlags(@ConversationFlags int flags) {
645             mConversationFlags &= ~flags;
646             return this;
647         }
648 
setStatuses(List<ConversationStatus> statuses)649         Builder setStatuses(List<ConversationStatus> statuses) {
650             mCurrStatuses.clear();
651             if (statuses != null) {
652                 for (ConversationStatus status : statuses) {
653                     mCurrStatuses.put(status.getId(), status);
654                 }
655             }
656             return this;
657         }
658 
addOrUpdateStatus(ConversationStatus status)659         Builder addOrUpdateStatus(ConversationStatus status) {
660             mCurrStatuses.put(status.getId(), status);
661             return this;
662         }
663 
clearStatus(String statusId)664         Builder clearStatus(String statusId) {
665             mCurrStatuses.remove(statusId);
666             return this;
667         }
668 
build()669         ConversationInfo build() {
670             Objects.requireNonNull(mShortcutId);
671             return new ConversationInfo(this);
672         }
673     }
674 }
675