Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2016 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 package com.android.contacts;
     17 
     18 import android.annotation.TargetApi;
     19 import android.app.ActivityManager;
     20 import android.app.job.JobInfo;
     21 import android.app.job.JobParameters;
     22 import android.app.job.JobScheduler;
     23 import android.app.job.JobService;
     24 import android.content.BroadcastReceiver;
     25 import android.content.ComponentName;
     26 import android.content.ContentResolver;
     27 import android.content.ContentUris;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.IntentFilter;
     31 import android.content.pm.ShortcutInfo;
     32 import android.content.pm.ShortcutManager;
     33 import android.database.Cursor;
     34 import android.graphics.Bitmap;
     35 import android.graphics.BitmapFactory;
     36 import android.graphics.BitmapRegionDecoder;
     37 import android.graphics.Canvas;
     38 import android.graphics.Rect;
     39 import android.graphics.drawable.AdaptiveIconDrawable;
     40 import android.graphics.drawable.Drawable;
     41 import android.graphics.drawable.Icon;
     42 import android.net.Uri;
     43 import android.os.AsyncTask;
     44 import android.os.Build;
     45 import android.os.PersistableBundle;
     46 import android.provider.ContactsContract;
     47 import android.provider.ContactsContract.Contacts;
     48 import android.support.annotation.VisibleForTesting;
     49 import android.support.v4.content.LocalBroadcastManager;
     50 import android.support.v4.os.BuildCompat;
     51 import android.util.Log;
     52 
     53 import com.android.contacts.activities.RequestPermissionsActivity;
     54 import com.android.contacts.compat.CompatUtils;
     55 import com.android.contacts.lettertiles.LetterTileDrawable;
     56 import com.android.contacts.util.BitmapUtil;
     57 import com.android.contacts.util.ImplicitIntentsUtil;
     58 import com.android.contacts.util.PermissionsUtil;
     59 import com.android.contactsbind.experiments.Flags;
     60 
     61 import java.io.IOException;
     62 import java.io.InputStream;
     63 import java.util.ArrayList;
     64 import java.util.Collections;
     65 import java.util.List;
     66 
     67 /**
     68  * This class creates and updates the dynamic shortcuts displayed on the Nexus launcher for the
     69  * Contacts app.
     70  *
     71  * Currently it adds shortcuts for the top 3 contacts in the {@link Contacts#CONTENT_STREQUENT_URI}
     72  *
     73  * Usage: DynamicShortcuts.initialize should be called during Application creation. This will
     74  * schedule a Job to keep the shortcuts up-to-date so no further interactions should be necessary.
     75  */
     76 @TargetApi(Build.VERSION_CODES.N_MR1)
     77 public class DynamicShortcuts {
     78     private static final String TAG = "DynamicShortcuts";
     79 
     80     // Must be the same as shortcutId in res/xml/shortcuts.xml
     81     // Note: This doesn't fit very well because this is a "static" shortcut but it's still the most
     82     // sensible place to put it right now.
     83     public static final String SHORTCUT_ADD_CONTACT = "shortcut-add-contact";
     84 
     85     // Note the Nexus launcher automatically truncates shortcut labels if they exceed these limits
     86     // however, we implement our own truncation in case the shortcut is shown on a launcher that
     87     // has different behavior
     88     private static final int SHORT_LABEL_MAX_LENGTH = 12;
     89     private static final int LONG_LABEL_MAX_LENGTH = 30;
     90     private static final int MAX_SHORTCUTS = 3;
     91 
     92     private static final String EXTRA_SHORTCUT_TYPE = "extraShortcutType";
     93 
     94     // Because pinned shortcuts persist across app upgrades these values should not be changed
     95     // though new ones may be added
     96     private static final int SHORTCUT_TYPE_UNKNOWN = 0;
     97     private static final int SHORTCUT_TYPE_CONTACT_URI = 1;
     98     private static final int SHORTCUT_TYPE_ACTION_URI = 2;
     99 
    100     @VisibleForTesting
    101     static final String[] PROJECTION = new String[] {
    102             Contacts._ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME_PRIMARY
    103     };
    104 
    105     private final Context mContext;
    106 
    107     private final ContentResolver mContentResolver;
    108     private final ShortcutManager mShortcutManager;
    109     private int mShortLabelMaxLength = SHORT_LABEL_MAX_LENGTH;
    110     private int mLongLabelMaxLength = LONG_LABEL_MAX_LENGTH;
    111     private int mIconSize;
    112     private final int mContentChangeMinUpdateDelay;
    113     private final int mContentChangeMaxUpdateDelay;
    114     private final JobScheduler mJobScheduler;
    115 
    116     public DynamicShortcuts(Context context) {
    117         this(context, context.getContentResolver(), (ShortcutManager)
    118                 context.getSystemService(Context.SHORTCUT_SERVICE),
    119                 (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE));
    120     }
    121 
    122     @VisibleForTesting
    123     public DynamicShortcuts(Context context, ContentResolver contentResolver,
    124             ShortcutManager shortcutManager, JobScheduler jobScheduler) {
    125         mContext = context;
    126         mContentResolver = contentResolver;
    127         mShortcutManager = shortcutManager;
    128         mJobScheduler = jobScheduler;
    129         mContentChangeMinUpdateDelay = Flags.getInstance()
    130                 .getInteger(Experiments.DYNAMIC_MIN_CONTENT_CHANGE_UPDATE_DELAY_MILLIS);
    131         mContentChangeMaxUpdateDelay = Flags.getInstance()
    132                 .getInteger(Experiments.DYNAMIC_MAX_CONTENT_CHANGE_UPDATE_DELAY_MILLIS);
    133         final ActivityManager am = (ActivityManager) context
    134                 .getSystemService(Context.ACTIVITY_SERVICE);
    135         mIconSize = context.getResources().getDimensionPixelSize(R.dimen.shortcut_icon_size);
    136         if (mIconSize == 0) {
    137             mIconSize = am.getLauncherLargeIconSize();
    138         }
    139     }
    140 
    141     @VisibleForTesting
    142     void setShortLabelMaxLength(int length) {
    143         this.mShortLabelMaxLength = length;
    144     }
    145 
    146     @VisibleForTesting
    147     void setLongLabelMaxLength(int length) {
    148         this.mLongLabelMaxLength = length;
    149     }
    150 
    151     @VisibleForTesting
    152     void refresh() {
    153         // Guard here in addition to initialize because this could be run by the JobScheduler
    154         // after permissions are revoked (maybe)
    155         if (!hasRequiredPermissions()) return;
    156 
    157         final List<ShortcutInfo> shortcuts = getStrequentShortcuts();
    158         mShortcutManager.setDynamicShortcuts(shortcuts);
    159         if (Log.isLoggable(TAG, Log.DEBUG)) {
    160             Log.d(TAG, "set dynamic shortcuts " + shortcuts);
    161         }
    162         updatePinned();
    163     }
    164 
    165     @VisibleForTesting
    166     void updatePinned() {
    167         final List<ShortcutInfo> updates = new ArrayList<>();
    168         final List<String> removedIds = new ArrayList<>();
    169         final List<String> enable = new ArrayList<>();
    170 
    171         for (ShortcutInfo shortcut : mShortcutManager.getPinnedShortcuts()) {
    172             final PersistableBundle extras = shortcut.getExtras();
    173 
    174             if (extras == null || extras.getInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_UNKNOWN) !=
    175                     SHORTCUT_TYPE_CONTACT_URI) {
    176                 continue;
    177             }
    178 
    179             // The contact ID may have changed but that's OK because it is just an optimization
    180             final long contactId = extras.getLong(Contacts._ID);
    181 
    182             final ShortcutInfo update = createShortcutForUri(
    183                     Contacts.getLookupUri(contactId, shortcut.getId()));
    184             if (update != null) {
    185                 updates.add(update);
    186                 if (!shortcut.isEnabled()) {
    187                     // Handle the case that a contact is disabled because it doesn't exist but
    188                     // later is created (for instance by a sync)
    189                     enable.add(update.getId());
    190                 }
    191             } else if (shortcut.isEnabled()) {
    192                 removedIds.add(shortcut.getId());
    193             }
    194         }
    195 
    196         if (Log.isLoggable(TAG, Log.DEBUG)) {
    197             Log.d(TAG, "updating " + updates);
    198             Log.d(TAG, "enabling " + enable);
    199             Log.d(TAG, "disabling " + removedIds);
    200         }
    201 
    202         mShortcutManager.updateShortcuts(updates);
    203         mShortcutManager.enableShortcuts(enable);
    204         mShortcutManager.disableShortcuts(removedIds,
    205                 mContext.getString(R.string.dynamic_shortcut_contact_removed_message));
    206     }
    207 
    208     private ShortcutInfo createShortcutForUri(Uri contactUri) {
    209         final Cursor cursor = mContentResolver.query(contactUri, PROJECTION, null, null, null);
    210         if (cursor == null) return null;
    211 
    212         try {
    213             if (cursor.moveToFirst()) {
    214                 return createShortcutFromRow(cursor);
    215             }
    216         } finally {
    217             cursor.close();
    218         }
    219         return null;
    220     }
    221 
    222     public List<ShortcutInfo> getStrequentShortcuts() {
    223         // The limit query parameter doesn't seem to work for this uri but we'll leave it because in
    224         // case it does work on some phones or platform versions.
    225         final Uri uri = Contacts.CONTENT_STREQUENT_URI.buildUpon()
    226                 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
    227                         String.valueOf(MAX_SHORTCUTS))
    228                 .build();
    229         final Cursor cursor = mContentResolver.query(uri, PROJECTION, null, null, null);
    230 
    231         if (cursor == null) return Collections.emptyList();
    232 
    233         final List<ShortcutInfo> result = new ArrayList<>();
    234 
    235         try {
    236             int i = 0;
    237             while (i < MAX_SHORTCUTS && cursor.moveToNext()) {
    238                 final ShortcutInfo shortcut = createShortcutFromRow(cursor);
    239                 if (shortcut == null) {
    240                     continue;
    241                 }
    242                 result.add(shortcut);
    243                 i++;
    244             }
    245         } finally {
    246             cursor.close();
    247         }
    248         return result;
    249     }
    250 
    251 
    252     @VisibleForTesting
    253     ShortcutInfo createShortcutFromRow(Cursor cursor) {
    254         final ShortcutInfo.Builder builder = builderForContactShortcut(cursor);
    255         if (builder == null) {
    256             return null;
    257         }
    258         addIconForContact(cursor, builder);
    259         return builder.build();
    260     }
    261 
    262     @VisibleForTesting
    263     ShortcutInfo.Builder builderForContactShortcut(Cursor cursor) {
    264         final long id = cursor.getLong(0);
    265         final String lookupKey = cursor.getString(1);
    266         final String displayName = cursor.getString(2);
    267         return builderForContactShortcut(id, lookupKey, displayName);
    268     }
    269 
    270     @VisibleForTesting
    271     ShortcutInfo.Builder builderForContactShortcut(long id, String lookupKey, String displayName) {
    272         if (lookupKey == null || displayName == null) {
    273             return null;
    274         }
    275         final PersistableBundle extras = new PersistableBundle();
    276         extras.putLong(Contacts._ID, id);
    277         extras.putInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_CONTACT_URI);
    278 
    279         final ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mContext, lookupKey)
    280                 .setIntent(ImplicitIntentsUtil.getIntentForQuickContactLauncherShortcut(mContext,
    281                         Contacts.getLookupUri(id, lookupKey)))
    282                 .setDisabledMessage(mContext.getString(R.string.dynamic_shortcut_disabled_message))
    283                 .setExtras(extras);
    284 
    285         setLabel(builder, displayName);
    286         return builder;
    287     }
    288 
    289     @VisibleForTesting
    290     ShortcutInfo getActionShortcutInfo(String id, String label, Intent action, Icon icon) {
    291         if (id == null || label == null) {
    292             return null;
    293         }
    294         final PersistableBundle extras = new PersistableBundle();
    295         extras.putInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_ACTION_URI);
    296 
    297         final ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mContext, id)
    298                 .setIntent(action)
    299                 .setIcon(icon)
    300                 .setExtras(extras)
    301                 .setDisabledMessage(mContext.getString(R.string.dynamic_shortcut_disabled_message));
    302 
    303         setLabel(builder, label);
    304         return builder.build();
    305     }
    306 
    307     public ShortcutInfo getQuickContactShortcutInfo(long id, String lookupKey, String displayName) {
    308         final ShortcutInfo.Builder builder = builderForContactShortcut(id, lookupKey, displayName);
    309         if (builder == null) {
    310             return null;
    311         }
    312         addIconForContact(id, lookupKey, displayName, builder);
    313         return builder.build();
    314     }
    315 
    316     private void setLabel(ShortcutInfo.Builder builder, String label) {
    317         if (label.length() < mLongLabelMaxLength) {
    318             builder.setLongLabel(label);
    319         } else {
    320             builder.setLongLabel(label.substring(0, mLongLabelMaxLength - 1).trim() + "");
    321         }
    322 
    323         if (label.length() < mShortLabelMaxLength) {
    324             builder.setShortLabel(label);
    325         } else {
    326             builder.setShortLabel(label.substring(0, mShortLabelMaxLength - 1).trim() + "");
    327         }
    328     }
    329 
    330     private void addIconForContact(Cursor cursor, ShortcutInfo.Builder builder) {
    331         final long id = cursor.getLong(0);
    332         final String lookupKey = cursor.getString(1);
    333         final String displayName = cursor.getString(2);
    334         addIconForContact(id, lookupKey, displayName, builder);
    335     }
    336 
    337     private void addIconForContact(long id, String lookupKey, String displayName,
    338             ShortcutInfo.Builder builder) {
    339         Bitmap bitmap = getContactPhoto(id);
    340         if (bitmap == null) {
    341             bitmap = getFallbackAvatar(displayName, lookupKey);
    342         }
    343         final Icon icon;
    344         if (BuildCompat.isAtLeastO()) {
    345             icon = Icon.createWithAdaptiveBitmap(bitmap);
    346         } else {
    347             icon = Icon.createWithBitmap(bitmap);
    348         }
    349 
    350         builder.setIcon(icon);
    351     }
    352 
    353     private Bitmap getContactPhoto(long id) {
    354         final InputStream photoStream = Contacts.openContactPhotoInputStream(
    355                 mContext.getContentResolver(),
    356                 ContentUris.withAppendedId(Contacts.CONTENT_URI, id), true);
    357 
    358         if (photoStream == null) return null;
    359         try {
    360             final Bitmap bitmap = decodeStreamForShortcut(photoStream);
    361             photoStream.close();
    362             return bitmap;
    363         } catch (IOException e) {
    364             Log.e(TAG, "Failed to decode contact photo for shortcut. ID=" + id, e);
    365             return null;
    366         } finally {
    367             try {
    368                 photoStream.close();
    369             } catch (IOException e) {
    370                 // swallow
    371             }
    372         }
    373     }
    374 
    375     private Bitmap decodeStreamForShortcut(InputStream stream) throws IOException {
    376         final BitmapRegionDecoder bitmapDecoder = BitmapRegionDecoder.newInstance(stream, false);
    377 
    378         final int sourceWidth = bitmapDecoder.getWidth();
    379         final int sourceHeight = bitmapDecoder.getHeight();
    380 
    381         final int iconMaxWidth = mShortcutManager.getIconMaxWidth();
    382         final int iconMaxHeight = mShortcutManager.getIconMaxHeight();
    383 
    384         final int sampleSize = Math.min(
    385                 BitmapUtil.findOptimalSampleSize(sourceWidth, mIconSize),
    386                 BitmapUtil.findOptimalSampleSize(sourceHeight, mIconSize));
    387         final BitmapFactory.Options opts = new BitmapFactory.Options();
    388         opts.inSampleSize = sampleSize;
    389 
    390         final int scaledWidth = sourceWidth / opts.inSampleSize;
    391         final int scaledHeight = sourceHeight / opts.inSampleSize;
    392 
    393         final int targetWidth = Math.min(scaledWidth, iconMaxWidth);
    394         final int targetHeight = Math.min(scaledHeight, iconMaxHeight);
    395 
    396         // Make it square.
    397         final int targetSize = Math.min(targetWidth, targetHeight);
    398 
    399         // The region is defined in the coordinates of the source image then the sampling is
    400         // done on the extracted region.
    401         final int prescaledXOffset = ((scaledWidth - targetSize) * opts.inSampleSize) / 2;
    402         final int prescaledYOffset = ((scaledHeight - targetSize) * opts.inSampleSize) / 2;
    403 
    404         final Bitmap bitmap = bitmapDecoder.decodeRegion(new Rect(
    405                 prescaledXOffset, prescaledYOffset,
    406                 sourceWidth - prescaledXOffset, sourceHeight - prescaledYOffset
    407         ), opts);
    408         bitmapDecoder.recycle();
    409 
    410         if (!BuildCompat.isAtLeastO()) {
    411             return BitmapUtil.getRoundedBitmap(bitmap, targetSize, targetSize);
    412         }
    413 
    414         return bitmap;
    415     }
    416 
    417     private Bitmap getFallbackAvatar(String displayName, String lookupKey) {
    418         // Use a circular icon if we're not on O or higher.
    419         final boolean circularIcon = !BuildCompat.isAtLeastO();
    420 
    421         final ContactPhotoManager.DefaultImageRequest request =
    422                 new ContactPhotoManager.DefaultImageRequest(displayName, lookupKey, circularIcon);
    423         if (BuildCompat.isAtLeastO()) {
    424             // On O, scale the image down to add the padding needed by AdaptiveIcons.
    425             request.scale = LetterTileDrawable.getAdaptiveIconScale();
    426         }
    427         final Drawable avatar = ContactPhotoManager.getDefaultAvatarDrawableForContact(
    428                 mContext.getResources(), true, request);
    429         final Bitmap result = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
    430         // The avatar won't draw unless it thinks it is visible
    431         avatar.setVisible(true, true);
    432         final Canvas canvas = new Canvas(result);
    433         avatar.setBounds(0, 0, mIconSize, mIconSize);
    434         avatar.draw(canvas);
    435         return result;
    436     }
    437 
    438     @VisibleForTesting
    439     void handleFlagDisabled() {
    440         removeAllShortcuts();
    441         mJobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
    442     }
    443 
    444     private void removeAllShortcuts() {
    445         mShortcutManager.removeAllDynamicShortcuts();
    446 
    447         final List<ShortcutInfo> pinned = mShortcutManager.getPinnedShortcuts();
    448         final List<String> ids = new ArrayList<>(pinned.size());
    449         for (ShortcutInfo shortcut : pinned) {
    450             ids.add(shortcut.getId());
    451         }
    452         mShortcutManager.disableShortcuts(ids, mContext
    453                 .getString(R.string.dynamic_shortcut_disabled_message));
    454         if (Log.isLoggable(TAG, Log.DEBUG)) {
    455             Log.d(TAG, "DynamicShortcuts have been removed.");
    456         }
    457     }
    458 
    459     @VisibleForTesting
    460     void scheduleUpdateJob() {
    461         final JobInfo job = new JobInfo.Builder(
    462                 ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID,
    463                 new ComponentName(mContext, ContactsJobService.class))
    464                 // We just observe all changes to contacts. It would be better to be more granular
    465                 // but CP2 only notifies using this URI anyway so there isn't any point in adding
    466                 // that complexity.
    467                 .addTriggerContentUri(new JobInfo.TriggerContentUri(ContactsContract.AUTHORITY_URI,
    468                         JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
    469                 .setTriggerContentUpdateDelay(mContentChangeMinUpdateDelay)
    470                 .setTriggerContentMaxDelay(mContentChangeMaxUpdateDelay)
    471                 .build();
    472         mJobScheduler.schedule(job);
    473     }
    474 
    475     void updateInBackground() {
    476         new ShortcutUpdateTask(this).execute();
    477     }
    478 
    479     public synchronized static void initialize(Context context) {
    480         if (Log.isLoggable(TAG, Log.DEBUG)) {
    481             final Flags flags = Flags.getInstance();
    482             Log.d(TAG, "DyanmicShortcuts.initialize\nVERSION >= N_MR1? " +
    483                     (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) +
    484                     "\nisJobScheduled? " +
    485                     (CompatUtils.isLauncherShortcutCompatible() && isJobScheduled(context)) +
    486                     "\nminDelay=" +
    487                     flags.getInteger(Experiments.DYNAMIC_MIN_CONTENT_CHANGE_UPDATE_DELAY_MILLIS) +
    488                     "\nmaxDelay=" +
    489                     flags.getInteger(Experiments.DYNAMIC_MAX_CONTENT_CHANGE_UPDATE_DELAY_MILLIS));
    490         }
    491 
    492         if (!CompatUtils.isLauncherShortcutCompatible()) return;
    493 
    494         final DynamicShortcuts shortcuts = new DynamicShortcuts(context);
    495 
    496         if (!shortcuts.hasRequiredPermissions()) {
    497             final IntentFilter filter = new IntentFilter();
    498             filter.addAction(RequestPermissionsActivity.BROADCAST_PERMISSIONS_GRANTED);
    499             LocalBroadcastManager.getInstance(shortcuts.mContext).registerReceiver(
    500                     new PermissionsGrantedReceiver(), filter);
    501         } else if (!isJobScheduled(context)) {
    502             // Update the shortcuts. If the job is already scheduled then either the app is being
    503             // launched to run the job in which case the shortcuts will get updated when it runs or
    504             // it has been launched for some other reason and the data we care about for shortcuts
    505             // hasn't changed. Because the job reschedules itself after completion this check
    506             // essentially means that this will run on each app launch that happens after a reboot.
    507             // Note: the task schedules the job after completing.
    508             new ShortcutUpdateTask(shortcuts).execute();
    509         }
    510     }
    511 
    512     @VisibleForTesting
    513     public static void reset(Context context) {
    514         final JobScheduler jobScheduler =
    515                 (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
    516         jobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
    517 
    518         if (!CompatUtils.isLauncherShortcutCompatible()) {
    519             return;
    520         }
    521         new DynamicShortcuts(context).removeAllShortcuts();
    522     }
    523 
    524     @VisibleForTesting
    525     boolean hasRequiredPermissions() {
    526         return PermissionsUtil.hasContactsPermissions(mContext);
    527     }
    528 
    529     public static void updateFromJob(final JobService service, final JobParameters jobParams) {
    530         new ShortcutUpdateTask(new DynamicShortcuts(service)) {
    531             @Override
    532             protected void onPostExecute(Void aVoid) {
    533                 // Must call super first which will reschedule the job before we call jobFinished
    534                 super.onPostExecute(aVoid);
    535                 service.jobFinished(jobParams, false);
    536             }
    537         }.execute();
    538     }
    539 
    540     @VisibleForTesting
    541     public static boolean isJobScheduled(Context context) {
    542         final JobScheduler scheduler = (JobScheduler) context
    543                 .getSystemService(Context.JOB_SCHEDULER_SERVICE);
    544         return scheduler.getPendingJob(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID) != null;
    545     }
    546 
    547     public static void reportShortcutUsed(Context context, String lookupKey) {
    548         if (!CompatUtils.isLauncherShortcutCompatible() || lookupKey == null) return;
    549         final ShortcutManager shortcutManager = (ShortcutManager) context
    550                 .getSystemService(Context.SHORTCUT_SERVICE);
    551         shortcutManager.reportShortcutUsed(lookupKey);
    552     }
    553 
    554     private static class ShortcutUpdateTask extends AsyncTask<Void, Void, Void> {
    555         private DynamicShortcuts mDynamicShortcuts;
    556 
    557         public ShortcutUpdateTask(DynamicShortcuts shortcuts) {
    558             mDynamicShortcuts = shortcuts;
    559         }
    560 
    561         @Override
    562         protected Void doInBackground(Void... voids) {
    563             mDynamicShortcuts.refresh();
    564             return null;
    565         }
    566 
    567         @Override
    568         protected void onPostExecute(Void aVoid) {
    569             if (Log.isLoggable(TAG, Log.DEBUG)) {
    570                 Log.d(TAG, "ShorcutUpdateTask.onPostExecute");
    571             }
    572             // The shortcuts may have changed so update the job so that we are observing the
    573             // correct Uris
    574             mDynamicShortcuts.scheduleUpdateJob();
    575         }
    576     }
    577 
    578     private static class PermissionsGrantedReceiver extends BroadcastReceiver {
    579         @Override
    580         public void onReceive(Context context, Intent intent) {
    581             // Clear the receiver.
    582             LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
    583             DynamicShortcuts.initialize(context);
    584         }
    585     }
    586 }
    587