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