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 
17 package com.android.dialer.shortcuts;
18 
19 import android.Manifest;
20 import android.annotation.TargetApi;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ShortcutInfo;
24 import android.content.pm.ShortcutManager;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Build.VERSION_CODES;
28 import android.provider.ContactsContract.Contacts;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.WorkerThread;
31 import android.support.v4.content.ContextCompat;
32 import android.util.ArrayMap;
33 import com.android.dialer.common.Assert;
34 import com.android.dialer.common.LogUtil;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Map;
38 
39 /**
40  * Handles refreshing of dialer pinned shortcuts.
41  *
42  * <p>Pinned shortcuts are icons that the user has dragged to their home screen from the dialer
43  * application launcher shortcut menu, which is accessible by tapping and holding the dialer
44  * launcher icon from the app drawer or a home screen.
45  *
46  * <p>When refreshing pinned shortcuts, we check to make sure that pinned contact information is
47  * still up to date (e.g. photo and name). We also check to see if the contact has been deleted from
48  * the user's contacts, and if so, we disable the pinned shortcut.
49  *
50  */
51 @TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
52 final class PinnedShortcuts {
53 
54   private static final String[] PROJECTION =
55       new String[] {
56         Contacts._ID, Contacts.DISPLAY_NAME_PRIMARY, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
57       };
58 
59   private static class Delta {
60 
61     final List<String> shortcutIdsToDisable = new ArrayList<>();
62     final Map<String, DialerShortcut> shortcutsToUpdateById = new ArrayMap<>();
63   }
64 
65   private final Context context;
66   private final ShortcutInfoFactory shortcutInfoFactory;
67 
PinnedShortcuts(@onNull Context context)68   PinnedShortcuts(@NonNull Context context) {
69     this.context = context;
70     this.shortcutInfoFactory = new ShortcutInfoFactory(context, new IconFactory(context));
71   }
72 
73   /**
74    * Performs a "complete refresh" of pinned shortcuts. This is done by (synchronously) querying for
75    * all contacts which currently have pinned shortcuts. The query results are used to compute a
76    * delta which contains a list of shortcuts which need to be updated (e.g. because of name/photo
77    * changes) or disabled (if contacts were deleted). Note that pinned shortcuts cannot be deleted
78    * programmatically and must be deleted by the user.
79    *
80    * <p>If the delta is non-empty, it is applied by making appropriate calls to the {@link
81    * ShortcutManager} system service.
82    *
83    * <p>This is a slow blocking call which performs file I/O and should not be performed on the main
84    * thread.
85    */
86   @WorkerThread
refresh()87   public void refresh() {
88     Assert.isWorkerThread();
89     LogUtil.enterBlock("PinnedShortcuts.refresh");
90 
91     if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
92         != PackageManager.PERMISSION_GRANTED) {
93       LogUtil.i("PinnedShortcuts.refresh", "no contact permissions");
94       return;
95     }
96 
97     Delta delta = new Delta();
98     ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
99     for (ShortcutInfo shortcutInfo : shortcutManager.getPinnedShortcuts()) {
100       if (shortcutInfo.isDeclaredInManifest()) {
101         // We never update/disable the manifest shortcut (the "create new contact" shortcut).
102         continue;
103       }
104       if (shortcutInfo.isDynamic()) {
105         // If the shortcut is both pinned and dynamic, let the logic which updates dynamic shortcuts
106         // handle the update. It would be problematic to try and apply the update here, because the
107         // setRank is nonsensical for pinned shortcuts and therefore could not be calculated.
108         continue;
109       }
110 
111       String lookupKey = DialerShortcut.getLookupKeyFromShortcutInfo(shortcutInfo);
112       Uri lookupUri = DialerShortcut.getLookupUriFromShortcutInfo(shortcutInfo);
113 
114       try (Cursor cursor =
115           context.getContentResolver().query(lookupUri, PROJECTION, null, null, null)) {
116 
117         if (cursor == null || !cursor.moveToNext()) {
118           LogUtil.i("PinnedShortcuts.refresh", "contact disabled");
119           delta.shortcutIdsToDisable.add(shortcutInfo.getId());
120           continue;
121         }
122 
123         // Note: The lookup key may have changed but we cannot refresh it because that would require
124         // changing the shortcut ID, which can only be accomplished with a remove and add; but
125         // pinned shortcuts cannot be added or removed.
126         DialerShortcut shortcut =
127             DialerShortcut.builder()
128                 .setContactId(cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID)))
129                 .setLookupKey(lookupKey)
130                 .setDisplayName(
131                     cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_PRIMARY)))
132                 .build();
133 
134         if (shortcut.needsUpdate(shortcutInfo)) {
135           LogUtil.i("PinnedShortcuts.refresh", "contact updated");
136           delta.shortcutsToUpdateById.put(shortcutInfo.getId(), shortcut);
137         }
138       }
139     }
140     applyDelta(delta);
141   }
142 
applyDelta(@onNull Delta delta)143   private void applyDelta(@NonNull Delta delta) {
144     ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
145     String shortcutDisabledMessage =
146         context.getResources().getString(R.string.dialer_shortcut_disabled_message);
147     if (!delta.shortcutIdsToDisable.isEmpty()) {
148       shortcutManager.disableShortcuts(delta.shortcutIdsToDisable, shortcutDisabledMessage);
149     }
150     if (!delta.shortcutsToUpdateById.isEmpty()) {
151       // Note: This call updates both pinned and dynamic shortcuts, but the delta should contain
152       // no dynamic shortcuts.
153       if (!shortcutManager.updateShortcuts(
154           shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToUpdateById))) {
155         LogUtil.i("PinnedShortcuts.applyDelta", "shortcutManager rate limited.");
156       }
157     }
158   }
159 }
160