blob: bfcc3df81832c89be2cdb3ed3faa727a2d288e6c [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dialer.shortcuts;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build.VERSION_CODES;
import android.provider.ContactsContract.Contacts;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v4.content.ContextCompat;
import android.util.ArrayMap;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Handles refreshing of dialer pinned shortcuts.
*
* <p>Pinned shortcuts are icons that the user has dragged to their home screen from the dialer
* application launcher shortcut menu, which is accessible by tapping and holding the dialer
* launcher icon from the app drawer or a home screen.
*
* <p>When refreshing pinned shortcuts, we check to make sure that pinned contact information is
* still up to date (e.g. photo and name). We also check to see if the contact has been deleted from
* the user's contacts, and if so, we disable the pinned shortcut.
*
*/
@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
final class PinnedShortcuts {
private static final String[] PROJECTION =
new String[] {
Contacts._ID, Contacts.DISPLAY_NAME_PRIMARY, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
};
private static class Delta {
final List<String> shortcutIdsToDisable = new ArrayList<>();
final Map<String, DialerShortcut> shortcutsToUpdateById = new ArrayMap<>();
}
private final Context context;
private final ShortcutInfoFactory shortcutInfoFactory;
PinnedShortcuts(@NonNull Context context) {
this.context = context;
this.shortcutInfoFactory = new ShortcutInfoFactory(context, new IconFactory(context));
}
/**
* Performs a "complete refresh" of pinned shortcuts. This is done by (synchronously) querying for
* all contacts which currently have pinned shortcuts. The query results are used to compute a
* delta which contains a list of shortcuts which need to be updated (e.g. because of name/photo
* changes) or disabled (if contacts were deleted). Note that pinned shortcuts cannot be deleted
* programmatically and must be deleted by the user.
*
* <p>If the delta is non-empty, it is applied by making appropriate calls to the {@link
* ShortcutManager} system service.
*
* <p>This is a slow blocking call which performs file I/O and should not be performed on the main
* thread.
*/
@WorkerThread
public void refresh() {
Assert.isWorkerThread();
LogUtil.enterBlock("PinnedShortcuts.refresh");
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
LogUtil.i("PinnedShortcuts.refresh", "no contact permissions");
return;
}
Delta delta = new Delta();
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
for (ShortcutInfo shortcutInfo : shortcutManager.getPinnedShortcuts()) {
if (shortcutInfo.isDeclaredInManifest()) {
// We never update/disable the manifest shortcut (the "create new contact" shortcut).
continue;
}
if (shortcutInfo.isDynamic()) {
// If the shortcut is both pinned and dynamic, let the logic which updates dynamic shortcuts
// handle the update. It would be problematic to try and apply the update here, because the
// setRank is nonsensical for pinned shortcuts and therefore could not be calculated.
continue;
}
String lookupKey = DialerShortcut.getLookupKeyFromShortcutInfo(shortcutInfo);
Uri lookupUri = DialerShortcut.getLookupUriFromShortcutInfo(shortcutInfo);
try (Cursor cursor =
context.getContentResolver().query(lookupUri, PROJECTION, null, null, null)) {
if (cursor == null || !cursor.moveToNext()) {
LogUtil.i("PinnedShortcuts.refresh", "contact disabled");
delta.shortcutIdsToDisable.add(shortcutInfo.getId());
continue;
}
// Note: The lookup key may have changed but we cannot refresh it because that would require
// changing the shortcut ID, which can only be accomplished with a remove and add; but
// pinned shortcuts cannot be added or removed.
DialerShortcut shortcut =
DialerShortcut.builder()
.setContactId(cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID)))
.setLookupKey(lookupKey)
.setDisplayName(
cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_PRIMARY)))
.build();
if (shortcut.needsUpdate(shortcutInfo)) {
LogUtil.i("PinnedShortcuts.refresh", "contact updated");
delta.shortcutsToUpdateById.put(shortcutInfo.getId(), shortcut);
}
}
}
applyDelta(delta);
}
private void applyDelta(@NonNull Delta delta) {
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
String shortcutDisabledMessage =
context.getResources().getString(R.string.dialer_shortcut_disabled_message);
if (!delta.shortcutIdsToDisable.isEmpty()) {
shortcutManager.disableShortcuts(delta.shortcutIdsToDisable, shortcutDisabledMessage);
}
if (!delta.shortcutsToUpdateById.isEmpty()) {
// Note: This call updates both pinned and dynamic shortcuts, but the delta should contain
// no dynamic shortcuts.
if (!shortcutManager.updateShortcuts(
shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToUpdateById))) {
LogUtil.i("PinnedShortcuts.applyDelta", "shortcutManager rate limited.");
}
}
}
}