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.systemui.people; 18 19 import static com.android.systemui.people.NotificationHelper.getContactUri; 20 import static com.android.systemui.people.NotificationHelper.getMessagingStyleMessages; 21 import static com.android.systemui.people.NotificationHelper.getSenderIfGroupConversation; 22 import static com.android.systemui.people.NotificationHelper.hasReadContactsPermission; 23 import static com.android.systemui.people.NotificationHelper.isMissedCall; 24 import static com.android.systemui.people.NotificationHelper.shouldMatchNotificationByUri; 25 26 import android.annotation.Nullable; 27 import android.app.Notification; 28 import android.app.backup.BackupManager; 29 import android.app.people.ConversationChannel; 30 import android.app.people.IPeopleManager; 31 import android.app.people.PeopleSpaceTile; 32 import android.content.Context; 33 import android.content.SharedPreferences; 34 import android.content.pm.LauncherApps; 35 import android.content.pm.PackageManager; 36 import android.content.pm.ShortcutInfo; 37 import android.database.Cursor; 38 import android.database.SQLException; 39 import android.graphics.Bitmap; 40 import android.graphics.Canvas; 41 import android.graphics.drawable.BitmapDrawable; 42 import android.graphics.drawable.Drawable; 43 import android.net.Uri; 44 import android.os.UserManager; 45 import android.provider.ContactsContract; 46 import android.service.notification.StatusBarNotification; 47 import android.text.TextUtils; 48 import android.util.Log; 49 50 import androidx.preference.PreferenceManager; 51 52 import com.android.internal.annotations.VisibleForTesting; 53 import com.android.internal.logging.UiEvent; 54 import com.android.internal.logging.UiEventLogger; 55 import com.android.internal.util.ArrayUtils; 56 import com.android.internal.widget.MessagingMessage; 57 import com.android.settingslib.utils.ThreadUtils; 58 import com.android.systemui.R; 59 import com.android.systemui.people.widget.PeopleSpaceWidgetManager; 60 import com.android.systemui.people.widget.PeopleTileKey; 61 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 62 63 import java.text.SimpleDateFormat; 64 import java.util.ArrayList; 65 import java.util.Date; 66 import java.util.HashSet; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Objects; 70 import java.util.Optional; 71 import java.util.Set; 72 import java.util.stream.Collectors; 73 import java.util.stream.Stream; 74 75 /** Utils class for People Space. */ 76 public class PeopleSpaceUtils { 77 /** Turns on debugging information about People Space. */ 78 public static final boolean DEBUG = false; 79 80 public static final String PACKAGE_NAME = "package_name"; 81 public static final String USER_ID = "user_id"; 82 public static final String SHORTCUT_ID = "shortcut_id"; 83 public static final String EMPTY_STRING = ""; 84 public static final int INVALID_USER_ID = -1; 85 public static final PeopleTileKey EMPTY_KEY = 86 new PeopleTileKey(EMPTY_STRING, INVALID_USER_ID, EMPTY_STRING); 87 static final float STARRED_CONTACT = 1f; 88 static final float VALID_CONTACT = .5f; 89 static final float DEFAULT_AFFINITY = 0f; 90 private static final String TAG = "PeopleSpaceUtils"; 91 92 /** Returns stored widgets for the conversation specified. */ getStoredWidgetIds(SharedPreferences sp, PeopleTileKey key)93 public static Set<String> getStoredWidgetIds(SharedPreferences sp, PeopleTileKey key) { 94 if (!PeopleTileKey.isValid(key)) { 95 return new HashSet<>(); 96 } 97 return new HashSet<>(sp.getStringSet(key.toString(), new HashSet<>())); 98 } 99 100 /** Sets all relevant storage for {@code appWidgetId} association to {@code tile}. */ setSharedPreferencesStorageForTile(Context context, PeopleTileKey key, int appWidgetId, Uri contactUri, BackupManager backupManager)101 public static void setSharedPreferencesStorageForTile(Context context, PeopleTileKey key, 102 int appWidgetId, Uri contactUri, BackupManager backupManager) { 103 if (!PeopleTileKey.isValid(key)) { 104 Log.e(TAG, "Not storing for invalid key"); 105 return; 106 } 107 // Write relevant persisted storage. 108 SharedPreferences widgetSp = context.getSharedPreferences(String.valueOf(appWidgetId), 109 Context.MODE_PRIVATE); 110 SharedPreferencesHelper.setPeopleTileKey(widgetSp, key); 111 112 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 113 SharedPreferences.Editor editor = sp.edit(); 114 String contactUriString = contactUri == null ? EMPTY_STRING : contactUri.toString(); 115 editor.putString(String.valueOf(appWidgetId), contactUriString); 116 117 // Don't overwrite existing widgets with the same key. 118 addAppWidgetIdForKey(sp, editor, appWidgetId, key.toString()); 119 if (!TextUtils.isEmpty(contactUriString)) { 120 addAppWidgetIdForKey(sp, editor, appWidgetId, contactUriString); 121 } 122 editor.apply(); 123 backupManager.dataChanged(); 124 } 125 126 /** Removes stored data when tile is deleted. */ removeSharedPreferencesStorageForTile(Context context, PeopleTileKey key, int widgetId, String contactUriString)127 public static void removeSharedPreferencesStorageForTile(Context context, PeopleTileKey key, 128 int widgetId, String contactUriString) { 129 // Delete widgetId mapping to key. 130 if (DEBUG) Log.d(TAG, "Removing widget info from sharedPrefs"); 131 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 132 SharedPreferences.Editor editor = sp.edit(); 133 editor.remove(String.valueOf(widgetId)); 134 removeAppWidgetIdForKey(sp, editor, widgetId, key.toString()); 135 removeAppWidgetIdForKey(sp, editor, widgetId, contactUriString); 136 editor.apply(); 137 138 // Delete all data specifically mapped to widgetId. 139 SharedPreferences widgetSp = context.getSharedPreferences(String.valueOf(widgetId), 140 Context.MODE_PRIVATE); 141 SharedPreferences.Editor widgetEditor = widgetSp.edit(); 142 widgetEditor.remove(PACKAGE_NAME); 143 widgetEditor.remove(USER_ID); 144 widgetEditor.remove(SHORTCUT_ID); 145 widgetEditor.apply(); 146 } 147 addAppWidgetIdForKey(SharedPreferences sp, SharedPreferences.Editor editor, int widgetId, String storageKey)148 private static void addAppWidgetIdForKey(SharedPreferences sp, SharedPreferences.Editor editor, 149 int widgetId, String storageKey) { 150 Set<String> storedWidgetIdsByKey = new HashSet<>( 151 sp.getStringSet(storageKey, new HashSet<>())); 152 storedWidgetIdsByKey.add(String.valueOf(widgetId)); 153 editor.putStringSet(storageKey, storedWidgetIdsByKey); 154 } 155 removeAppWidgetIdForKey(SharedPreferences sp, SharedPreferences.Editor editor, int widgetId, String storageKey)156 private static void removeAppWidgetIdForKey(SharedPreferences sp, 157 SharedPreferences.Editor editor, 158 int widgetId, String storageKey) { 159 Set<String> storedWidgetIds = new HashSet<>( 160 sp.getStringSet(storageKey, new HashSet<>())); 161 storedWidgetIds.remove(String.valueOf(widgetId)); 162 editor.putStringSet(storageKey, storedWidgetIds); 163 } 164 165 /** Returns notifications that match provided {@code contactUri}. */ getNotificationsByUri( PackageManager packageManager, String contactUri, Map<PeopleTileKey, Set<NotificationEntry>> notifications)166 public static List<NotificationEntry> getNotificationsByUri( 167 PackageManager packageManager, String contactUri, 168 Map<PeopleTileKey, Set<NotificationEntry>> notifications) { 169 if (DEBUG) Log.d(TAG, "Getting notifications by contact URI."); 170 if (TextUtils.isEmpty(contactUri)) { 171 return new ArrayList<>(); 172 } 173 return notifications.entrySet().stream().flatMap(e -> e.getValue().stream()) 174 .filter(e -> 175 hasReadContactsPermission(packageManager, e.getSbn()) 176 && shouldMatchNotificationByUri(e.getSbn()) 177 && Objects.equals(contactUri, getContactUri(e.getSbn())) 178 ) 179 .collect(Collectors.toList()); 180 } 181 182 /** Returns the total messages in {@code notificationEntries}. */ getMessagesCount(Set<NotificationEntry> notificationEntries)183 public static int getMessagesCount(Set<NotificationEntry> notificationEntries) { 184 if (DEBUG) { 185 Log.d(TAG, "Calculating messages count from " + notificationEntries.size() 186 + " notifications."); 187 } 188 int messagesCount = 0; 189 for (NotificationEntry entry : notificationEntries) { 190 Notification notification = entry.getSbn().getNotification(); 191 // Should not count messages from missed call notifications. 192 if (isMissedCall(notification)) { 193 continue; 194 } 195 196 List<Notification.MessagingStyle.Message> messages = 197 getMessagingStyleMessages(notification); 198 if (messages != null) { 199 messagesCount += messages.size(); 200 } 201 } 202 return messagesCount; 203 } 204 205 /** Removes all notification related fields from {@code tile}. */ removeNotificationFields(PeopleSpaceTile tile)206 public static PeopleSpaceTile removeNotificationFields(PeopleSpaceTile tile) { 207 if (DEBUG) { 208 Log.i(TAG, "Removing any notification stored for tile Id: " + tile.getId()); 209 } 210 PeopleSpaceTile.Builder updatedTile = tile 211 .toBuilder() 212 // Reset notification content. 213 .setNotificationKey(null) 214 .setNotificationContent(null) 215 .setNotificationSender(null) 216 .setNotificationDataUri(null) 217 .setMessagesCount(0) 218 // Reset missed calls category. 219 .setNotificationCategory(null); 220 221 // Only set last interaction to now if we are clearing a notification. 222 if (!TextUtils.isEmpty(tile.getNotificationKey())) { 223 long currentTimeMillis = System.currentTimeMillis(); 224 if (DEBUG) Log.d(TAG, "Set last interaction on clear: " + currentTimeMillis); 225 updatedTile.setLastInteractionTimestamp(currentTimeMillis); 226 } 227 return updatedTile.build(); 228 } 229 230 /** 231 * Augments {@code tile} with the notification content from {@code notificationEntry} and 232 * {@code messagesCount}. 233 */ augmentTileFromNotification(Context context, PeopleSpaceTile tile, PeopleTileKey key, NotificationEntry notificationEntry, int messagesCount, Optional<Integer> appWidgetId, BackupManager backupManager)234 public static PeopleSpaceTile augmentTileFromNotification(Context context, PeopleSpaceTile tile, 235 PeopleTileKey key, NotificationEntry notificationEntry, int messagesCount, 236 Optional<Integer> appWidgetId, BackupManager backupManager) { 237 if (notificationEntry == null || notificationEntry.getSbn().getNotification() == null) { 238 if (DEBUG) Log.d(TAG, "Tile key: " + key.toString() + ". Notification is null"); 239 return removeNotificationFields(tile); 240 } 241 StatusBarNotification sbn = notificationEntry.getSbn(); 242 Notification notification = sbn.getNotification(); 243 244 PeopleSpaceTile.Builder updatedTile = tile.toBuilder(); 245 String uriFromNotification = getContactUri(sbn); 246 if (appWidgetId.isPresent() && tile.getContactUri() == null && !TextUtils.isEmpty( 247 uriFromNotification)) { 248 if (DEBUG) Log.d(TAG, "Add uri from notification to tile: " + uriFromNotification); 249 Uri contactUri = Uri.parse(uriFromNotification); 250 // Update storage. 251 setSharedPreferencesStorageForTile(context, new PeopleTileKey(tile), appWidgetId.get(), 252 contactUri, backupManager); 253 // Update cached tile in-memory. 254 updatedTile.setContactUri(contactUri); 255 } 256 boolean isMissedCall = isMissedCall(notification); 257 List<Notification.MessagingStyle.Message> messages = 258 getMessagingStyleMessages(notification); 259 260 if (!isMissedCall && ArrayUtils.isEmpty(messages)) { 261 if (DEBUG) Log.d(TAG, "Tile key: " + key.toString() + ". Notification has no content"); 262 return removeNotificationFields(updatedTile.build()); 263 } 264 265 // messages are in chronological order from most recent to least. 266 Notification.MessagingStyle.Message message = messages != null ? messages.get(0) : null; 267 // If it's a missed call notification and it doesn't include content, use fallback value, 268 // otherwise, use notification content. 269 boolean hasMessageText = message != null && !TextUtils.isEmpty(message.getText()); 270 CharSequence content = (isMissedCall && !hasMessageText) 271 ? context.getString(R.string.missed_call) : message.getText(); 272 273 // We only use the URI if it's an image, otherwise we fallback to text (for example, with an 274 // audio URI) 275 Uri imageUri = message != null && MessagingMessage.hasImage(message) 276 ? message.getDataUri() : null; 277 278 if (DEBUG) { 279 Log.d(TAG, "Tile key: " + key.toString() + ". Notification message has text: " 280 + hasMessageText + ". Image URI: " + imageUri + ". Has last interaction: " 281 + sbn.getPostTime()); 282 } 283 CharSequence sender = getSenderIfGroupConversation(notification, message); 284 285 return updatedTile 286 .setLastInteractionTimestamp(sbn.getPostTime()) 287 .setNotificationKey(sbn.getKey()) 288 .setNotificationCategory(notification.category) 289 .setNotificationContent(content) 290 .setNotificationSender(sender) 291 .setNotificationDataUri(imageUri) 292 .setMessagesCount(messagesCount) 293 .build(); 294 } 295 296 /** Returns a list sorted by ascending last interaction time from {@code stream}. */ getSortedTiles(IPeopleManager peopleManager, LauncherApps launcherApps, UserManager userManager, Stream<ShortcutInfo> stream)297 public static List<PeopleSpaceTile> getSortedTiles(IPeopleManager peopleManager, 298 LauncherApps launcherApps, UserManager userManager, 299 Stream<ShortcutInfo> stream) { 300 return stream 301 .filter(Objects::nonNull) 302 .filter(c -> !userManager.isQuietModeEnabled(c.getUserHandle())) 303 .map(c -> new PeopleSpaceTile.Builder(c, launcherApps).build()) 304 .filter(c -> shouldKeepConversation(c)) 305 .map(c -> c.toBuilder().setLastInteractionTimestamp( 306 getLastInteraction(peopleManager, c)).build()) 307 .sorted((c1, c2) -> new Long(c2.getLastInteractionTimestamp()).compareTo( 308 new Long(c1.getLastInteractionTimestamp()))) 309 .collect(Collectors.toList()); 310 } 311 312 /** Returns {@code PeopleSpaceTile} based on provided {@ConversationChannel}. */ getTile(ConversationChannel channel, LauncherApps launcherApps)313 public static PeopleSpaceTile getTile(ConversationChannel channel, LauncherApps launcherApps) { 314 if (channel == null) { 315 Log.i(TAG, "ConversationChannel is null"); 316 return null; 317 } 318 PeopleSpaceTile tile = new PeopleSpaceTile.Builder(channel, launcherApps).build(); 319 if (!PeopleSpaceUtils.shouldKeepConversation(tile)) { 320 Log.i(TAG, "PeopleSpaceTile is not valid"); 321 return null; 322 } 323 324 return tile; 325 } 326 327 /** Returns the last interaction time with the user specified by {@code PeopleSpaceTile}. */ getLastInteraction(IPeopleManager peopleManager, PeopleSpaceTile tile)328 private static Long getLastInteraction(IPeopleManager peopleManager, 329 PeopleSpaceTile tile) { 330 try { 331 int userId = getUserId(tile); 332 String pkg = tile.getPackageName(); 333 return peopleManager.getLastInteraction(pkg, userId, tile.getId()); 334 } catch (Exception e) { 335 Log.e(TAG, "Couldn't retrieve last interaction time", e); 336 return 0L; 337 } 338 } 339 340 /** Converts {@code drawable} to a {@link Bitmap}. */ convertDrawableToBitmap(Drawable drawable)341 public static Bitmap convertDrawableToBitmap(Drawable drawable) { 342 if (drawable == null) { 343 return null; 344 } 345 346 if (drawable instanceof BitmapDrawable) { 347 BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; 348 if (bitmapDrawable.getBitmap() != null) { 349 return bitmapDrawable.getBitmap(); 350 } 351 } 352 353 Bitmap bitmap; 354 if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { 355 bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); 356 // Single color bitmap will be created of 1x1 pixel 357 } else { 358 bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), 359 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 360 } 361 362 Canvas canvas = new Canvas(bitmap); 363 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 364 drawable.draw(canvas); 365 return bitmap; 366 } 367 368 /** 369 * Returns whether the {@code conversation} should be kept for display in the People Space. 370 * 371 * <p>A valid {@code conversation} must: 372 * <ul> 373 * <li>Have a non-null {@link PeopleSpaceTile} 374 * <li>Have an associated label in the {@link PeopleSpaceTile} 375 * </ul> 376 * </li> 377 */ shouldKeepConversation(PeopleSpaceTile tile)378 public static boolean shouldKeepConversation(PeopleSpaceTile tile) { 379 return tile != null && !TextUtils.isEmpty(tile.getUserName()); 380 } 381 hasBirthdayStatus(PeopleSpaceTile tile, Context context)382 private static boolean hasBirthdayStatus(PeopleSpaceTile tile, Context context) { 383 return tile.getBirthdayText() != null && tile.getBirthdayText().equals( 384 context.getString(R.string.birthday_status)); 385 } 386 387 /** Calls to retrieve birthdays & contact affinity on a background thread. */ getDataFromContactsOnBackgroundThread(Context context, PeopleSpaceWidgetManager manager, Map<Integer, PeopleSpaceTile> peopleSpaceTiles, int[] appWidgetIds)388 public static void getDataFromContactsOnBackgroundThread(Context context, 389 PeopleSpaceWidgetManager manager, 390 Map<Integer, PeopleSpaceTile> peopleSpaceTiles, int[] appWidgetIds) { 391 ThreadUtils.postOnBackgroundThread( 392 () -> getDataFromContacts(context, manager, peopleSpaceTiles, appWidgetIds)); 393 } 394 395 /** Queries the Contacts DB for any birthdays today & updates contact affinity. */ 396 @VisibleForTesting getDataFromContacts(Context context, PeopleSpaceWidgetManager peopleSpaceWidgetManager, Map<Integer, PeopleSpaceTile> widgetIdToTile, int[] appWidgetIds)397 public static void getDataFromContacts(Context context, 398 PeopleSpaceWidgetManager peopleSpaceWidgetManager, 399 Map<Integer, PeopleSpaceTile> widgetIdToTile, int[] appWidgetIds) { 400 if (DEBUG) Log.d(TAG, "Get birthdays"); 401 if (appWidgetIds.length == 0) return; 402 List<String> lookupKeysWithBirthdaysToday = getContactLookupKeysWithBirthdaysToday(context); 403 for (int appWidgetId : appWidgetIds) { 404 PeopleSpaceTile storedTile = widgetIdToTile.get(appWidgetId); 405 if (storedTile == null || storedTile.getContactUri() == null) { 406 if (DEBUG) Log.d(TAG, "No contact uri for: " + storedTile); 407 updateTileContactFields(peopleSpaceWidgetManager, context, storedTile, 408 appWidgetId, DEFAULT_AFFINITY, /* birthdayString= */ null); 409 continue; 410 } 411 updateTileWithBirthdayAndUpdateAffinity(context, peopleSpaceWidgetManager, 412 lookupKeysWithBirthdaysToday, 413 storedTile, 414 appWidgetId); 415 } 416 } 417 418 /** 419 * Updates the {@code storedTile} with {@code affinity} & {@code birthdayString} if 420 * necessary. 421 */ updateTileContactFields(PeopleSpaceWidgetManager manager, Context context, PeopleSpaceTile storedTile, int appWidgetId, float affinity, @Nullable String birthdayString)422 private static void updateTileContactFields(PeopleSpaceWidgetManager manager, 423 Context context, PeopleSpaceTile storedTile, int appWidgetId, float affinity, 424 @Nullable String birthdayString) { 425 boolean outdatedBirthdayStatus = hasBirthdayStatus(storedTile, context) 426 && birthdayString == null; 427 boolean addBirthdayStatus = !hasBirthdayStatus(storedTile, context) 428 && birthdayString != null; 429 boolean shouldUpdate = storedTile.getContactAffinity() != affinity || outdatedBirthdayStatus 430 || addBirthdayStatus; 431 if (shouldUpdate) { 432 if (DEBUG) Log.d(TAG, "Update " + storedTile.getUserName() + " from contacts"); 433 manager.updateAppWidgetOptionsAndView(appWidgetId, 434 storedTile.toBuilder() 435 .setBirthdayText(birthdayString) 436 .setContactAffinity(affinity) 437 .build()); 438 } 439 } 440 441 /** 442 * Update {@code storedTile} if the contact has a lookup key matched to any {@code 443 * lookupKeysWithBirthdays}. 444 */ updateTileWithBirthdayAndUpdateAffinity(Context context, PeopleSpaceWidgetManager manager, List<String> lookupKeysWithBirthdaysToday, PeopleSpaceTile storedTile, int appWidgetId)445 private static void updateTileWithBirthdayAndUpdateAffinity(Context context, 446 PeopleSpaceWidgetManager manager, 447 List<String> lookupKeysWithBirthdaysToday, PeopleSpaceTile storedTile, 448 int appWidgetId) { 449 Cursor cursor = null; 450 try { 451 cursor = context.getContentResolver().query(storedTile.getContactUri(), 452 null, null, null, null); 453 while (cursor != null && cursor.moveToNext()) { 454 String storedLookupKey = cursor.getString( 455 cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event.LOOKUP_KEY)); 456 float affinity = getContactAffinity(cursor); 457 if (!storedLookupKey.isEmpty() && lookupKeysWithBirthdaysToday.contains( 458 storedLookupKey)) { 459 if (DEBUG) Log.d(TAG, storedTile.getUserName() + "'s birthday today!"); 460 updateTileContactFields(manager, context, storedTile, appWidgetId, 461 affinity, /* birthdayString= */ 462 context.getString(R.string.birthday_status)); 463 } else { 464 updateTileContactFields(manager, context, storedTile, appWidgetId, 465 affinity, /* birthdayString= */ null); 466 } 467 } 468 } catch (SQLException e) { 469 Log.e(TAG, "Failed to query contact: " + e); 470 } finally { 471 if (cursor != null) { 472 cursor.close(); 473 } 474 } 475 } 476 477 /** Pulls the contact affinity from {@code cursor}. */ getContactAffinity(Cursor cursor)478 private static float getContactAffinity(Cursor cursor) { 479 float affinity = VALID_CONTACT; 480 int starIdx = cursor.getColumnIndex(ContactsContract.Contacts.STARRED); 481 if (starIdx >= 0) { 482 boolean isStarred = cursor.getInt(starIdx) != 0; 483 if (isStarred) { 484 affinity = Math.max(affinity, STARRED_CONTACT); 485 } 486 } 487 if (DEBUG) Log.d(TAG, "Affinity is: " + affinity); 488 return affinity; 489 } 490 491 /** 492 * Returns lookup keys for all contacts with a birthday today. 493 * 494 * <p>Birthdays are queried from a different table within the Contacts DB than the table for 495 * the Contact Uri provided by most messaging apps. Matching by the contact ID is then quite 496 * fragile as the row IDs across the different tables are not guaranteed to stay aligned, so we 497 * match the data by {@link ContactsContract.ContactsColumns#LOOKUP_KEY} key to ensure proper 498 * matching across all the Contacts DB tables. 499 */ 500 @VisibleForTesting getContactLookupKeysWithBirthdaysToday(Context context)501 public static List<String> getContactLookupKeysWithBirthdaysToday(Context context) { 502 List<String> lookupKeysWithBirthdaysToday = new ArrayList<>(1); 503 String today = new SimpleDateFormat("MM-dd").format(new Date()); 504 String[] projection = new String[]{ 505 ContactsContract.CommonDataKinds.Event.LOOKUP_KEY, 506 ContactsContract.CommonDataKinds.Event.START_DATE}; 507 String where = 508 ContactsContract.Data.MIMETYPE 509 + "= ? AND " + ContactsContract.CommonDataKinds.Event.TYPE + "=" 510 + ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY + " AND (substr(" 511 // Birthdays stored with years will match this format 512 + ContactsContract.CommonDataKinds.Event.START_DATE + ",6) = ? OR substr(" 513 // Birthdays stored without years will match this format 514 + ContactsContract.CommonDataKinds.Event.START_DATE + ",3) = ? )"; 515 String[] selection = 516 new String[]{ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, today, 517 today}; 518 Cursor cursor = null; 519 try { 520 cursor = context 521 .getContentResolver() 522 .query(ContactsContract.Data.CONTENT_URI, 523 projection, where, selection, null); 524 while (cursor != null && cursor.moveToNext()) { 525 String lookupKey = cursor.getString( 526 cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event.LOOKUP_KEY)); 527 lookupKeysWithBirthdaysToday.add(lookupKey); 528 } 529 } catch (SQLException e) { 530 Log.e(TAG, "Failed to query birthdays: " + e); 531 } finally { 532 if (cursor != null) { 533 cursor.close(); 534 } 535 } 536 return lookupKeysWithBirthdaysToday; 537 } 538 539 /** Returns the userId associated with a {@link PeopleSpaceTile} */ getUserId(PeopleSpaceTile tile)540 public static int getUserId(PeopleSpaceTile tile) { 541 return tile.getUserHandle().getIdentifier(); 542 } 543 544 /** Represents whether {@link StatusBarNotification} was posted or removed. */ 545 public enum NotificationAction { 546 POSTED, 547 REMOVED 548 } 549 550 /** 551 * The UiEvent enums that this class can log. 552 */ 553 public enum PeopleSpaceWidgetEvent implements UiEventLogger.UiEventEnum { 554 @UiEvent(doc = "People space widget deleted") 555 PEOPLE_SPACE_WIDGET_DELETED(666), 556 @UiEvent(doc = "People space widget added") 557 PEOPLE_SPACE_WIDGET_ADDED(667), 558 @UiEvent(doc = "People space widget clicked to launch conversation") 559 PEOPLE_SPACE_WIDGET_CLICKED(668); 560 561 private final int mId; 562 PeopleSpaceWidgetEvent(int id)563 PeopleSpaceWidgetEvent(int id) { 564 mId = id; 565 } 566 567 @Override getId()568 public int getId() { 569 return mId; 570 } 571 } 572 }