blob: c7d8456006d6c3d9f064a1a8778003e35b439e98 [file] [log] [blame]
Marcus Hagerottc5083f92016-09-14 08:34:29 -07001/*
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 */
16package com.android.contacts;
17
18import android.annotation.TargetApi;
19import android.app.job.JobInfo;
20import android.app.job.JobParameters;
21import android.app.job.JobScheduler;
22import android.app.job.JobService;
Marcus Hagerott8ac989c2016-10-04 08:45:58 -070023import android.content.BroadcastReceiver;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070024import android.content.ComponentName;
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.Context;
Marcus Hagerott8ac989c2016-10-04 08:45:58 -070028import android.content.Intent;
29import android.content.IntentFilter;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070030import android.content.pm.ShortcutInfo;
31import android.content.pm.ShortcutManager;
32import android.database.Cursor;
33import android.graphics.Bitmap;
34import android.graphics.BitmapFactory;
35import android.graphics.BitmapRegionDecoder;
36import android.graphics.Canvas;
37import android.graphics.Rect;
38import android.graphics.drawable.Drawable;
39import android.graphics.drawable.Icon;
40import android.net.Uri;
41import android.os.AsyncTask;
42import android.os.Build;
43import android.os.PersistableBundle;
44import android.provider.ContactsContract;
45import android.provider.ContactsContract.Contacts;
46import android.support.annotation.VisibleForTesting;
Marcus Hagerott8ac989c2016-10-04 08:45:58 -070047import android.support.v4.content.LocalBroadcastManager;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070048import android.util.Log;
49
Gary Mai0a49afa2016-12-05 15:53:58 -080050import com.android.contacts.activities.RequestPermissionsActivity;
Gary Mai69c182a2016-12-05 13:07:03 -080051import com.android.contacts.compat.CompatUtils;
52import com.android.contacts.util.BitmapUtil;
53import com.android.contacts.util.ImplicitIntentsUtil;
54import com.android.contacts.util.PermissionsUtil;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070055import com.android.contactsbind.experiments.Flags;
56
57import java.io.IOException;
58import java.io.InputStream;
59import java.util.ArrayList;
60import java.util.Collections;
61import java.util.List;
62
Marcus Hagerottc5083f92016-09-14 08:34:29 -070063/**
64 * This class creates and updates the dynamic shortcuts displayed on the Nexus launcher for the
65 * Contacts app.
66 *
67 * Currently it adds shortcuts for the top 3 contacts in the {@link Contacts#CONTENT_STREQUENT_URI}
68 *
69 * Usage: DynamicShortcuts.initialize should be called during Application creation. This will
70 * schedule a Job to keep the shortcuts up-to-date so no further interations should be necessary.
71 */
72@TargetApi(Build.VERSION_CODES.N_MR1)
73public class DynamicShortcuts {
74 private static final String TAG = "DynamicShortcuts";
75
Marcus Hagerottd105c1e2016-09-30 14:28:00 -070076 // Must be the same as shortcutId in res/xml/shortcuts.xml
77 // Note: This doesn't fit very well because this is a "static" shortcut but it's still the most
78 // sensible place to put it right now.
79 public static final String SHORTCUT_ADD_CONTACT = "shortcut-add-contact";
80
Marcus Hagerottc5083f92016-09-14 08:34:29 -070081 // Note the Nexus launcher automatically truncates shortcut labels if they exceed these limits
82 // however, we implement our own truncation in case the shortcut is shown on a launcher that
83 // has different behavior
84 private static final int SHORT_LABEL_MAX_LENGTH = 12;
85 private static final int LONG_LABEL_MAX_LENGTH = 30;
86 private static final int MAX_SHORTCUTS = 3;
87
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -080088 private static final String EXTRA_SHORTCUT_TYPE = "extraShortcutType";
89
90 // Because pinned shortcuts persist across app upgrades these values should not be changed
91 // though new ones may be added
92 private static final int SHORTCUT_TYPE_UNKNOWN = 0;
93 private static final int SHORTCUT_TYPE_CONTACT_URI = 1;
94
Marcus Hagerottc5083f92016-09-14 08:34:29 -070095 // The spec specifies that it should be 44dp @ xxxhdpi
96 // Note that ShortcutManager.getIconMaxWidth and ShortcutManager.getMaxHeight return different
97 // (larger) values.
98 private static final int RECOMMENDED_ICON_PIXEL_LENGTH = 176;
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 private final ContentResolver mContentResolver;
107 private final ShortcutManager mShortcutManager;
108 private int mShortLabelMaxLength = SHORT_LABEL_MAX_LENGTH;
109 private int mLongLabelMaxLength = LONG_LABEL_MAX_LENGTH;
Arthur Wang8debbac2016-09-21 16:39:04 -0700110 private final int mContentChangeMinUpdateDelay;
111 private final int mContentChangeMaxUpdateDelay;
Marcus Hagerott020f0412016-09-22 12:13:49 -0700112 private final JobScheduler mJobScheduler;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700113
114 public DynamicShortcuts(Context context) {
115 this(context, context.getContentResolver(), (ShortcutManager)
Marcus Hagerott020f0412016-09-22 12:13:49 -0700116 context.getSystemService(Context.SHORTCUT_SERVICE),
117 (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE));
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700118 }
119
Marcus Hagerott020f0412016-09-22 12:13:49 -0700120 @VisibleForTesting
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700121 public DynamicShortcuts(Context context, ContentResolver contentResolver,
Marcus Hagerott020f0412016-09-22 12:13:49 -0700122 ShortcutManager shortcutManager, JobScheduler jobScheduler) {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700123 mContext = context;
124 mContentResolver = contentResolver;
125 mShortcutManager = shortcutManager;
Marcus Hagerott020f0412016-09-22 12:13:49 -0700126 mJobScheduler = jobScheduler;
Walter Jangdf86ede2016-10-19 09:48:29 -0700127 mContentChangeMinUpdateDelay = Flags.getInstance()
Arthur Wang8debbac2016-09-21 16:39:04 -0700128 .getInteger(Experiments.DYNAMIC_MIN_CONTENT_CHANGE_UPDATE_DELAY_MILLIS);
Walter Jangdf86ede2016-10-19 09:48:29 -0700129 mContentChangeMaxUpdateDelay = Flags.getInstance()
Arthur Wang8debbac2016-09-21 16:39:04 -0700130 .getInteger(Experiments.DYNAMIC_MAX_CONTENT_CHANGE_UPDATE_DELAY_MILLIS);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700131 }
132
133 @VisibleForTesting
134 void setShortLabelMaxLength(int length) {
135 this.mShortLabelMaxLength = length;
136 }
137
138 @VisibleForTesting
139 void setLongLabelMaxLength(int length) {
140 this.mLongLabelMaxLength = length;
141 }
142
143 @VisibleForTesting
144 void refresh() {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700145 // Guard here in addition to initialize because this could be run by the JobScheduler
146 // after permissions are revoked (maybe)
147 if (!hasRequiredPermissions()) return;
148
Marcus Hagerott020f0412016-09-22 12:13:49 -0700149 final List<ShortcutInfo> shortcuts = getStrequentShortcuts();
150 mShortcutManager.setDynamicShortcuts(shortcuts);
151 if (Log.isLoggable(TAG, Log.DEBUG)) {
152 Log.d(TAG, "set dynamic shortcuts " + shortcuts);
153 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700154 updatePinned();
155 }
156
157 @VisibleForTesting
158 void updatePinned() {
159 final List<ShortcutInfo> updates = new ArrayList<>();
160 final List<String> removedIds = new ArrayList<>();
161 final List<String> enable = new ArrayList<>();
162
163 for (ShortcutInfo shortcut : mShortcutManager.getPinnedShortcuts()) {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700164 final PersistableBundle extras = shortcut.getExtras();
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -0800165
166 if (!shortcut.isDynamic() || extras == null ||
167 extras.getInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_UNKNOWN) !=
168 SHORTCUT_TYPE_CONTACT_URI) {
169 continue;
170 }
171
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700172 // The contact ID may have changed but that's OK because it is just an optimization
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -0800173 final long contactId = extras.getLong(Contacts._ID);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700174
175 final ShortcutInfo update = createShortcutForUri(
176 Contacts.getLookupUri(contactId, shortcut.getId()));
177 if (update != null) {
178 updates.add(update);
179 if (!shortcut.isEnabled()) {
180 // Handle the case that a contact is disabled because it doesn't exist but
181 // later is created (for instance by a sync)
182 enable.add(update.getId());
183 }
184 } else if (shortcut.isEnabled()) {
185 removedIds.add(shortcut.getId());
186 }
187 }
188
Marcus Hagerott020f0412016-09-22 12:13:49 -0700189 if (Log.isLoggable(TAG, Log.DEBUG)) {
190 Log.d(TAG, "updating " + updates);
191 Log.d(TAG, "enabling " + enable);
192 Log.d(TAG, "disabling " + removedIds);
193 }
194
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700195 mShortcutManager.updateShortcuts(updates);
196 mShortcutManager.enableShortcuts(enable);
197 mShortcutManager.disableShortcuts(removedIds,
198 mContext.getString(R.string.dynamic_shortcut_contact_removed_message));
199 }
200
201 private ShortcutInfo createShortcutForUri(Uri contactUri) {
202 final Cursor cursor = mContentResolver.query(contactUri, PROJECTION, null, null, null);
203 if (cursor == null) return null;
204
205 try {
206 if (cursor.moveToFirst()) {
207 return createShortcutFromRow(cursor);
208 }
209 } finally {
210 cursor.close();
211 }
212 return null;
213 }
214
215 public List<ShortcutInfo> getStrequentShortcuts() {
216 // The limit query parameter doesn't seem to work for this uri but we'll leave it because in
217 // case it does work on some phones or platform versions.
218 final Uri uri = Contacts.CONTENT_STREQUENT_URI.buildUpon()
219 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
220 String.valueOf(MAX_SHORTCUTS))
221 .build();
222 final Cursor cursor = mContentResolver.query(uri, PROJECTION, null, null, null);
223
224 if (cursor == null) return Collections.emptyList();
225
226 final List<ShortcutInfo> result = new ArrayList<>();
227
228 try {
Marcus Hagerottf2e38082016-11-16 15:47:20 -0800229 int i = 0;
230 while (i < MAX_SHORTCUTS && cursor.moveToNext()) {
231 final ShortcutInfo shortcut = createShortcutFromRow(cursor);
232 if (shortcut == null) {
233 continue;
234 }
235 result.add(shortcut);
236 i++;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700237 }
238 } finally {
239 cursor.close();
240 }
241 return result;
242 }
243
244
245 @VisibleForTesting
246 ShortcutInfo createShortcutFromRow(Cursor cursor) {
247 final ShortcutInfo.Builder builder = builderForContactShortcut(cursor);
Marcus Hagerottf2e38082016-11-16 15:47:20 -0800248 if (builder == null) {
249 return null;
250 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700251 addIconForContact(cursor, builder);
252 return builder.build();
253 }
254
255 @VisibleForTesting
256 ShortcutInfo.Builder builderForContactShortcut(Cursor cursor) {
257 final long id = cursor.getLong(0);
258 final String lookupKey = cursor.getString(1);
259 final String displayName = cursor.getString(2);
260 return builderForContactShortcut(id, lookupKey, displayName);
261 }
262
263 @VisibleForTesting
264 ShortcutInfo.Builder builderForContactShortcut(long id, String lookupKey, String displayName) {
Marcus Hagerottf2e38082016-11-16 15:47:20 -0800265 if (lookupKey == null || displayName == null) {
266 return null;
267 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700268 final PersistableBundle extras = new PersistableBundle();
269 extras.putLong(Contacts._ID, id);
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -0800270 extras.putInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_CONTACT_URI);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700271
272 final ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mContext, lookupKey)
273 .setIntent(ImplicitIntentsUtil.getIntentForQuickContactLauncherShortcut(mContext,
274 Contacts.getLookupUri(id, lookupKey)))
275 .setDisabledMessage(mContext.getString(R.string.dynamic_shortcut_disabled_message))
276 .setExtras(extras);
277
278 if (displayName.length() < mLongLabelMaxLength) {
279 builder.setLongLabel(displayName);
280 } else {
281 builder.setLongLabel(displayName.substring(0, mLongLabelMaxLength - 1).trim() + "…");
282 }
283
284 if (displayName.length() < mShortLabelMaxLength) {
285 builder.setShortLabel(displayName);
286 } else {
287 builder.setShortLabel(displayName.substring(0, mShortLabelMaxLength - 1).trim() + "…");
288 }
289 return builder;
290 }
291
292 private void addIconForContact(Cursor cursor, ShortcutInfo.Builder builder) {
293 final long id = cursor.getLong(0);
294 final String lookupKey = cursor.getString(1);
295 final String displayName = cursor.getString(2);
296
297 final Bitmap bitmap = getContactPhoto(id);
298 if (bitmap != null) {
299 builder.setIcon(Icon.createWithBitmap(bitmap));
300 } else {
301 builder.setIcon(Icon.createWithBitmap(getFallbackAvatar(displayName, lookupKey)));
302 }
303 }
304
305 private Bitmap getContactPhoto(long id) {
306 final InputStream photoStream = Contacts.openContactPhotoInputStream(
307 mContext.getContentResolver(),
308 ContentUris.withAppendedId(Contacts.CONTENT_URI, id), true);
309
310 if (photoStream == null) return null;
311 try {
312 final Bitmap bitmap = decodeStreamForShortcut(photoStream);
313 photoStream.close();
314 return bitmap;
315 } catch (IOException e) {
316 Log.e(TAG, "Failed to decode contact photo for shortcut. ID=" + id, e);
317 return null;
318 } finally {
319 try {
320 photoStream.close();
321 } catch (IOException e) {
322 // swallow
323 }
324 }
325 }
326
327 private Bitmap decodeStreamForShortcut(InputStream stream) throws IOException {
328 final BitmapRegionDecoder bitmapDecoder = BitmapRegionDecoder.newInstance(stream, false);
329
330 final int sourceWidth = bitmapDecoder.getWidth();
331 final int sourceHeight = bitmapDecoder.getHeight();
332
333 final int iconMaxWidth = mShortcutManager.getIconMaxWidth();;
334 final int iconMaxHeight = mShortcutManager.getIconMaxHeight();
335
336 final int sampleSize = Math.min(
337 BitmapUtil.findOptimalSampleSize(sourceWidth,
338 RECOMMENDED_ICON_PIXEL_LENGTH),
339 BitmapUtil.findOptimalSampleSize(sourceHeight,
340 RECOMMENDED_ICON_PIXEL_LENGTH));
341 final BitmapFactory.Options opts = new BitmapFactory.Options();
342 opts.inSampleSize = sampleSize;
343
344 final int scaledWidth = sourceWidth / opts.inSampleSize;
345 final int scaledHeight = sourceHeight / opts.inSampleSize;
346
347 final int targetWidth = Math.min(scaledWidth, iconMaxWidth);
348 final int targetHeight = Math.min(scaledHeight, iconMaxHeight);
349
350 // Make it square.
351 final int targetSize = Math.min(targetWidth, targetHeight);
352
353 // The region is defined in the coordinates of the source image then the sampling is
354 // done on the extracted region.
355 final int prescaledXOffset = ((scaledWidth - targetSize) * opts.inSampleSize) / 2;
356 final int prescaledYOffset = ((scaledHeight - targetSize) * opts.inSampleSize) / 2;
357
358 final Bitmap bitmap = bitmapDecoder.decodeRegion(new Rect(
359 prescaledXOffset, prescaledYOffset,
360 sourceWidth - prescaledXOffset, sourceHeight - prescaledYOffset
361 ), opts);
362
363 bitmapDecoder.recycle();
364
365 return BitmapUtil.getRoundedBitmap(bitmap, targetSize, targetSize);
366 }
367
368 private Bitmap getFallbackAvatar(String displayName, String lookupKey) {
369 final int w = RECOMMENDED_ICON_PIXEL_LENGTH;
370 final int h = RECOMMENDED_ICON_PIXEL_LENGTH;
371
372 final ContactPhotoManager.DefaultImageRequest request =
373 new ContactPhotoManager.DefaultImageRequest(displayName, lookupKey, true);
374 final Drawable avatar = ContactPhotoManager.getDefaultAvatarDrawableForContact(
375 mContext.getResources(), true, request);
376 final Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
377 // The avatar won't draw unless it thinks it is visible
378 avatar.setVisible(true, true);
379 final Canvas canvas = new Canvas(result);
380 avatar.setBounds(0, 0, w, h);
381 avatar.draw(canvas);
382 return result;
383 }
384
Marcus Hagerott020f0412016-09-22 12:13:49 -0700385 @VisibleForTesting
386 void handleFlagDisabled() {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700387 removeAllShortcuts();
388 mJobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
389 }
390
391 private void removeAllShortcuts() {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700392 mShortcutManager.removeAllDynamicShortcuts();
393
394 final List<ShortcutInfo> pinned = mShortcutManager.getPinnedShortcuts();
395 final List<String> ids = new ArrayList<>(pinned.size());
396 for (ShortcutInfo shortcut : pinned) {
397 ids.add(shortcut.getId());
398 }
399 mShortcutManager.disableShortcuts(ids, mContext
400 .getString(R.string.dynamic_shortcut_disabled_message));
Marcus Hagerott020f0412016-09-22 12:13:49 -0700401 if (Log.isLoggable(TAG, Log.DEBUG)) {
402 Log.d(TAG, "DynamicShortcuts have been removed.");
403 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700404 }
405
406 @VisibleForTesting
407 void scheduleUpdateJob() {
408 final JobInfo job = new JobInfo.Builder(
409 ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID,
410 new ComponentName(mContext, ContactsJobService.class))
411 // We just observe all changes to contacts. It would be better to be more granular
412 // but CP2 only notifies using this URI anyway so there isn't any point in adding
413 // that complexity.
414 .addTriggerContentUri(new JobInfo.TriggerContentUri(ContactsContract.AUTHORITY_URI,
415 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
Arthur Wang8debbac2016-09-21 16:39:04 -0700416 .setTriggerContentUpdateDelay(mContentChangeMinUpdateDelay)
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700417 .setTriggerContentMaxDelay(mContentChangeMaxUpdateDelay)
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700418 .build();
Marcus Hagerott020f0412016-09-22 12:13:49 -0700419 mJobScheduler.schedule(job);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700420 }
421
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700422 void updateInBackground() {
423 new ShortcutUpdateTask(this).execute();
424 }
425
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700426 public synchronized static void initialize(Context context) {
Marcus Hagerott020f0412016-09-22 12:13:49 -0700427 if (Log.isLoggable(TAG, Log.DEBUG)) {
Walter Jangdf86ede2016-10-19 09:48:29 -0700428 final Flags flags = Flags.getInstance();
Marcus Hagerott020f0412016-09-22 12:13:49 -0700429 Log.d(TAG, "DyanmicShortcuts.initialize\nVERSION >= N_MR1? " +
430 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) +
Marcus Hagerottb2504c72016-12-13 09:29:18 -0800431 "\nisJobScheduled? " +
432 (CompatUtils.isLauncherShortcutCompatible() && isJobScheduled(context)) +
Marcus Hagerott020f0412016-09-22 12:13:49 -0700433 "\nminDelay=" +
434 flags.getInteger(Experiments.DYNAMIC_MIN_CONTENT_CHANGE_UPDATE_DELAY_MILLIS) +
435 "\nmaxDelay=" +
436 flags.getInteger(Experiments.DYNAMIC_MAX_CONTENT_CHANGE_UPDATE_DELAY_MILLIS));
437 }
438
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700439 if (!CompatUtils.isLauncherShortcutCompatible()) return;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700440
441 final DynamicShortcuts shortcuts = new DynamicShortcuts(context);
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700442
Walter Jang9adc9ef2016-11-02 18:50:38 -0700443 if (!shortcuts.hasRequiredPermissions()) {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700444 final IntentFilter filter = new IntentFilter();
445 filter.addAction(RequestPermissionsActivity.BROADCAST_PERMISSIONS_GRANTED);
446 LocalBroadcastManager.getInstance(shortcuts.mContext).registerReceiver(
447 new PermissionsGrantedReceiver(), filter);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700448 } else if (!isJobScheduled(context)) {
449 // Update the shortcuts. If the job is already scheduled then either the app is being
450 // launched to run the job in which case the shortcuts will get updated when it runs or
451 // it has been launched for some other reason and the data we care about for shortcuts
452 // hasn't changed. Because the job reschedules itself after completion this check
453 // essentially means that this will run on each app launch that happens after a reboot.
454 // Note: the task schedules the job after completing.
455 new ShortcutUpdateTask(shortcuts).execute();
456 }
457 }
458
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700459 @VisibleForTesting
460 public static void reset(Context context) {
461 final JobScheduler jobScheduler =
462 (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
463 jobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
464
465 if (!CompatUtils.isLauncherShortcutCompatible()) {
466 return;
467 }
468 new DynamicShortcuts(context).removeAllShortcuts();
469 }
470
471 @VisibleForTesting
472 boolean hasRequiredPermissions() {
473 return PermissionsUtil.hasContactsPermissions(mContext);
474 }
475
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700476 public static void updateFromJob(final JobService service, final JobParameters jobParams) {
477 new ShortcutUpdateTask(new DynamicShortcuts(service)) {
478 @Override
479 protected void onPostExecute(Void aVoid) {
480 // Must call super first which will reschedule the job before we call jobFinished
481 super.onPostExecute(aVoid);
482 service.jobFinished(jobParams, false);
483 }
484 }.execute();
485 }
486
487 @VisibleForTesting
488 public static boolean isJobScheduled(Context context) {
489 final JobScheduler scheduler = (JobScheduler) context
490 .getSystemService(Context.JOB_SCHEDULER_SERVICE);
491 return scheduler.getPendingJob(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID) != null;
492 }
493
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700494 public static void reportShortcutUsed(Context context, String lookupKey) {
Marcus Hagerott677ee2b2016-10-28 15:31:55 -0700495 if (!CompatUtils.isLauncherShortcutCompatible() || lookupKey == null) return;
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700496 final ShortcutManager shortcutManager = (ShortcutManager) context
497 .getSystemService(Context.SHORTCUT_SERVICE);
498 shortcutManager.reportShortcutUsed(lookupKey);
499 }
500
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700501 private static class ShortcutUpdateTask extends AsyncTask<Void, Void, Void> {
502 private DynamicShortcuts mDynamicShortcuts;
503
504 public ShortcutUpdateTask(DynamicShortcuts shortcuts) {
505 mDynamicShortcuts = shortcuts;
506 }
507
508 @Override
509 protected Void doInBackground(Void... voids) {
510 mDynamicShortcuts.refresh();
511 return null;
512 }
513
514 @Override
515 protected void onPostExecute(Void aVoid) {
Marcus Hagerott020f0412016-09-22 12:13:49 -0700516 if (Log.isLoggable(TAG, Log.DEBUG)) {
517 Log.d(TAG, "ShorcutUpdateTask.onPostExecute");
518 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700519 // The shortcuts may have changed so update the job so that we are observing the
520 // correct Uris
521 mDynamicShortcuts.scheduleUpdateJob();
522 }
523 }
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700524
525 private static class PermissionsGrantedReceiver extends BroadcastReceiver {
526 @Override
527 public void onReceive(Context context, Intent intent) {
528 // Clear the receiver.
529 LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
530 DynamicShortcuts.initialize(context);
531 }
532 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700533}