blob: 0901e56cd28b2092b5138bdde5fbd311d746ca49 [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
Gary Mai91520d72017-02-28 10:56:44 -080070 * schedule a Job to keep the shortcuts up-to-date so no further interactions should be necessary.
Marcus Hagerottc5083f92016-09-14 08:34:29 -070071 */
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
Gary Mai91520d72017-02-28 10:56:44 -0800292 public ShortcutInfo getQuickContactShortcutInfo(long id, String lookupKey, String displayName) {
293 final ShortcutInfo.Builder builder = builderForContactShortcut(id, lookupKey, displayName);
294 addIconForContact(id, lookupKey, displayName, builder);
295 return builder.build();
296 }
297
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700298 private void addIconForContact(Cursor cursor, ShortcutInfo.Builder builder) {
299 final long id = cursor.getLong(0);
300 final String lookupKey = cursor.getString(1);
301 final String displayName = cursor.getString(2);
Gary Mai91520d72017-02-28 10:56:44 -0800302 addIconForContact(id, lookupKey, displayName, builder);
303 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700304
Gary Mai91520d72017-02-28 10:56:44 -0800305 private void addIconForContact(long id, String lookupKey, String displayName,
306 ShortcutInfo.Builder builder) {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700307 final Bitmap bitmap = getContactPhoto(id);
308 if (bitmap != null) {
309 builder.setIcon(Icon.createWithBitmap(bitmap));
310 } else {
311 builder.setIcon(Icon.createWithBitmap(getFallbackAvatar(displayName, lookupKey)));
312 }
313 }
314
315 private Bitmap getContactPhoto(long id) {
316 final InputStream photoStream = Contacts.openContactPhotoInputStream(
317 mContext.getContentResolver(),
318 ContentUris.withAppendedId(Contacts.CONTENT_URI, id), true);
319
320 if (photoStream == null) return null;
321 try {
322 final Bitmap bitmap = decodeStreamForShortcut(photoStream);
323 photoStream.close();
324 return bitmap;
325 } catch (IOException e) {
326 Log.e(TAG, "Failed to decode contact photo for shortcut. ID=" + id, e);
327 return null;
328 } finally {
329 try {
330 photoStream.close();
331 } catch (IOException e) {
332 // swallow
333 }
334 }
335 }
336
337 private Bitmap decodeStreamForShortcut(InputStream stream) throws IOException {
338 final BitmapRegionDecoder bitmapDecoder = BitmapRegionDecoder.newInstance(stream, false);
339
340 final int sourceWidth = bitmapDecoder.getWidth();
341 final int sourceHeight = bitmapDecoder.getHeight();
342
343 final int iconMaxWidth = mShortcutManager.getIconMaxWidth();;
344 final int iconMaxHeight = mShortcutManager.getIconMaxHeight();
345
346 final int sampleSize = Math.min(
347 BitmapUtil.findOptimalSampleSize(sourceWidth,
348 RECOMMENDED_ICON_PIXEL_LENGTH),
349 BitmapUtil.findOptimalSampleSize(sourceHeight,
350 RECOMMENDED_ICON_PIXEL_LENGTH));
351 final BitmapFactory.Options opts = new BitmapFactory.Options();
352 opts.inSampleSize = sampleSize;
353
354 final int scaledWidth = sourceWidth / opts.inSampleSize;
355 final int scaledHeight = sourceHeight / opts.inSampleSize;
356
357 final int targetWidth = Math.min(scaledWidth, iconMaxWidth);
358 final int targetHeight = Math.min(scaledHeight, iconMaxHeight);
359
360 // Make it square.
361 final int targetSize = Math.min(targetWidth, targetHeight);
362
363 // The region is defined in the coordinates of the source image then the sampling is
364 // done on the extracted region.
365 final int prescaledXOffset = ((scaledWidth - targetSize) * opts.inSampleSize) / 2;
366 final int prescaledYOffset = ((scaledHeight - targetSize) * opts.inSampleSize) / 2;
367
368 final Bitmap bitmap = bitmapDecoder.decodeRegion(new Rect(
369 prescaledXOffset, prescaledYOffset,
370 sourceWidth - prescaledXOffset, sourceHeight - prescaledYOffset
371 ), opts);
372
373 bitmapDecoder.recycle();
374
375 return BitmapUtil.getRoundedBitmap(bitmap, targetSize, targetSize);
376 }
377
378 private Bitmap getFallbackAvatar(String displayName, String lookupKey) {
379 final int w = RECOMMENDED_ICON_PIXEL_LENGTH;
380 final int h = RECOMMENDED_ICON_PIXEL_LENGTH;
381
382 final ContactPhotoManager.DefaultImageRequest request =
383 new ContactPhotoManager.DefaultImageRequest(displayName, lookupKey, true);
384 final Drawable avatar = ContactPhotoManager.getDefaultAvatarDrawableForContact(
385 mContext.getResources(), true, request);
386 final Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
387 // The avatar won't draw unless it thinks it is visible
388 avatar.setVisible(true, true);
389 final Canvas canvas = new Canvas(result);
390 avatar.setBounds(0, 0, w, h);
391 avatar.draw(canvas);
392 return result;
393 }
394
Marcus Hagerott020f0412016-09-22 12:13:49 -0700395 @VisibleForTesting
396 void handleFlagDisabled() {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700397 removeAllShortcuts();
398 mJobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
399 }
400
401 private void removeAllShortcuts() {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700402 mShortcutManager.removeAllDynamicShortcuts();
403
404 final List<ShortcutInfo> pinned = mShortcutManager.getPinnedShortcuts();
405 final List<String> ids = new ArrayList<>(pinned.size());
406 for (ShortcutInfo shortcut : pinned) {
407 ids.add(shortcut.getId());
408 }
409 mShortcutManager.disableShortcuts(ids, mContext
410 .getString(R.string.dynamic_shortcut_disabled_message));
Marcus Hagerott020f0412016-09-22 12:13:49 -0700411 if (Log.isLoggable(TAG, Log.DEBUG)) {
412 Log.d(TAG, "DynamicShortcuts have been removed.");
413 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700414 }
415
416 @VisibleForTesting
417 void scheduleUpdateJob() {
418 final JobInfo job = new JobInfo.Builder(
419 ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID,
420 new ComponentName(mContext, ContactsJobService.class))
421 // We just observe all changes to contacts. It would be better to be more granular
422 // but CP2 only notifies using this URI anyway so there isn't any point in adding
423 // that complexity.
424 .addTriggerContentUri(new JobInfo.TriggerContentUri(ContactsContract.AUTHORITY_URI,
425 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
Arthur Wang8debbac2016-09-21 16:39:04 -0700426 .setTriggerContentUpdateDelay(mContentChangeMinUpdateDelay)
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700427 .setTriggerContentMaxDelay(mContentChangeMaxUpdateDelay)
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700428 .build();
Marcus Hagerott020f0412016-09-22 12:13:49 -0700429 mJobScheduler.schedule(job);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700430 }
431
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700432 void updateInBackground() {
433 new ShortcutUpdateTask(this).execute();
434 }
435
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700436 public synchronized static void initialize(Context context) {
Marcus Hagerott020f0412016-09-22 12:13:49 -0700437 if (Log.isLoggable(TAG, Log.DEBUG)) {
Walter Jangdf86ede2016-10-19 09:48:29 -0700438 final Flags flags = Flags.getInstance();
Marcus Hagerott020f0412016-09-22 12:13:49 -0700439 Log.d(TAG, "DyanmicShortcuts.initialize\nVERSION >= N_MR1? " +
440 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) +
Marcus Hagerottb2504c72016-12-13 09:29:18 -0800441 "\nisJobScheduled? " +
442 (CompatUtils.isLauncherShortcutCompatible() && isJobScheduled(context)) +
Marcus Hagerott020f0412016-09-22 12:13:49 -0700443 "\nminDelay=" +
444 flags.getInteger(Experiments.DYNAMIC_MIN_CONTENT_CHANGE_UPDATE_DELAY_MILLIS) +
445 "\nmaxDelay=" +
446 flags.getInteger(Experiments.DYNAMIC_MAX_CONTENT_CHANGE_UPDATE_DELAY_MILLIS));
447 }
448
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700449 if (!CompatUtils.isLauncherShortcutCompatible()) return;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700450
451 final DynamicShortcuts shortcuts = new DynamicShortcuts(context);
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700452
Walter Jang9adc9ef2016-11-02 18:50:38 -0700453 if (!shortcuts.hasRequiredPermissions()) {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700454 final IntentFilter filter = new IntentFilter();
455 filter.addAction(RequestPermissionsActivity.BROADCAST_PERMISSIONS_GRANTED);
456 LocalBroadcastManager.getInstance(shortcuts.mContext).registerReceiver(
457 new PermissionsGrantedReceiver(), filter);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700458 } else if (!isJobScheduled(context)) {
459 // Update the shortcuts. If the job is already scheduled then either the app is being
460 // launched to run the job in which case the shortcuts will get updated when it runs or
461 // it has been launched for some other reason and the data we care about for shortcuts
462 // hasn't changed. Because the job reschedules itself after completion this check
463 // essentially means that this will run on each app launch that happens after a reboot.
464 // Note: the task schedules the job after completing.
465 new ShortcutUpdateTask(shortcuts).execute();
466 }
467 }
468
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700469 @VisibleForTesting
470 public static void reset(Context context) {
471 final JobScheduler jobScheduler =
472 (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
473 jobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
474
475 if (!CompatUtils.isLauncherShortcutCompatible()) {
476 return;
477 }
478 new DynamicShortcuts(context).removeAllShortcuts();
479 }
480
481 @VisibleForTesting
482 boolean hasRequiredPermissions() {
483 return PermissionsUtil.hasContactsPermissions(mContext);
484 }
485
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700486 public static void updateFromJob(final JobService service, final JobParameters jobParams) {
487 new ShortcutUpdateTask(new DynamicShortcuts(service)) {
488 @Override
489 protected void onPostExecute(Void aVoid) {
490 // Must call super first which will reschedule the job before we call jobFinished
491 super.onPostExecute(aVoid);
492 service.jobFinished(jobParams, false);
493 }
494 }.execute();
495 }
496
497 @VisibleForTesting
498 public static boolean isJobScheduled(Context context) {
499 final JobScheduler scheduler = (JobScheduler) context
500 .getSystemService(Context.JOB_SCHEDULER_SERVICE);
501 return scheduler.getPendingJob(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID) != null;
502 }
503
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700504 public static void reportShortcutUsed(Context context, String lookupKey) {
Marcus Hagerott677ee2b2016-10-28 15:31:55 -0700505 if (!CompatUtils.isLauncherShortcutCompatible() || lookupKey == null) return;
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700506 final ShortcutManager shortcutManager = (ShortcutManager) context
507 .getSystemService(Context.SHORTCUT_SERVICE);
508 shortcutManager.reportShortcutUsed(lookupKey);
509 }
510
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700511 private static class ShortcutUpdateTask extends AsyncTask<Void, Void, Void> {
512 private DynamicShortcuts mDynamicShortcuts;
513
514 public ShortcutUpdateTask(DynamicShortcuts shortcuts) {
515 mDynamicShortcuts = shortcuts;
516 }
517
518 @Override
519 protected Void doInBackground(Void... voids) {
520 mDynamicShortcuts.refresh();
521 return null;
522 }
523
524 @Override
525 protected void onPostExecute(Void aVoid) {
Marcus Hagerott020f0412016-09-22 12:13:49 -0700526 if (Log.isLoggable(TAG, Log.DEBUG)) {
527 Log.d(TAG, "ShorcutUpdateTask.onPostExecute");
528 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700529 // The shortcuts may have changed so update the job so that we are observing the
530 // correct Uris
531 mDynamicShortcuts.scheduleUpdateJob();
532 }
533 }
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700534
535 private static class PermissionsGrantedReceiver extends BroadcastReceiver {
536 @Override
537 public void onReceive(Context context, Intent intent) {
538 // Clear the receiver.
539 LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
540 DynamicShortcuts.initialize(context);
541 }
542 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700543}