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