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