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