• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 @TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
51 final class PinnedShortcuts {
52 
53   private static final String[] PROJECTION =
54       new String[] {
55         Contacts._ID, Contacts.DISPLAY_NAME_PRIMARY, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
56       };
57 
58   private static class Delta {
59 
60     final List<String> shortcutIdsToDisable = new ArrayList<>();
61     final Map<String, DialerShortcut> shortcutsToUpdateById = new ArrayMap<>();
62   }
63 
64   private final Context context;
65   private final ShortcutInfoFactory shortcutInfoFactory;
66 
PinnedShortcuts(@onNull Context context)67   PinnedShortcuts(@NonNull Context context) {
68     this.context = context;
69     this.shortcutInfoFactory = new ShortcutInfoFactory(context, new IconFactory(context));
70   }
71 
72   /**
73    * Performs a "complete refresh" of pinned shortcuts. This is done by (synchronously) querying for
74    * all contacts which currently have pinned shortcuts. The query results are used to compute a
75    * delta which contains a list of shortcuts which need to be updated (e.g. because of name/photo
76    * changes) or disabled (if contacts were deleted). Note that pinned shortcuts cannot be deleted
77    * programmatically and must be deleted by the user.
78    *
79    * <p>If the delta is non-empty, it is applied by making appropriate calls to the {@link
80    * ShortcutManager} system service.
81    *
82    * <p>This is a slow blocking call which performs file I/O and should not be performed on the main
83    * thread.
84    */
85   @WorkerThread
refresh()86   public void refresh() {
87     Assert.isWorkerThread();
88     LogUtil.enterBlock("PinnedShortcuts.refresh");
89 
90     if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
91         != PackageManager.PERMISSION_GRANTED) {
92       LogUtil.i("PinnedShortcuts.refresh", "no contact permissions");
93       return;
94     }
95 
96     Delta delta = new Delta();
97     ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
98     for (ShortcutInfo shortcutInfo : shortcutManager.getPinnedShortcuts()) {
99       if (shortcutInfo.isDeclaredInManifest()) {
100         // We never update/disable the manifest shortcut (the "create new contact" shortcut).
101         continue;
102       }
103       if (shortcutInfo.isDynamic()) {
104         // If the shortcut is both pinned and dynamic, let the logic which updates dynamic shortcuts
105         // handle the update. It would be problematic to try and apply the update here, because the
106         // setRank is nonsensical for pinned shortcuts and therefore could not be calculated.
107         continue;
108       }
109       // Exclude shortcuts not for contacts.
110       String action = null;
111       if (shortcutInfo.getIntent() != null) {
112         action = shortcutInfo.getIntent().getAction();
113       }
114       if (action == null || !action.equals("com.android.dialer.shortcuts.CALL_CONTACT")) {
115         continue;
116       }
117 
118       String lookupKey = DialerShortcut.getLookupKeyFromShortcutInfo(shortcutInfo);
119       Uri lookupUri = DialerShortcut.getLookupUriFromShortcutInfo(shortcutInfo);
120 
121       try (Cursor cursor =
122           context.getContentResolver().query(lookupUri, PROJECTION, null, null, null)) {
123 
124         if (cursor == null || !cursor.moveToNext()) {
125           LogUtil.i("PinnedShortcuts.refresh", "contact disabled");
126           delta.shortcutIdsToDisable.add(shortcutInfo.getId());
127           continue;
128         }
129 
130         // Note: The lookup key may have changed but we cannot refresh it because that would require
131         // changing the shortcut ID, which can only be accomplished with a remove and add; but
132         // pinned shortcuts cannot be added or removed.
133         DialerShortcut shortcut =
134             DialerShortcut.builder()
135                 .setContactId(cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID)))
136                 .setLookupKey(lookupKey)
137                 .setDisplayName(
138                     cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_PRIMARY)))
139                 .build();
140 
141         if (shortcut.needsUpdate(shortcutInfo)) {
142           LogUtil.i("PinnedShortcuts.refresh", "contact updated");
143           delta.shortcutsToUpdateById.put(shortcutInfo.getId(), shortcut);
144         }
145       }
146     }
147     applyDelta(delta);
148   }
149 
applyDelta(@onNull Delta delta)150   private void applyDelta(@NonNull Delta delta) {
151     ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
152     String shortcutDisabledMessage =
153         context.getResources().getString(R.string.dialer_shortcut_disabled_message);
154     if (!delta.shortcutIdsToDisable.isEmpty()) {
155       shortcutManager.disableShortcuts(delta.shortcutIdsToDisable, shortcutDisabledMessage);
156     }
157     if (!delta.shortcutsToUpdateById.isEmpty()) {
158       // Note: This call updates both pinned and dynamic shortcuts, but the delta should contain
159       // no dynamic shortcuts.
160       if (!shortcutManager.updateShortcuts(
161           shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToUpdateById))) {
162         LogUtil.i("PinnedShortcuts.applyDelta", "shortcutManager rate limited.");
163       }
164     }
165   }
166 }
167