blob: 8130776aeabe9aa928250ee8e75287fff949843c [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;
Gary Mai02c3dee2017-05-05 15:49:11 -070019import android.app.ActivityManager;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070020import android.app.job.JobInfo;
21import android.app.job.JobParameters;
22import android.app.job.JobScheduler;
23import android.app.job.JobService;
Marcus Hagerott8ac989c2016-10-04 08:45:58 -070024import android.content.BroadcastReceiver;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070025import android.content.ComponentName;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.Context;
Marcus Hagerott8ac989c2016-10-04 08:45:58 -070029import android.content.Intent;
30import android.content.IntentFilter;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070031import android.content.pm.ShortcutInfo;
32import android.content.pm.ShortcutManager;
33import android.database.Cursor;
34import android.graphics.Bitmap;
35import android.graphics.BitmapFactory;
36import android.graphics.BitmapRegionDecoder;
37import android.graphics.Canvas;
38import android.graphics.Rect;
Gary Maia80b9372017-03-21 18:02:19 -070039import android.graphics.drawable.AdaptiveIconDrawable;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070040import android.graphics.drawable.Drawable;
41import android.graphics.drawable.Icon;
42import android.net.Uri;
43import android.os.AsyncTask;
44import android.os.Build;
45import android.os.PersistableBundle;
46import android.provider.ContactsContract;
47import android.provider.ContactsContract.Contacts;
Aravind Sreekumar71212852018-04-06 15:47:45 -070048import androidx.annotation.VisibleForTesting;
49import androidx.localbroadcastmanager.content.LocalBroadcastManager;
50import androidx.core.os.BuildCompat;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070051import android.util.Log;
52
Gary Mai0a49afa2016-12-05 15:53:58 -080053import com.android.contacts.activities.RequestPermissionsActivity;
Gary Mai69c182a2016-12-05 13:07:03 -080054import com.android.contacts.compat.CompatUtils;
Gary Mai02c3dee2017-05-05 15:49:11 -070055import com.android.contacts.lettertiles.LetterTileDrawable;
Gary Mai69c182a2016-12-05 13:07:03 -080056import com.android.contacts.util.BitmapUtil;
57import com.android.contacts.util.ImplicitIntentsUtil;
58import com.android.contacts.util.PermissionsUtil;
Marcus Hagerottc5083f92016-09-14 08:34:29 -070059import com.android.contactsbind.experiments.Flags;
60
61import java.io.IOException;
62import java.io.InputStream;
63import java.util.ArrayList;
64import java.util.Collections;
65import java.util.List;
66
Marcus Hagerottc5083f92016-09-14 08:34:29 -070067/**
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
Gary Mai91520d72017-02-28 10:56:44 -080074 * schedule a Job to keep the shortcuts up-to-date so no further interactions should be necessary.
Marcus Hagerottc5083f92016-09-14 08:34:29 -070075 */
76@TargetApi(Build.VERSION_CODES.N_MR1)
77public class DynamicShortcuts {
78 private static final String TAG = "DynamicShortcuts";
79
Marcus Hagerottd105c1e2016-09-30 14:28:00 -070080 // 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
Marcus Hagerottc5083f92016-09-14 08:34:29 -070085 // 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
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -080092 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;
Gary Mai19186832017-03-17 13:38:43 -070098 private static final int SHORTCUT_TYPE_ACTION_URI = 2;
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -080099
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700100 @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;
Gary Mai02c3dee2017-05-05 15:49:11 -0700106
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700107 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;
Gary Mai02c3dee2017-05-05 15:49:11 -0700111 private int mIconSize;
Arthur Wang8debbac2016-09-21 16:39:04 -0700112 private final int mContentChangeMinUpdateDelay;
113 private final int mContentChangeMaxUpdateDelay;
Marcus Hagerott020f0412016-09-22 12:13:49 -0700114 private final JobScheduler mJobScheduler;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700115
116 public DynamicShortcuts(Context context) {
117 this(context, context.getContentResolver(), (ShortcutManager)
Marcus Hagerott020f0412016-09-22 12:13:49 -0700118 context.getSystemService(Context.SHORTCUT_SERVICE),
119 (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE));
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700120 }
121
Marcus Hagerott020f0412016-09-22 12:13:49 -0700122 @VisibleForTesting
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700123 public DynamicShortcuts(Context context, ContentResolver contentResolver,
Marcus Hagerott020f0412016-09-22 12:13:49 -0700124 ShortcutManager shortcutManager, JobScheduler jobScheduler) {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700125 mContext = context;
126 mContentResolver = contentResolver;
127 mShortcutManager = shortcutManager;
Marcus Hagerott020f0412016-09-22 12:13:49 -0700128 mJobScheduler = jobScheduler;
Walter Jangdf86ede2016-10-19 09:48:29 -0700129 mContentChangeMinUpdateDelay = Flags.getInstance()
Arthur Wang8debbac2016-09-21 16:39:04 -0700130 .getInteger(Experiments.DYNAMIC_MIN_CONTENT_CHANGE_UPDATE_DELAY_MILLIS);
Walter Jangdf86ede2016-10-19 09:48:29 -0700131 mContentChangeMaxUpdateDelay = Flags.getInstance()
Arthur Wang8debbac2016-09-21 16:39:04 -0700132 .getInteger(Experiments.DYNAMIC_MAX_CONTENT_CHANGE_UPDATE_DELAY_MILLIS);
Gary Mai02c3dee2017-05-05 15:49:11 -0700133 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 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700139 }
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() {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700153 // 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
Marcus Hagerott020f0412016-09-22 12:13:49 -0700157 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 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700162 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()) {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700172 final PersistableBundle extras = shortcut.getExtras();
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -0800173
Gary Mai19186832017-03-17 13:38:43 -0700174 if (extras == null || extras.getInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_UNKNOWN) !=
175 SHORTCUT_TYPE_CONTACT_URI) {
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -0800176 continue;
177 }
178
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700179 // The contact ID may have changed but that's OK because it is just an optimization
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -0800180 final long contactId = extras.getLong(Contacts._ID);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700181
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
Marcus Hagerott020f0412016-09-22 12:13:49 -0700196 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
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700202 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 {
Marcus Hagerottf2e38082016-11-16 15:47:20 -0800236 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++;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700244 }
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);
Marcus Hagerottf2e38082016-11-16 15:47:20 -0800255 if (builder == null) {
256 return null;
257 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700258 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) {
Marcus Hagerottf2e38082016-11-16 15:47:20 -0800272 if (lookupKey == null || displayName == null) {
273 return null;
274 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700275 final PersistableBundle extras = new PersistableBundle();
276 extras.putLong(Contacts._ID, id);
Marcus Hagerott5d1ec1d2016-12-13 08:49:55 -0800277 extras.putInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_CONTACT_URI);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700278
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
Gary Mai08d87ee2017-03-15 11:01:28 -0700285 setLabel(builder, displayName);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700286 return builder;
287 }
288
Gary Mai08d87ee2017-03-15 11:01:28 -0700289 @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();
Gary Mai19186832017-03-17 13:38:43 -0700295 extras.putInt(EXTRA_SHORTCUT_TYPE, SHORTCUT_TYPE_ACTION_URI);
Gary Mai08d87ee2017-03-15 11:01:28 -0700296
297 final ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mContext, id)
298 .setIntent(action)
299 .setIcon(icon)
Gary Mai9b1b9372017-06-13 17:43:39 -0700300 .setExtras(extras)
Gary Mai08d87ee2017-03-15 11:01:28 -0700301 .setDisabledMessage(mContext.getString(R.string.dynamic_shortcut_disabled_message));
302
303 setLabel(builder, label);
304 return builder.build();
305 }
306
Gary Mai91520d72017-02-28 10:56:44 -0800307 public ShortcutInfo getQuickContactShortcutInfo(long id, String lookupKey, String displayName) {
308 final ShortcutInfo.Builder builder = builderForContactShortcut(id, lookupKey, displayName);
Gary Mai9b1b9372017-06-13 17:43:39 -0700309 if (builder == null) {
310 return null;
311 }
Yingren Wang1f9f8512019-02-22 14:16:35 +0800312 addIconForContact(id, lookupKey, displayName, builder);
Gary Mai91520d72017-02-28 10:56:44 -0800313 return builder.build();
314 }
315
Gary Mai08d87ee2017-03-15 11:01:28 -0700316 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
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700330 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);
Gary Mai91520d72017-02-28 10:56:44 -0800334 addIconForContact(id, lookupKey, displayName, builder);
335 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700336
Gary Mai91520d72017-02-28 10:56:44 -0800337 private void addIconForContact(long id, String lookupKey, String displayName,
338 ShortcutInfo.Builder builder) {
Gary Mai19186832017-03-17 13:38:43 -0700339 Bitmap bitmap = getContactPhoto(id);
340 if (bitmap == null) {
Yingren Wang1f9f8512019-02-22 14:16:35 +0800341 bitmap = getFallbackAvatar(displayName, lookupKey);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700342 }
Gary Maia80b9372017-03-21 18:02:19 -0700343 final Icon icon;
344 if (BuildCompat.isAtLeastO()) {
345 icon = Icon.createWithAdaptiveBitmap(bitmap);
346 } else {
347 icon = Icon.createWithBitmap(bitmap);
348 }
Gary Mai19186832017-03-17 13:38:43 -0700349
350 builder.setIcon(icon);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700351 }
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
Gary Maia80b9372017-03-21 18:02:19 -0700381 final int iconMaxWidth = mShortcutManager.getIconMaxWidth();
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700382 final int iconMaxHeight = mShortcutManager.getIconMaxHeight();
383
384 final int sampleSize = Math.min(
Gary Mai02c3dee2017-05-05 15:49:11 -0700385 BitmapUtil.findOptimalSampleSize(sourceWidth, mIconSize),
386 BitmapUtil.findOptimalSampleSize(sourceHeight, mIconSize));
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700387 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);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700408 bitmapDecoder.recycle();
409
Gary Maia80b9372017-03-21 18:02:19 -0700410 if (!BuildCompat.isAtLeastO()) {
411 return BitmapUtil.getRoundedBitmap(bitmap, targetSize, targetSize);
412 }
413
Gary Mai02c3dee2017-05-05 15:49:11 -0700414 return bitmap;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700415 }
416
Yingren Wang1f9f8512019-02-22 14:16:35 +0800417 private Bitmap getFallbackAvatar(String displayName, String lookupKey) {
Gary Mai02c3dee2017-05-05 15:49:11 -0700418 // Use a circular icon if we're not on O or higher.
419 final boolean circularIcon = !BuildCompat.isAtLeastO();
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700420
421 final ContactPhotoManager.DefaultImageRequest request =
Gary Mai02c3dee2017-05-05 15:49:11 -0700422 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 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700427 final Drawable avatar = ContactPhotoManager.getDefaultAvatarDrawableForContact(
Yingren Wang1f9f8512019-02-22 14:16:35 +0800428 mContext.getResources(), true, request);
Gary Mai02c3dee2017-05-05 15:49:11 -0700429 final Bitmap result = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700430 // The avatar won't draw unless it thinks it is visible
431 avatar.setVisible(true, true);
432 final Canvas canvas = new Canvas(result);
Gary Mai02c3dee2017-05-05 15:49:11 -0700433 avatar.setBounds(0, 0, mIconSize, mIconSize);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700434 avatar.draw(canvas);
435 return result;
436 }
437
Marcus Hagerott020f0412016-09-22 12:13:49 -0700438 @VisibleForTesting
439 void handleFlagDisabled() {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700440 removeAllShortcuts();
441 mJobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
442 }
443
444 private void removeAllShortcuts() {
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700445 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));
Marcus Hagerott020f0412016-09-22 12:13:49 -0700454 if (Log.isLoggable(TAG, Log.DEBUG)) {
455 Log.d(TAG, "DynamicShortcuts have been removed.");
456 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700457 }
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))
Arthur Wang8debbac2016-09-21 16:39:04 -0700469 .setTriggerContentUpdateDelay(mContentChangeMinUpdateDelay)
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700470 .setTriggerContentMaxDelay(mContentChangeMaxUpdateDelay)
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700471 .build();
Marcus Hagerott020f0412016-09-22 12:13:49 -0700472 mJobScheduler.schedule(job);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700473 }
474
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700475 void updateInBackground() {
476 new ShortcutUpdateTask(this).execute();
477 }
478
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700479 public synchronized static void initialize(Context context) {
Marcus Hagerott020f0412016-09-22 12:13:49 -0700480 if (Log.isLoggable(TAG, Log.DEBUG)) {
Walter Jangdf86ede2016-10-19 09:48:29 -0700481 final Flags flags = Flags.getInstance();
Marcus Hagerott020f0412016-09-22 12:13:49 -0700482 Log.d(TAG, "DyanmicShortcuts.initialize\nVERSION >= N_MR1? " +
483 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) +
Marcus Hagerottb2504c72016-12-13 09:29:18 -0800484 "\nisJobScheduled? " +
485 (CompatUtils.isLauncherShortcutCompatible() && isJobScheduled(context)) +
Marcus Hagerott020f0412016-09-22 12:13:49 -0700486 "\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
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700492 if (!CompatUtils.isLauncherShortcutCompatible()) return;
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700493
494 final DynamicShortcuts shortcuts = new DynamicShortcuts(context);
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700495
Walter Jang9adc9ef2016-11-02 18:50:38 -0700496 if (!shortcuts.hasRequiredPermissions()) {
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700497 final IntentFilter filter = new IntentFilter();
498 filter.addAction(RequestPermissionsActivity.BROADCAST_PERMISSIONS_GRANTED);
499 LocalBroadcastManager.getInstance(shortcuts.mContext).registerReceiver(
500 new PermissionsGrantedReceiver(), filter);
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700501 } 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
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700512 @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
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700529 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
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700547 public static void reportShortcutUsed(Context context, String lookupKey) {
Marcus Hagerott677ee2b2016-10-28 15:31:55 -0700548 if (!CompatUtils.isLauncherShortcutCompatible() || lookupKey == null) return;
Marcus Hagerottd105c1e2016-09-30 14:28:00 -0700549 final ShortcutManager shortcutManager = (ShortcutManager) context
550 .getSystemService(Context.SHORTCUT_SERVICE);
551 shortcutManager.reportShortcutUsed(lookupKey);
552 }
553
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700554 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) {
Marcus Hagerott020f0412016-09-22 12:13:49 -0700569 if (Log.isLoggable(TAG, Log.DEBUG)) {
570 Log.d(TAG, "ShorcutUpdateTask.onPostExecute");
571 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700572 // The shortcuts may have changed so update the job so that we are observing the
573 // correct Uris
574 mDynamicShortcuts.scheduleUpdateJob();
575 }
576 }
Marcus Hagerott8ac989c2016-10-04 08:45:58 -0700577
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 }
Marcus Hagerottc5083f92016-09-14 08:34:29 -0700586}