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