summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/shortcuts
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/shortcuts')
-rw-r--r--java/com/android/dialer/shortcuts/AndroidManifest.xml50
-rw-r--r--java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java161
-rw-r--r--java/com/android/dialer/shortcuts/CallContactActivity.java133
-rw-r--r--java/com/android/dialer/shortcuts/DialerShortcut.java190
-rw-r--r--java/com/android/dialer/shortcuts/DynamicShortcuts.java243
-rw-r--r--java/com/android/dialer/shortcuts/IconFactory.java112
-rw-r--r--java/com/android/dialer/shortcuts/PeriodicJobService.java118
-rw-r--r--java/com/android/dialer/shortcuts/PinnedShortcuts.java159
-rw-r--r--java/com/android/dialer/shortcuts/RefreshShortcutsTask.java71
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutInfoFactory.java100
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutRefresher.java86
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutUsageReporter.java132
-rw-r--r--java/com/android/dialer/shortcuts/Shortcuts.java34
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java48
-rw-r--r--java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml39
-rw-r--r--java/com/android/dialer/shortcuts/res/values/colors.xml20
-rw-r--r--java/com/android/dialer/shortcuts/res/values/dimens.xml19
-rw-r--r--java/com/android/dialer/shortcuts/res/values/strings.xml37
-rw-r--r--java/com/android/dialer/shortcuts/res/values/themes.xml39
-rw-r--r--java/com/android/dialer/shortcuts/res/xml/shortcuts.xml31
20 files changed, 1822 insertions, 0 deletions
diff --git a/java/com/android/dialer/shortcuts/AndroidManifest.xml b/java/com/android/dialer/shortcuts/AndroidManifest.xml
new file mode 100644
index 000000000..e731a3e68
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/AndroidManifest.xml
@@ -0,0 +1,50 @@
+<!--
+ ~ 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
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.shortcuts">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <application>
+
+ <service
+ android:exported="false"
+ android:name=".PeriodicJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE"/>
+
+ <!--
+ Comments for attributes in CallContactActivity:
+ taskAffinity="" -> Open the dialog without opening the dialer app behind it
+ noHistory="true" -> Navigating away finishes activity
+ excludeFromRecents="true" -> Don't show in "recent apps" screen
+
+ We do not export this activity and do not declare an intent filter as a security precaution
+ so that apps other than the dialer cannot attempt to make phone calls using it.
+ -->
+ <activity
+ android:name=".CallContactActivity"
+ android:taskAffinity=""
+ android:noHistory="true"
+ android:excludeFromRecents="true"
+ android:label=""
+ android:exported="false"
+ android:theme="@style/CallContactsTheme"/>
+
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java b/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
new file mode 100644
index 000000000..ef995c816
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 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.support.annotation.NonNull;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_DialerShortcut extends DialerShortcut {
+
+ private final long contactId;
+ private final String lookupKey;
+ private final String displayName;
+ private final int rank;
+
+ private AutoValue_DialerShortcut(
+ long contactId,
+ String lookupKey,
+ String displayName,
+ int rank) {
+ this.contactId = contactId;
+ this.lookupKey = lookupKey;
+ this.displayName = displayName;
+ this.rank = rank;
+ }
+
+ @Override
+ long getContactId() {
+ return contactId;
+ }
+
+ @NonNull
+ @Override
+ String getLookupKey() {
+ return lookupKey;
+ }
+
+ @NonNull
+ @Override
+ String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ int getRank() {
+ return rank;
+ }
+
+ @Override
+ public String toString() {
+ return "DialerShortcut{"
+ + "contactId=" + contactId + ", "
+ + "lookupKey=" + lookupKey + ", "
+ + "displayName=" + displayName + ", "
+ + "rank=" + rank
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof DialerShortcut) {
+ DialerShortcut that = (DialerShortcut) o;
+ return (this.contactId == that.getContactId())
+ && (this.lookupKey.equals(that.getLookupKey()))
+ && (this.displayName.equals(that.getDisplayName()))
+ && (this.rank == that.getRank());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (this.contactId >>> 32) ^ this.contactId;
+ h *= 1000003;
+ h ^= this.lookupKey.hashCode();
+ h *= 1000003;
+ h ^= this.displayName.hashCode();
+ h *= 1000003;
+ h ^= this.rank;
+ return h;
+ }
+
+ static final class Builder extends DialerShortcut.Builder {
+ private Long contactId;
+ private String lookupKey;
+ private String displayName;
+ private Integer rank;
+ Builder() {
+ }
+ private Builder(DialerShortcut source) {
+ this.contactId = source.getContactId();
+ this.lookupKey = source.getLookupKey();
+ this.displayName = source.getDisplayName();
+ this.rank = source.getRank();
+ }
+ @Override
+ DialerShortcut.Builder setContactId(long contactId) {
+ this.contactId = contactId;
+ return this;
+ }
+ @Override
+ DialerShortcut.Builder setLookupKey(String lookupKey) {
+ this.lookupKey = lookupKey;
+ return this;
+ }
+ @Override
+ DialerShortcut.Builder setDisplayName(String displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+ @Override
+ DialerShortcut.Builder setRank(int rank) {
+ this.rank = rank;
+ return this;
+ }
+ @Override
+ DialerShortcut build() {
+ String missing = "";
+ if (this.contactId == null) {
+ missing += " contactId";
+ }
+ if (this.lookupKey == null) {
+ missing += " lookupKey";
+ }
+ if (this.displayName == null) {
+ missing += " displayName";
+ }
+ if (this.rank == null) {
+ missing += " rank";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_DialerShortcut(
+ this.contactId,
+ this.lookupKey,
+ this.displayName,
+ this.rank);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/java/com/android/dialer/shortcuts/CallContactActivity.java b/java/com/android/dialer/shortcuts/CallContactActivity.java
new file mode 100644
index 000000000..1e9a01b39
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/CallContactActivity.java
@@ -0,0 +1,133 @@
+/*
+ * 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.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.widget.Toast;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.interactions.PhoneNumberInteraction;
+import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode;
+import com.android.dialer.util.TransactionSafeActivity;
+
+/**
+ * Invisible activity launched when a shortcut is selected by user. Calls a contact based on URI.
+ */
+public class CallContactActivity extends TransactionSafeActivity
+ implements PhoneNumberInteraction.DisambigDialogDismissedListener,
+ PhoneNumberInteraction.InteractionErrorListener,
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String CONTACT_URI_KEY = "uri_key";
+
+ private Uri contactUri;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if ("com.android.dialer.shortcuts.CALL_CONTACT".equals(getIntent().getAction())) {
+ if (Shortcuts.areDynamicShortcutsEnabled(this)) {
+ LogUtil.i("CallContactActivity.onCreate", "shortcut clicked");
+ contactUri = getIntent().getData();
+ makeCall();
+ } else {
+ LogUtil.i("CallContactActivity.onCreate", "dynamic shortcuts disabled");
+ finish();
+ }
+ }
+ }
+
+ private void makeCall() {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = CallInitiationType.Type.LAUNCHER_SHORTCUT;
+ PhoneNumberInteraction.startInteractionForPhoneCall(
+ this, contactUri, false /* isVideoCall */, callSpecificAppData);
+ }
+
+ @Override
+ public void onDisambigDialogDismissed() {
+ finish();
+ }
+
+ @Override
+ public void interactionError(@InteractionErrorCode int interactionErrorCode) {
+ // Note: There is some subtlety to how contact lookup keys work that make it difficult to
+ // distinguish the case of the contact missing from the case of the a contact not having a
+ // number. For example, if a contact's phone number is deleted, subsequent lookups based on
+ // lookup key will actually return no results because the phone number was part of the
+ // lookup key. In this case, it would be inaccurate to say the contact can't be found though, so
+ // in all cases we just say the contact can't be found or the contact doesn't have a number.
+ switch (interactionErrorCode) {
+ case InteractionErrorCode.CONTACT_NOT_FOUND:
+ case InteractionErrorCode.CONTACT_HAS_NO_NUMBER:
+ Toast.makeText(
+ this,
+ R.string.dialer_shortcut_contact_not_found_or_has_no_number,
+ Toast.LENGTH_SHORT)
+ .show();
+ break;
+ case InteractionErrorCode.USER_LEAVING_ACTIVITY:
+ case InteractionErrorCode.OTHER_ERROR:
+ default:
+ // If the user is leaving the activity or the error code was "other" there's no useful
+ // information to display but we still need to finish this invisible activity.
+ break;
+ }
+ finish();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(CONTACT_URI_KEY, contactUri);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState == null) {
+ return;
+ }
+ contactUri = savedInstanceState.getParcelable(CONTACT_URI_KEY);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case PhoneNumberInteraction.REQUEST_READ_CONTACTS:
+ {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ makeCall();
+ } else {
+ Toast.makeText(this, R.string.dialer_shortcut_no_permissions, Toast.LENGTH_SHORT)
+ .show();
+ }
+ finish();
+ break;
+ }
+ default:
+ throw new IllegalStateException("Unsupported request code: " + requestCode);
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/DialerShortcut.java b/java/com/android/dialer/shortcuts/DialerShortcut.java
new file mode 100644
index 000000000..f2fb3301a
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/DialerShortcut.java
@@ -0,0 +1,190 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.pm.ShortcutInfo;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.NonNull;
+
+
+/**
+ * Convenience data structure.
+ *
+ * <p>This differs from {@link ShortcutInfo} in that it doesn't hold an icon or intent, and provides
+ * convenience methods for doing things like constructing labels.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+
+abstract class DialerShortcut {
+
+ /** Marker value indicates that shortcut has no setRank. Used by pinned shortcuts. */
+ static final int NO_RANK = -1;
+
+ /**
+ * Contact ID from contacts provider. Note that this a numeric row ID from the
+ * ContactsContract.Contacts._ID column.
+ */
+ abstract long getContactId();
+
+ /**
+ * Lookup key from contacts provider. An example lookup key is: "0r8-47392D". This is the value
+ * from ContactsContract.Contacts.LOOKUP_KEY.
+ */
+ @NonNull
+ abstract String getLookupKey();
+
+ /** Display name from contacts provider. */
+ @NonNull
+ abstract String getDisplayName();
+
+ /**
+ * Rank for dynamic shortcuts. This value should be positive or {@link #NO_RANK}.
+ *
+ * <p>For floating shortcuts (pinned shortcuts with no corresponding dynamic shortcut), setRank
+ * has no meaning and the setRank may be set to {@link #NO_RANK}.
+ */
+ abstract int getRank();
+
+ /** The short label for the shortcut. Used when pinning shortcuts, for example. */
+ @NonNull
+ String getShortLabel() {
+ // Be sure to update getDisplayNameFromShortcutInfo when updating this.
+ return getDisplayName();
+ }
+
+ /**
+ * The long label for the shortcut. Used for shortcuts displayed when pressing and holding the app
+ * launcher icon, for example.
+ */
+ @NonNull
+ String getLongLabel() {
+ return getDisplayName();
+ }
+
+ /** The display name for the provided shortcut. */
+ static String getDisplayNameFromShortcutInfo(ShortcutInfo shortcutInfo) {
+ return shortcutInfo.getShortLabel().toString();
+ }
+
+ /**
+ * The id used to identify launcher shortcuts. Used for updating/deleting shortcuts.
+ *
+ * <p>Lookup keys are used for shortcut IDs. See {@link #getLookupKey()}.
+ *
+ * <p>If you change this, you probably also need to change {@link #getLookupKeyFromShortcutInfo}.
+ */
+ @NonNull
+ String getShortcutId() {
+ return getLookupKey();
+ }
+
+ /**
+ * Returns the contact lookup key from the provided {@link ShortcutInfo}.
+ *
+ * <p>Lookup keys are used for shortcut IDs. See {@link #getLookupKey()}.
+ */
+ @NonNull
+ static String getLookupKeyFromShortcutInfo(@NonNull ShortcutInfo shortcutInfo) {
+ return shortcutInfo.getId(); // Lookup keys are used for shortcut IDs.
+ }
+
+ /**
+ * Returns the lookup URI from the provided {@link ShortcutInfo}.
+ *
+ * <p>Lookup URIs are constructed from lookup key and contact ID. Here is an example lookup URI
+ * where lookup key is "0r8-47392D" and contact ID is 8:
+ *
+ * <p>"content://com.android.contacts/contacts/lookup/0r8-47392D/8"
+ */
+ @NonNull
+ static Uri getLookupUriFromShortcutInfo(@NonNull ShortcutInfo shortcutInfo) {
+ long contactId =
+ shortcutInfo.getIntent().getLongExtra(ShortcutInfoFactory.EXTRA_CONTACT_ID, -1);
+ if (contactId == -1) {
+ throw new IllegalStateException("No contact ID found for shortcut: " + shortcutInfo.getId());
+ }
+ String lookupKey = getLookupKeyFromShortcutInfo(shortcutInfo);
+ return Contacts.getLookupUri(contactId, lookupKey);
+ }
+
+ /**
+ * Contacts provider URI which uses the contact lookup key.
+ *
+ * <p>Lookup URIs are constructed from lookup key and contact ID. Here is an example lookup URI
+ * where lookup key is "0r8-47392D" and contact ID is 8:
+ *
+ * <p>"content://com.android.contacts/contacts/lookup/0r8-47392D/8"
+ */
+ @NonNull
+ Uri getLookupUri() {
+ return Contacts.getLookupUri(getContactId(), getLookupKey());
+ }
+
+ /**
+ * Given an existing shortcut with the same shortcut ID, returns true if the existing shortcut
+ * needs to be updated, e.g. if the contact's name or rank has changed.
+ *
+ * <p>Does not detect photo updates.
+ */
+ boolean needsUpdate(@NonNull ShortcutInfo oldInfo) {
+ if (this.getRank() != NO_RANK && oldInfo.getRank() != this.getRank()) {
+ return true;
+ }
+ if (!oldInfo.getShortLabel().equals(this.getShortLabel())) {
+ return true;
+ }
+ if (!oldInfo.getLongLabel().equals(this.getLongLabel())) {
+ return true;
+ }
+ return false;
+ }
+
+ static Builder builder() {
+ return new AutoValue_DialerShortcut.Builder().setRank(NO_RANK);
+ }
+
+
+ abstract static class Builder {
+
+ /**
+ * Sets the contact ID. This should be a value from the contact provider's Contact._ID column.
+ */
+ abstract Builder setContactId(long value);
+
+ /**
+ * Sets the lookup key. This should be a contact lookup key as provided by the contact provider.
+ */
+ abstract Builder setLookupKey(@NonNull String value);
+
+ /** Sets the display name. This should be a value provided by the contact provider. */
+ abstract Builder setDisplayName(@NonNull String value);
+
+ /**
+ * Sets the rank for the shortcut, used for ordering dynamic shortcuts. This is required for
+ * dynamic shortcuts but unused for floating shortcuts because rank has no meaning for floating
+ * shortcuts. (Floating shortcuts are shortcuts which are pinned but have no corresponding
+ * dynamic shortcut.)
+ */
+ abstract Builder setRank(int value);
+
+ /** Builds the immutable {@link DialerShortcut} object from this builder. */
+ abstract DialerShortcut build();
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/DynamicShortcuts.java b/java/com/android/dialer/shortcuts/DynamicShortcuts.java
new file mode 100644
index 000000000..be9e088e1
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/DynamicShortcuts.java
@@ -0,0 +1,243 @@
+/*
+ * 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.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.content.ContextCompat;
+import android.util.ArrayMap;
+import com.android.contacts.common.list.ContactEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Handles refreshing of dialer dynamic shortcuts.
+ *
+ * <p>Dynamic shortcuts are the list of shortcuts which is accessible by tapping and holding the
+ * dialer launcher icon from the app drawer or a home screen.
+ *
+ * <p>Dynamic shortcuts are refreshed whenever the dialtacts activity detects changes to favorites
+ * tiles. This class compares the newly updated favorites tiles to the existing list of (previously
+ * published) dynamic shortcuts to compute a delta, which consists of lists of shortcuts which need
+ * to be updated, added, or deleted.
+ *
+ * <p>Dynamic shortcuts should mirror (in order) the contacts displayed in the "tiled favorites" tab
+ * of the dialer application. When selecting a dynamic shortcut, the behavior should be the same as
+ * if the user had tapped on the contact from the tiled favorites tab. Specifically, if the user has
+ * more than one phone number, a number picker should be displayed, and otherwise the contact should
+ * be called directly.
+ *
+ * <p>Note that an icon change by itself does not trigger a shortcut update, because it is not
+ * possible to detect an icon update and we don't want to constantly force update icons, because
+ * that is an expensive operation which requires storage I/O.
+ *
+ * <p>However, the job scheduler uses {@link #updateIcons()} to makes sure icons are forcefully
+ * updated periodically (about once a day).
+ *
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+final class DynamicShortcuts {
+
+ private static final int MAX_DYNAMIC_SHORTCUTS = 3;
+
+ private static class Delta {
+
+ final Map<String, DialerShortcut> shortcutsToUpdateById = new ArrayMap<>();
+ final List<String> shortcutIdsToRemove = new ArrayList<>();
+ final Map<String, DialerShortcut> shortcutsToAddById = new ArrayMap<>();
+ }
+
+ private final Context context;
+ private final ShortcutInfoFactory shortcutInfoFactory;
+
+ DynamicShortcuts(@NonNull Context context, IconFactory iconFactory) {
+ this.context = context;
+ this.shortcutInfoFactory = new ShortcutInfoFactory(context, iconFactory);
+ }
+
+ /**
+ * Performs a "complete refresh" of dynamic shortcuts. This is done by comparing the provided
+ * contact information with the existing dynamic shortcuts in order to compute a delta which
+ * contains shortcuts which should be added, updated, or removed.
+ *
+ * <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(List<ContactEntry> contacts) {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("DynamicShortcuts.refresh");
+
+ ShortcutManager shortcutManager = getShortcutManager(context);
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("DynamicShortcuts.refresh", "no contact permissions");
+ shortcutManager.removeAllDynamicShortcuts();
+ return;
+ }
+
+ // Fill the available shortcuts with dynamic shortcuts up to a maximum of 3 dynamic shortcuts.
+ int numDynamicShortcutsToCreate =
+ Math.min(
+ MAX_DYNAMIC_SHORTCUTS,
+ shortcutManager.getMaxShortcutCountPerActivity()
+ - shortcutManager.getManifestShortcuts().size());
+
+ Map<String, DialerShortcut> newDynamicShortcutsById =
+ new ArrayMap<>(numDynamicShortcutsToCreate);
+ int rank = 0;
+ for (ContactEntry entry : contacts) {
+ if (newDynamicShortcutsById.size() >= numDynamicShortcutsToCreate) {
+ break;
+ }
+
+ DialerShortcut shortcut =
+ DialerShortcut.builder()
+ .setContactId(entry.id)
+ .setLookupKey(entry.lookupKey)
+ .setDisplayName(entry.getPreferredDisplayName())
+ .setRank(rank++)
+ .build();
+ newDynamicShortcutsById.put(shortcut.getShortcutId(), shortcut);
+ }
+
+ List<ShortcutInfo> oldDynamicShortcuts = new ArrayList<>(shortcutManager.getDynamicShortcuts());
+ Delta delta = computeDelta(oldDynamicShortcuts, newDynamicShortcutsById);
+ applyDelta(delta);
+ }
+
+ /**
+ * Forces an update of all dynamic shortcut icons. This should only be done from job scheduler as
+ * updating icons requires storage I/O.
+ */
+ @WorkerThread
+ void updateIcons() {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("DynamicShortcuts.updateIcons");
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("DynamicShortcuts.updateIcons", "no contact permissions");
+ return;
+ }
+
+ ShortcutManager shortcutManager = getShortcutManager(context);
+
+ int maxDynamicShortcutsToCreate =
+ shortcutManager.getMaxShortcutCountPerActivity()
+ - shortcutManager.getManifestShortcuts().size();
+ int count = 0;
+
+ List<ShortcutInfo> newShortcuts = new ArrayList<>();
+ for (ShortcutInfo oldInfo : shortcutManager.getDynamicShortcuts()) {
+ newShortcuts.add(shortcutInfoFactory.withUpdatedIcon(oldInfo));
+ if (++count >= maxDynamicShortcutsToCreate) {
+ break;
+ }
+ }
+ LogUtil.i("DynamicShortcuts.updateIcons", "updating %d shortcut icons", newShortcuts.size());
+ shortcutManager.setDynamicShortcuts(newShortcuts);
+ }
+
+ @NonNull
+ private Delta computeDelta(
+ @NonNull List<ShortcutInfo> oldDynamicShortcuts,
+ @NonNull Map<String, DialerShortcut> newDynamicShortcutsById) {
+ Delta delta = new Delta();
+ if (oldDynamicShortcuts.isEmpty()) {
+ delta.shortcutsToAddById.putAll(newDynamicShortcutsById);
+ return delta;
+ }
+
+ for (ShortcutInfo oldInfo : oldDynamicShortcuts) {
+ // Check to see if the new shortcut list contains the existing shortcut.
+ DialerShortcut newShortcut = newDynamicShortcutsById.get(oldInfo.getId());
+ if (newShortcut != null) {
+ if (newShortcut.needsUpdate(oldInfo)) {
+ LogUtil.i("DynamicShortcuts.computeDelta", "contact updated");
+ delta.shortcutsToUpdateById.put(oldInfo.getId(), newShortcut);
+ } // else the shortcut hasn't changed, nothing to do to it
+ } else {
+ // The old shortcut is not in the new shortcut list, remove it.
+ LogUtil.i("DynamicShortcuts.computeDelta", "contact removed");
+ delta.shortcutIdsToRemove.add(oldInfo.getId());
+ }
+ }
+
+ // Add any new shortcuts that were not in the old shortcuts.
+ for (Entry<String, DialerShortcut> entry : newDynamicShortcutsById.entrySet()) {
+ String newId = entry.getKey();
+ DialerShortcut newShortcut = entry.getValue();
+ if (!containsShortcut(oldDynamicShortcuts, newId)) {
+ // The new shortcut was not found in the old shortcut list, so add it.
+ LogUtil.i("DynamicShortcuts.computeDelta", "contact added");
+ delta.shortcutsToAddById.put(newId, newShortcut);
+ }
+ }
+ return delta;
+ }
+
+ private void applyDelta(@NonNull Delta delta) {
+ ShortcutManager shortcutManager = getShortcutManager(context);
+ // Must perform remove before performing add to avoid adding more than supported by system.
+ if (!delta.shortcutIdsToRemove.isEmpty()) {
+ shortcutManager.removeDynamicShortcuts(delta.shortcutIdsToRemove);
+ }
+ if (!delta.shortcutsToUpdateById.isEmpty()) {
+ // Note: This may update pinned shortcuts as well. Pinned shortcuts which are also dynamic
+ // are not updated by the pinned shortcut logic. The reason that they are updated here
+ // instead of in the pinned shortcut logic is because setRank is required and only available
+ // here.
+ shortcutManager.updateShortcuts(
+ shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToUpdateById));
+ }
+ if (!delta.shortcutsToAddById.isEmpty()) {
+ shortcutManager.addDynamicShortcuts(
+ shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToAddById));
+ }
+ }
+
+ private boolean containsShortcut(
+ @NonNull List<ShortcutInfo> shortcutInfos, @NonNull String shortcutId) {
+ for (ShortcutInfo oldInfo : shortcutInfos) {
+ if (oldInfo.getId().equals(shortcutId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static ShortcutManager getShortcutManager(Context context) {
+ //noinspection WrongConstant
+ return (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/IconFactory.java b/java/com/android/dialer/shortcuts/IconFactory.java
new file mode 100644
index 000000000..a8c4ada4e
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/IconFactory.java
@@ -0,0 +1,112 @@
+/*
+ * 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.content.Context;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.DrawableConverter;
+import java.io.InputStream;
+
+/** Constructs the icons for dialer shortcuts. */
+class IconFactory {
+
+ private final Context context;
+
+ IconFactory(@NonNull Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Creates an icon for the provided {@link DialerShortcut}.
+ *
+ * <p>The icon is a circle which contains a photo of the contact associated with the shortcut, if
+ * available. If a photo is not available, a circular colored icon with a single letter is instead
+ * created, where the letter is the first letter of the contact's name. If the contact has no
+ * name, a default colored "anonymous" avatar is used.
+ *
+ * <p>These icons should match exactly the favorites tiles in the starred tab of the dialer
+ * application, except that they are circular instead of rectangular.
+ */
+ @WorkerThread
+ @NonNull
+ public Icon create(@NonNull DialerShortcut shortcut) {
+ Assert.isWorkerThread();
+
+ return create(shortcut.getLookupUri(), shortcut.getDisplayName(), shortcut.getLookupKey());
+ }
+
+ /** Same as {@link #create(DialerShortcut)}, but accepts a {@link ShortcutInfo}. */
+ @WorkerThread
+ @NonNull
+ public Icon create(@NonNull ShortcutInfo shortcutInfo) {
+ Assert.isWorkerThread();
+ return create(
+ DialerShortcut.getLookupUriFromShortcutInfo(shortcutInfo),
+ DialerShortcut.getDisplayNameFromShortcutInfo(shortcutInfo),
+ DialerShortcut.getLookupKeyFromShortcutInfo(shortcutInfo));
+ }
+
+ @WorkerThread
+ @NonNull
+ private Icon create(
+ @NonNull Uri lookupUri, @NonNull String displayName, @NonNull String lookupKey) {
+ Assert.isWorkerThread();
+
+ // In testing, there was no difference between high-res and thumbnail.
+ InputStream inputStream =
+ ContactsContract.Contacts.openContactPhotoInputStream(
+ context.getContentResolver(), lookupUri, false /* preferHighres */);
+
+ Drawable drawable;
+ if (inputStream == null) {
+ // No photo for contact; use a letter tile.
+ LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources());
+ letterTileDrawable.setCanonicalDialerLetterTileDetails(
+ displayName, lookupKey, LetterTileDrawable.SHAPE_CIRCLE, LetterTileDrawable.TYPE_DEFAULT);
+ drawable = letterTileDrawable;
+ } else {
+ // There's a photo, create a circular drawable from it.
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ drawable = createCircularDrawable(bitmap);
+ }
+ int iconSize =
+ context.getResources().getDimensionPixelSize(R.dimen.launcher_shortcut_icon_size);
+ return Icon.createWithBitmap(
+ DrawableConverter.drawableToBitmap(drawable, iconSize /* width */, iconSize /* height */));
+ }
+
+ @NonNull
+ private Drawable createCircularDrawable(@NonNull Bitmap bitmap) {
+ RoundedBitmapDrawable roundedBitmapDrawable =
+ RoundedBitmapDrawableFactory.create(context.getResources(), bitmap);
+ roundedBitmapDrawable.setCircular(true);
+ roundedBitmapDrawable.setAntiAlias(true);
+ return roundedBitmapDrawable;
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/PeriodicJobService.java b/java/com/android/dialer/shortcuts/PeriodicJobService.java
new file mode 100644
index 000000000..62c9e37a0
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/PeriodicJobService.java
@@ -0,0 +1,118 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.v4.os.UserManagerCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.constants.ScheduledJobIds;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * {@link JobService} which starts the periodic job to refresh dynamic and pinned shortcuts.
+ *
+ * <p>Only {@link #schedulePeriodicJob(Context)} should be used by callers.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+public final class PeriodicJobService extends JobService {
+
+ private static final long REFRESH_PERIOD_MILLIS = TimeUnit.HOURS.toMillis(24);
+
+ private RefreshShortcutsTask refreshShortcutsTask;
+
+ /**
+ * Schedules the periodic job to refresh shortcuts. If called repeatedly, the job will just be
+ * rescheduled.
+ *
+ * <p>The job will not be scheduled if the build version is not at least N MR1 or if the user is
+ * locked.
+ */
+ @MainThread
+ public static void schedulePeriodicJob(@NonNull Context context) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.schedulePeriodicJob");
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1 && UserManagerCompat.isUserUnlocked(context)) {
+ JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+ if (jobScheduler.getPendingJob(ScheduledJobIds.SHORTCUT_PERIODIC_JOB) != null) {
+ LogUtil.i("PeriodicJobService.schedulePeriodicJob", "job already scheduled.");
+ return;
+ }
+ JobInfo jobInfo =
+ new JobInfo.Builder(
+ ScheduledJobIds.SHORTCUT_PERIODIC_JOB,
+ new ComponentName(context, PeriodicJobService.class))
+ .setPeriodic(REFRESH_PERIOD_MILLIS)
+ .setPersisted(true)
+ .setRequiresCharging(true)
+ .setRequiresDeviceIdle(true)
+ .build();
+ jobScheduler.schedule(jobInfo);
+ }
+ }
+
+ /** Cancels the periodic job. */
+ @MainThread
+ public static void cancelJob(@NonNull Context context) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.cancelJob");
+
+ context.getSystemService(JobScheduler.class).cancel(ScheduledJobIds.SHORTCUT_PERIODIC_JOB);
+ }
+
+ @Override
+ @MainThread
+ public boolean onStartJob(@NonNull JobParameters params) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.onStartJob");
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ (refreshShortcutsTask = new RefreshShortcutsTask(this)).execute(params);
+ } else {
+ // It is possible for the job to have been scheduled on NMR1+ and then the system was
+ // downgraded to < NMR1. In this case, shortcuts are no longer supported so we cancel the job
+ // which creates them.
+ LogUtil.i("PeriodicJobService.onStartJob", "not running on NMR1, cancelling job");
+ cancelJob(this);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ @MainThread
+ public boolean onStopJob(@NonNull JobParameters params) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.onStopJob");
+
+ if (refreshShortcutsTask != null) {
+ refreshShortcutsTask.cancel(false /* mayInterruptIfRunning */);
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/PinnedShortcuts.java b/java/com/android/dialer/shortcuts/PinnedShortcuts.java
new file mode 100644
index 000000000..bfcc3df81
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/PinnedShortcuts.java
@@ -0,0 +1,159 @@
+/*
+ * 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.");
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/RefreshShortcutsTask.java b/java/com/android/dialer/shortcuts/RefreshShortcutsTask.java
new file mode 100644
index 000000000..086d1dc7a
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/RefreshShortcutsTask.java
@@ -0,0 +1,71 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** {@link AsyncTask} used by the periodic job service to refresh dynamic and pinned shortcuts. */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+final class RefreshShortcutsTask extends AsyncTask<JobParameters, Void, JobParameters> {
+
+ private final JobService jobService;
+
+ RefreshShortcutsTask(@NonNull JobService jobService) {
+ this.jobService = jobService;
+ }
+
+ /** @param params array with length 1, provided from PeriodicJobService */
+ @Override
+ @NonNull
+ @WorkerThread
+ protected JobParameters doInBackground(JobParameters... params) {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("RefreshShortcutsTask.doInBackground");
+
+ // Dynamic shortcuts are refreshed from the UI but icons can become stale, so update them
+ // periodically using the job service.
+ //
+ // The reason that icons can become is stale is that there is no last updated timestamp for
+ // pictures; there is only a last updated timestamp for the entire contact row, which changes
+ // frequently (for example, when they are called their "times_contacted" is incremented).
+ // Relying on such a spuriously updated timestamp would result in too frequent shortcut updates,
+ // so instead we just allow the icon to become stale in the case that the contact's photo is
+ // updated, and then rely on the job service to periodically force update it.
+ new DynamicShortcuts(jobService, new IconFactory(jobService)).updateIcons(); // Blocking
+ new PinnedShortcuts(jobService).refresh(); // Blocking
+
+ return params[0];
+ }
+
+ @Override
+ @MainThread
+ protected void onPostExecute(JobParameters params) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("RefreshShortcutsTask.onPostExecute");
+
+ jobService.jobFinished(params, false /* needsReschedule */);
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutInfoFactory.java b/java/com/android/dialer/shortcuts/ShortcutInfoFactory.java
new file mode 100644
index 000000000..cf780bbd7
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutInfoFactory.java
@@ -0,0 +1,100 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Creates {@link ShortcutInfo} objects (which are required by shortcut manager system service) from
+ * {@link DialerShortcut} objects (which are package-private convenience data structures).
+ *
+ * <p>The main work this factory does is create shortcut intents. It also delegates to the {@link
+ * IconFactory} to create icons.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+final class ShortcutInfoFactory {
+
+ /** Key for the contact ID extra (a long) stored as part of the shortcut intent. */
+ static final String EXTRA_CONTACT_ID = "contactId";
+
+ private final Context context;
+ private final IconFactory iconFactory;
+
+ ShortcutInfoFactory(@NonNull Context context, IconFactory iconFactory) {
+ this.context = context;
+ this.iconFactory = iconFactory;
+ }
+
+ /**
+ * Builds a list {@link ShortcutInfo} objects from the provided collection of {@link
+ * DialerShortcut} objects. This primarily means setting the intent and adding the icon, which
+ * {@link DialerShortcut} objects do not hold.
+ */
+ @WorkerThread
+ @NonNull
+ List<ShortcutInfo> buildShortcutInfos(@NonNull Map<String, DialerShortcut> shortcutsById) {
+ Assert.isWorkerThread();
+ List<ShortcutInfo> shortcuts = new ArrayList<>(shortcutsById.size());
+ for (DialerShortcut shortcut : shortcutsById.values()) {
+ Intent intent = new Intent();
+ intent.setClassName(context, "com.android.dialer.shortcuts.CallContactActivity");
+ intent.setData(shortcut.getLookupUri());
+ intent.setAction("com.android.dialer.shortcuts.CALL_CONTACT");
+ intent.putExtra(EXTRA_CONTACT_ID, shortcut.getContactId());
+
+ ShortcutInfo.Builder shortcutInfo =
+ new ShortcutInfo.Builder(context, shortcut.getShortcutId())
+ .setIntent(intent)
+ .setShortLabel(shortcut.getShortLabel())
+ .setLongLabel(shortcut.getLongLabel())
+ .setIcon(iconFactory.create(shortcut));
+
+ if (shortcut.getRank() != DialerShortcut.NO_RANK) {
+ shortcutInfo.setRank(shortcut.getRank());
+ }
+ shortcuts.add(shortcutInfo.build());
+ }
+ return shortcuts;
+ }
+
+ /**
+ * Creates a copy of the provided {@link ShortcutInfo} but with an updated icon fetched from
+ * contacts provider.
+ */
+ @WorkerThread
+ @NonNull
+ ShortcutInfo withUpdatedIcon(ShortcutInfo info) {
+ Assert.isWorkerThread();
+ return new ShortcutInfo.Builder(context, info.getId())
+ .setIntent(info.getIntent())
+ .setShortLabel(info.getShortLabel())
+ .setLongLabel(info.getLongLabel())
+ .setRank(info.getRank())
+ .setIcon(iconFactory.create(info))
+ .build();
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutRefresher.java b/java/com/android/dialer/shortcuts/ShortcutRefresher.java
new file mode 100644
index 000000000..f5ff64874
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutRefresher.java
@@ -0,0 +1,86 @@
+/*
+ * 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.content.Context;
+import android.os.Build;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.contacts.common.list.ContactEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.FallibleAsyncTask;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Refreshes launcher shortcuts from UI components using provided list of contacts. */
+public final class ShortcutRefresher {
+
+ private static final AsyncTaskExecutor EXECUTOR = AsyncTaskExecutors.createThreadPoolExecutor();
+
+ /** Asynchronously updates launcher shortcuts using the provided list of contacts. */
+ @MainThread
+ public static void refresh(@NonNull Context context, List<ContactEntry> contacts) {
+ Assert.isMainThread();
+ Assert.isNotNull(context);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
+ return;
+ }
+
+ if (!Shortcuts.areDynamicShortcutsEnabled(context)) {
+ return;
+ }
+
+ //noinspection unchecked
+ EXECUTOR.submit(Task.ID, new Task(context), new ArrayList<>(contacts));
+ }
+
+ private static final class Task extends FallibleAsyncTask<List<ContactEntry>, Void, Void> {
+ private static final String ID = "ShortcutRefresher.Task";
+
+ private final Context context;
+
+ Task(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * @param params array containing exactly one element, the list of contacts from favorites
+ * tiles, ordered in tile order.
+ */
+ @SafeVarargs
+ @Override
+ @NonNull
+ @WorkerThread
+ protected final Void doInBackgroundFallible(List<ContactEntry>... params) {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("ShortcutRefresher.Task.doInBackground");
+
+ // Only dynamic shortcuts are maintained from UI components. Pinned shortcuts are maintained
+ // by the job scheduler. This is because a pinned contact may not necessarily still be in the
+ // favorites tiles, so refreshing it would require an additional database query. We don't want
+ // to incur the cost of that extra database query every time the favorites tiles change.
+ new DynamicShortcuts(context, new IconFactory(context)).refresh(params[0]); // Blocking
+
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutUsageReporter.java b/java/com/android/dialer/shortcuts/ShortcutUsageReporter.java
new file mode 100644
index 000000000..50130fc49
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutUsageReporter.java
@@ -0,0 +1,132 @@
+/*
+ * 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.ShortcutManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Reports outgoing calls as shortcut usage.
+ *
+ * <p>Note that all outgoing calls are considered shortcut usage, no matter where they are initiated
+ * from (i.e. from anywhere in the dialer app, or even from other apps).
+ *
+ * <p>This allows launcher applications to provide users with shortcut suggestions, even if the user
+ * isn't already using shortcuts.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N_MR1
+public class ShortcutUsageReporter {
+
+ private static final AsyncTaskExecutor EXECUTOR = AsyncTaskExecutors.createThreadPoolExecutor();
+
+ /**
+ * Called when an outgoing call is added to the call list in order to report outgoing calls as
+ * shortcut usage. This should be called exactly once for each outgoing call.
+ *
+ * <p>Asynchronously queries the contacts database for the contact's lookup key which corresponds
+ * to the provided phone number, and uses that to report shortcut usage.
+ *
+ * @param context used to access ShortcutManager system service
+ * @param phoneNumber the phone number being called
+ */
+ @MainThread
+ public static void onOutgoingCallAdded(@NonNull Context context, @Nullable String phoneNumber) {
+ Assert.isMainThread();
+ Assert.isNotNull(context);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || TextUtils.isEmpty(phoneNumber)) {
+ return;
+ }
+
+ EXECUTOR.submit(Task.ID, new Task(context), phoneNumber);
+ }
+
+ private static final class Task extends AsyncTask<String, Void, Void> {
+ private static final String ID = "ShortcutUsageReporter.Task";
+
+ private final Context context;
+
+ public Task(Context context) {
+ this.context = context;
+ }
+
+ /** @param phoneNumbers array with exactly one non-empty phone number */
+ @Override
+ @WorkerThread
+ protected Void doInBackground(@NonNull String... phoneNumbers) {
+ Assert.isWorkerThread();
+
+ String lookupKey = queryForLookupKey(phoneNumbers[0]);
+ if (!TextUtils.isEmpty(lookupKey)) {
+ LogUtil.i("ShortcutUsageReporter.backgroundLogUsage", "%s", lookupKey);
+ ShortcutManager shortcutManager =
+ (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
+
+ // Note: There may not currently exist a shortcut with the provided key, but it is logged
+ // anyway, so that launcher applications at least have the information should the shortcut
+ // be created in the future.
+ shortcutManager.reportShortcutUsed(lookupKey);
+ }
+ return null;
+ }
+
+ @Nullable
+ @WorkerThread
+ private String queryForLookupKey(String phoneNumber) {
+ Assert.isWorkerThread();
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("ShortcutUsageReporter.queryForLookupKey", "No contact permissions");
+ return null;
+ }
+
+ Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(uri, new String[] {Contacts.LOOKUP_KEY}, null, null, null)) {
+
+ if (cursor == null || !cursor.moveToNext()) {
+ return null; // No contact for dialed number
+ }
+ // Arbitrarily use first result.
+ return cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/Shortcuts.java b/java/com/android/dialer/shortcuts/Shortcuts.java
new file mode 100644
index 000000000..b6a7fa82a
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/Shortcuts.java
@@ -0,0 +1,34 @@
+/*
+ * 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.content.Context;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.ConfigProviderBindings;
+
+/** Checks if dynamic shortcuts should be enabled. */
+public class Shortcuts {
+
+ /** Key for boolean config value which determines whether or not to enable dynamic shortcuts. */
+ private static final String DYNAMIC_SHORTCUTS_ENABLED = "dynamic_shortcuts_enabled";
+
+ static boolean areDynamicShortcutsEnabled(@NonNull Context context) {
+ return ConfigProviderBindings.get(context).getBoolean(DYNAMIC_SHORTCUTS_ENABLED, true);
+ }
+
+ private Shortcuts() {}
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java b/java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java
new file mode 100644
index 000000000..4cfc4361c
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java
@@ -0,0 +1,48 @@
+/*
+ * 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.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Schedules dialer shortcut jobs.
+ *
+ * <p>A {@link ConfigProvider} value controls whether the jobs which creates shortcuts should be
+ * scheduled or cancelled.
+ */
+public class ShortcutsJobScheduler {
+
+ @MainThread
+ public static void scheduleAllJobs(@NonNull Context context) {
+ LogUtil.enterBlock("ShortcutsJobScheduler.scheduleAllJobs");
+ Assert.isMainThread();
+
+ if (Shortcuts.areDynamicShortcutsEnabled(context)) {
+ LogUtil.i("ShortcutsJobScheduler.scheduleAllJobs", "enabling shortcuts");
+
+ PeriodicJobService.schedulePeriodicJob(context);
+ } else {
+ LogUtil.i("ShortcutsJobScheduler.scheduleAllJobs", "disabling shortcuts");
+
+ PeriodicJobService.cancelJob(context);
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml b/java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml
new file mode 100644
index 000000000..c06aec82f
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:bottom="2dp"
+ android:left="2dp"
+ android:right="2dp"
+ android:top="2dp">
+ <shape android:shape="oval">
+ <size
+ android:height="44dp"
+ android:width="44dp"/>
+ <solid android:color="@color/shortcut_add_contact_background_color"/>
+ </shape>
+ </item>
+
+ <item
+ android:bottom="12dp"
+ android:left="10dp"
+ android:right="14dp"
+ android:top="12dp">
+ <bitmap android:src="@drawable/quantum_ic_person_add_white_24"
+ android:tint="@color/shortcut_add_contact_foreground_color"/>
+ </item>
+</layer-list>
diff --git a/java/com/android/dialer/shortcuts/res/values/colors.xml b/java/com/android/dialer/shortcuts/res/values/colors.xml
new file mode 100644
index 000000000..e20309b56
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <color name="shortcut_add_contact_foreground_color">#2A56C6</color>
+ <color name="shortcut_add_contact_background_color">#f5f5f5</color>
+</resources>
diff --git a/java/com/android/dialer/shortcuts/res/values/dimens.xml b/java/com/android/dialer/shortcuts/res/values/dimens.xml
new file mode 100644
index 000000000..232125653
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <dimen name="launcher_shortcut_icon_size">48dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/shortcuts/res/values/strings.xml b/java/com/android/dialer/shortcuts/res/values/strings.xml
new file mode 100644
index 000000000..1e2c87f12
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <!-- Text to display in launcher shortcut for adding a new contact. Short version. [CHAR LIMIT=10] -->
+ <string name="dialer_shortcut_add_contact_short">New contact</string>
+
+ <!-- Text to display in launcher shortcut for adding a new contact. Long version. [CHAR LIMIT=25] -->
+ <string name="dialer_shortcut_add_contact_long">New contact</string>
+
+ <!-- Message to display when the user taps a pinned launcher shortcut (on a
+ homescreen) which has been disabled. A shortcut may be disabled if the
+ contact has been deleted or if it is invalid for some other reason. [CHAR LIMIT=70] -->
+ <string name="dialer_shortcut_disabled_message">Shortcut not working. Drag to remove.</string>
+
+ <!-- Error message to display when a tapping a shortcut fails because the specified contact can't
+ be found or doesn't have any phone numbers. [CHAR LIMIT=70] -->
+ <string name="dialer_shortcut_contact_not_found_or_has_no_number">Contact no longer available.</string>
+
+ <!-- Error message to display when a tapping a shortcut fails because contact permissions are
+ missing. [CHAR LIMIT=70] -->
+ <string name="dialer_shortcut_no_permissions">Cannot call without contact permissions.</string>
+
+</resources>
diff --git a/java/com/android/dialer/shortcuts/res/values/themes.xml b/java/com/android/dialer/shortcuts/res/values/themes.xml
new file mode 100644
index 000000000..085854d89
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/themes.xml
@@ -0,0 +1,39 @@
+<resources>
+ <!--
+ ~ 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
+ -->
+
+ <!-- CallContactsActivity is an invisible trampoline activity for launcher shortcuts to jump into
+ the calling activity. When the user taps a shortcut they will be taken to either the phone
+ number disambiguation dialog or directly into the incall UI via this activity, but this
+ activity itself should be completely transparent to the user.
+
+ Note that this must inherit from Theme.AppCompat. We inherit from Theme.AppCompat.Light so
+ that the colors of the disambiguation dialog match the colors when it is shown via the
+ favorites tiles tab. -->
+ <style name="CallContactsTheme" parent="Theme.AppCompat.Light">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:backgroundDimEnabled">false</item>
+ <item name="android:windowBackground">@null</item>
+ <item name="android:windowFrame">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowDisablePreview">true</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
new file mode 100644
index 000000000..5e8f58d1f
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+ <shortcut
+ android:enabled="true"
+ android:icon="@drawable/ic_shortcut_add_contact"
+ android:shortcutId="dialer-shortcut-add-contact"
+ android:shortcutLongLabel="@string/dialer_shortcut_add_contact_long"
+ android:shortcutShortLabel="@string/dialer_shortcut_add_contact_short">
+
+ <intent
+ android:action="android.intent.action.INSERT"
+ android:data="content://com.android.contacts/contacts"
+ android:targetPackage="com.google.android.contacts"
+ android:targetClass="com.android.contacts.activities.CompactContactEditorActivity"/>
+ </shortcut>
+</shortcuts>