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