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