• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }