Update dialer sources.
Test: Built package and system image.
This change clobbers the old source, and is an export
from an internal Google repository.
The internal repository was forked form Android in March,
and this change includes modifications since then, to
near the v8 release.
Since the fork, we've moved code from monolithic to independent modules. In addition,
we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make.
New dependencies have been added:
- Dagger
- Auto-Value
- Glide
- Libshortcutbadger
Going forward, development will still be in Google3, and the Gerrit release
will become an automated export, with the next drop happening in ~ two weeks.
Android.mk includes local modifications from ToT.
Abridged changelog:
Bug fixes
● Not able to mute, add a call when using Phone app in multiwindow mode
● Double tap on keypad triggering multiple key and tones
● Reported spam numbers not showing as spam in the call log
● Crash when user tries to block number while Phone app is not set as default
● Crash when user picks a number from search auto-complete list
Visual Voicemail (VVM) improvements
● Share Voicemail audio via standard exporting mechanisms that support file attachment
(email, MMS, etc.)
● Make phone number, email and web sites in VVM transcript clickable
● Set PIN before declining VVM Terms of Service {Carrier}
● Set client type for outbound visual voicemail SMS {Carrier}
New incoming call and incall UI on older devices
(Android M)
● Updated Phone app icon
● New incall UI (large buttons, button labels)
● New and animated Answer/Reject gestures
Accessibility
● Add custom answer/decline call buttons on answer screen for touch exploration
accessibility services
● Increase size of touch target
● Add verbal feedback when a Voicemail fails to load
● Fix pressing of Phone buttons while in a phone call using Switch Access
● Fix selecting and opening contacts in talkback mode
● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text
Other
● Backup & Restore for App Preferences
● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is
connected
● Rename “Dialpad” to “Keypad”
● Show "Private number" for restricted calls
● Delete unused items (vcard, add contact, call history) from Phone menu
Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
diff --git a/java/com/android/dialer/animation/AnimUtils.java b/java/com/android/dialer/animation/AnimUtils.java
new file mode 100644
index 0000000..9c9396e
--- /dev/null
+++ b/java/com/android/dialer/animation/AnimUtils.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2014 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.animation;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+import com.android.dialer.compat.PathInterpolatorCompat;
+
+public class AnimUtils {
+
+ public static final int DEFAULT_DURATION = -1;
+ public static final int NO_DELAY = 0;
+
+ public static final Interpolator EASE_IN = PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f);
+ public static final Interpolator EASE_OUT = PathInterpolatorCompat.create(0.4f, 0.0f, 1.0f, 1.0f);
+ public static final Interpolator EASE_OUT_EASE_IN =
+ PathInterpolatorCompat.create(0.4f, 0, 0.2f, 1);
+
+ public static void crossFadeViews(View fadeIn, View fadeOut, int duration) {
+ fadeIn(fadeIn, duration);
+ fadeOut(fadeOut, duration);
+ }
+
+ public static void fadeOut(View fadeOut, int duration) {
+ fadeOut(fadeOut, duration, null);
+ }
+
+ public static void fadeOut(final View fadeOut, int durationMs, final AnimationCallback callback) {
+ fadeOut.setAlpha(1);
+ final ViewPropertyAnimator animator = fadeOut.animate();
+ animator.cancel();
+ animator
+ .alpha(0)
+ .withLayer()
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ fadeOut.setVisibility(View.GONE);
+ if (callback != null) {
+ callback.onAnimationEnd();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ fadeOut.setVisibility(View.GONE);
+ fadeOut.setAlpha(0);
+ if (callback != null) {
+ callback.onAnimationCancel();
+ }
+ }
+ });
+ if (durationMs != DEFAULT_DURATION) {
+ animator.setDuration(durationMs);
+ }
+ animator.start();
+ }
+
+ public static void fadeIn(View fadeIn, int durationMs) {
+ fadeIn(fadeIn, durationMs, NO_DELAY, null);
+ }
+
+ public static void fadeIn(
+ final View fadeIn, int durationMs, int delay, final AnimationCallback callback) {
+ fadeIn.setAlpha(0);
+ final ViewPropertyAnimator animator = fadeIn.animate();
+ animator.cancel();
+
+ animator.setStartDelay(delay);
+ animator
+ .alpha(1)
+ .withLayer()
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ fadeIn.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ fadeIn.setAlpha(1);
+ if (callback != null) {
+ callback.onAnimationCancel();
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (callback != null) {
+ callback.onAnimationEnd();
+ }
+ }
+ });
+ if (durationMs != DEFAULT_DURATION) {
+ animator.setDuration(durationMs);
+ }
+ animator.start();
+ }
+
+ /**
+ * Scales in the view from scale of 0 to actual dimensions.
+ *
+ * @param view The view to scale.
+ * @param durationMs The duration of the scaling in milliseconds.
+ * @param startDelayMs The delay to applying the scaling in milliseconds.
+ */
+ public static void scaleIn(final View view, int durationMs, int startDelayMs) {
+ AnimatorListenerAdapter listener =
+ (new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ view.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ view.setScaleX(1);
+ view.setScaleY(1);
+ }
+ });
+ scaleInternal(
+ view,
+ 0 /* startScaleValue */,
+ 1 /* endScaleValue */,
+ durationMs,
+ startDelayMs,
+ listener,
+ EASE_IN);
+ }
+
+ /**
+ * Scales out the view from actual dimensions to 0.
+ *
+ * @param view The view to scale.
+ * @param durationMs The duration of the scaling in milliseconds.
+ */
+ public static void scaleOut(final View view, int durationMs) {
+ AnimatorListenerAdapter listener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ view.setVisibility(View.GONE);
+ view.setScaleX(0);
+ view.setScaleY(0);
+ }
+ };
+
+ scaleInternal(
+ view,
+ 1 /* startScaleValue */,
+ 0 /* endScaleValue */,
+ durationMs,
+ NO_DELAY,
+ listener,
+ EASE_OUT);
+ }
+
+ private static void scaleInternal(
+ final View view,
+ int startScaleValue,
+ int endScaleValue,
+ int durationMs,
+ int startDelay,
+ AnimatorListenerAdapter listener,
+ Interpolator interpolator) {
+ view.setScaleX(startScaleValue);
+ view.setScaleY(startScaleValue);
+
+ final ViewPropertyAnimator animator = view.animate();
+ animator.cancel();
+
+ animator
+ .setInterpolator(interpolator)
+ .scaleX(endScaleValue)
+ .scaleY(endScaleValue)
+ .setListener(listener)
+ .withLayer();
+
+ if (durationMs != DEFAULT_DURATION) {
+ animator.setDuration(durationMs);
+ }
+ animator.setStartDelay(startDelay);
+
+ animator.start();
+ }
+
+ /**
+ * Animates a view to the new specified dimensions.
+ *
+ * @param view The view to change the dimensions of.
+ * @param newWidth The new width of the view.
+ * @param newHeight The new height of the view.
+ */
+ public static void changeDimensions(final View view, final int newWidth, final int newHeight) {
+ ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+
+ final int oldWidth = view.getWidth();
+ final int oldHeight = view.getHeight();
+ final int deltaWidth = newWidth - oldWidth;
+ final int deltaHeight = newHeight - oldHeight;
+
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ Float value = (Float) animator.getAnimatedValue();
+
+ view.getLayoutParams().width = (int) (value * deltaWidth + oldWidth);
+ view.getLayoutParams().height = (int) (value * deltaHeight + oldHeight);
+ view.requestLayout();
+ }
+ });
+ animator.start();
+ }
+
+ public static class AnimationCallback {
+
+ public void onAnimationEnd() {}
+
+ public void onAnimationCancel() {}
+ }
+}
diff --git a/java/com/android/dialer/animation/AnimationListenerAdapter.java b/java/com/android/dialer/animation/AnimationListenerAdapter.java
new file mode 100644
index 0000000..3f847f2
--- /dev/null
+++ b/java/com/android/dialer/animation/AnimationListenerAdapter.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 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.animation;
+
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+
+/**
+ * Provides empty implementations of the methods in {@link AnimationListener} for convenience
+ * reasons.
+ */
+public class AnimationListenerAdapter implements AnimationListener {
+
+ /** {@inheritDoc} */
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ /** {@inheritDoc} */
+ @Override
+ public void onAnimationEnd(Animation animation) {}
+
+ /** {@inheritDoc} */
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+}
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
new file mode 100644
index 0000000..80f294a
--- /dev/null
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -0,0 +1,116 @@
+<!-- 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.app">
+
+ <uses-permission android:name="android.permission.CALL_PHONE"/>
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
+ <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+ <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+ <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+ <uses-permission android:name="android.permission.READ_PROFILE"/>
+ <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
+ <uses-permission android:name="android.permission.NFC"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+ <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+ <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
+ <uses-permission android:name="android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK"/>
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission android:name="android.permission.BROADCAST_STICKY"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+
+ <!-- This tells the activity manager to not delay any of our activity
+ start requests, even if they happen immediately after the user
+ presses home. -->
+ <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <application
+ android:backupAgent='com.android.dialer.backup.DialerBackupAgent'
+ android:fullBackupOnly="true"
+ android:restoreAnyVersion="true"
+ android:name="com.android.dialer.app.DialerApplication">
+
+ <activity
+ android:exported="false"
+ android:label="@string/manage_blocked_numbers_label"
+ android:name="com.android.dialer.app.filterednumber.BlockedNumbersSettingsActivity"
+ android:parentActivityName="com.android.dialer.app.settings.DialerSettingsActivity"
+ android:theme="@style/ManageBlockedNumbersStyle">
+ <intent-filter>
+ <action android:name="com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ <receiver android:name="com.android.dialer.app.calllog.CallLogReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.NEW_VOICEMAIL"/>
+ <data
+ android:host="com.android.voicemail"
+ android:mimeType="vnd.android.cursor.item/voicemail"
+ android:scheme="content"
+ />
+ </intent-filter>
+ <intent-filter android:priority="100">
+ <action android:name="android.intent.action.BOOT_COMPLETED"/>
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:directBootAware="true"
+ android:exported="false"
+ android:name="com.android.dialer.app.calllog.CallLogNotificationsService"
+ />
+
+ <receiver
+ android:directBootAware="true"
+ android:name="com.android.dialer.app.calllog.MissedCallNotificationReceiver">
+ <intent-filter>
+ <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"/>
+ </intent-filter>
+ </receiver>
+
+ <provider
+ android:authorities="com.android.dialer.files"
+ android:exported="false"
+ android:grantUriPermissions="true"
+ android:name="android.support.v4.content.FileProvider">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/file_paths"/>
+ </provider>
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/app/Bindings.java b/java/com/android/dialer/app/Bindings.java
new file mode 100644
index 0000000..2beb401
--- /dev/null
+++ b/java/com/android/dialer/app/Bindings.java
@@ -0,0 +1,77 @@
+/*
+ * 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.app;
+
+import android.content.Context;
+import com.android.dialer.app.bindings.DialerBindings;
+import com.android.dialer.app.bindings.DialerBindingsFactory;
+import com.android.dialer.app.bindings.DialerBindingsStub;
+import com.android.dialer.app.legacybindings.DialerLegacyBindings;
+import com.android.dialer.app.legacybindings.DialerLegacyBindingsFactory;
+import com.android.dialer.app.legacybindings.DialerLegacyBindingsStub;
+import java.util.Objects;
+
+/** Accessor for the in call UI bindings. */
+public class Bindings {
+
+ private static DialerBindings instance;
+ private static DialerLegacyBindings legacyInstance;
+
+ private Bindings() {}
+
+ public static DialerBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (instance != null) {
+ return instance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof DialerBindingsFactory) {
+ instance = ((DialerBindingsFactory) application).newDialerBindings();
+ }
+
+ if (instance == null) {
+ instance = new DialerBindingsStub();
+ }
+ return instance;
+ }
+
+ public static DialerLegacyBindings getLegacy(Context context) {
+ Objects.requireNonNull(context);
+ if (legacyInstance != null) {
+ return legacyInstance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof DialerLegacyBindingsFactory) {
+ legacyInstance = ((DialerLegacyBindingsFactory) application).newDialerLegacyBindings();
+ }
+
+ if (legacyInstance == null) {
+ legacyInstance = new DialerLegacyBindingsStub();
+ }
+ return legacyInstance;
+ }
+
+ public static void setForTesting(DialerBindings testInstance) {
+ instance = testInstance;
+ }
+
+ public static void setLegacyBindingForTesting(DialerLegacyBindings testLegacyInstance) {
+ legacyInstance = testLegacyInstance;
+ }
+}
diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java
new file mode 100644
index 0000000..cda2b2e
--- /dev/null
+++ b/java/com/android/dialer/app/CallDetailActivity.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2009 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.app;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.app.AppCompatActivity;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.contacts.common.ClipboardUtils;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.app.calllog.CallDetailHistoryAdapter;
+import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener;
+import com.android.dialer.app.calllog.CallTypeHelper;
+import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.proguard.UsedByReflection;
+import com.android.dialer.spam.Spam;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.TouchPointManager;
+
+/**
+ * Displays the details of a specific call log entry.
+ *
+ * <p>This activity can be either started with the URI of a single call log entry, or with the
+ * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries.
+ */
+@UsedByReflection(value = "AndroidManifest-app.xml")
+public class CallDetailActivity extends AppCompatActivity
+ implements MenuItem.OnMenuItemClickListener, View.OnClickListener {
+
+ /** A long array extra containing ids of call log entries to display. */
+ public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
+ /** If we are started with a voicemail, we'll find the uri to play with this extra. */
+ public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
+ /** If the activity was triggered from a notification. */
+ public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION";
+
+ public static final String BLOCKED_OR_SPAM_QUERY_IDENTIFIER = "blockedOrSpamIdentifier";
+
+ private final AsyncTaskExecutor executor = AsyncTaskExecutors.createAsyncTaskExecutor();
+ protected String mNumber;
+ private Context mContext;
+ private ContactInfoHelper mContactInfoHelper;
+ private ContactsPreferences mContactsPreferences;
+ private CallTypeHelper mCallTypeHelper;
+ private ContactPhotoManager mContactPhotoManager;
+ private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+ private LayoutInflater mInflater;
+ private Resources mResources;
+ private PhoneCallDetails mDetails;
+ private Uri mVoicemailUri;
+ private String mPostDialDigits = "";
+ private ListView mHistoryList;
+ private QuickContactBadge mQuickContactBadge;
+ private TextView mCallerName;
+ private TextView mCallerNumber;
+ private TextView mAccountLabel;
+ private View mCallButton;
+ private View mEditBeforeCallActionItem;
+ private View mReportActionItem;
+ private View mCopyNumberActionItem;
+ private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+ private CallLogAsyncTaskListener mCallLogAsyncTaskListener =
+ new CallLogAsyncTaskListener() {
+ @Override
+ public void onDeleteCall() {
+ finish();
+ }
+
+ @Override
+ public void onDeleteVoicemail() {
+ finish();
+ }
+
+ @Override
+ public void onGetCallDetails(final PhoneCallDetails[] details) {
+ if (details == null) {
+ // Somewhere went wrong: we're going to bail out and show error to users.
+ Toast.makeText(mContext, R.string.toast_call_detail_error, Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ // All calls are from the same number and same contact, so pick the first detail.
+ mDetails = details[0];
+ mNumber = TextUtils.isEmpty(mDetails.number) ? null : mDetails.number.toString();
+
+ if (mNumber == null) {
+ updateDataAndRender(details);
+ return;
+ }
+
+ executor.submit(
+ BLOCKED_OR_SPAM_QUERY_IDENTIFIER,
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ mDetails.isBlocked =
+ mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly(
+ mNumber, mDetails.countryIso)
+ != null;
+ if (Spam.get(mContext).isSpamEnabled()) {
+ mDetails.isSpam =
+ hasIncomingCalls(details)
+ && Spam.get(mContext)
+ .checkSpamStatusSynchronous(mNumber, mDetails.countryIso);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ updateDataAndRender(details);
+ }
+ });
+ }
+
+ private void updateDataAndRender(PhoneCallDetails[] details) {
+ mPostDialDigits =
+ TextUtils.isEmpty(mDetails.postDialDigits) ? "" : mDetails.postDialDigits;
+
+ final CharSequence callLocationOrType = getNumberTypeOrLocation(mDetails);
+
+ final CharSequence displayNumber;
+ if (!TextUtils.isEmpty(mDetails.postDialDigits)) {
+ displayNumber = mDetails.number + mDetails.postDialDigits;
+ } else {
+ displayNumber = mDetails.displayNumber;
+ }
+
+ final String displayNumberStr =
+ mBidiFormatter.unicodeWrap(displayNumber.toString(), TextDirectionHeuristics.LTR);
+
+ mDetails.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
+
+ if (!TextUtils.isEmpty(mDetails.getPreferredName())) {
+ mCallerName.setText(mDetails.getPreferredName());
+ mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
+ } else {
+ mCallerName.setText(displayNumberStr);
+ if (!TextUtils.isEmpty(callLocationOrType)) {
+ mCallerNumber.setText(callLocationOrType);
+ mCallerNumber.setVisibility(View.VISIBLE);
+ } else {
+ mCallerNumber.setVisibility(View.GONE);
+ }
+ }
+
+ CharSequence accountLabel =
+ PhoneAccountUtils.getAccountLabel(mContext, mDetails.accountHandle);
+ CharSequence accountContentDescription =
+ PhoneCallDetails.createAccountLabelDescription(
+ mResources, mDetails.viaNumber, accountLabel);
+ if (!TextUtils.isEmpty(mDetails.viaNumber)) {
+ if (!TextUtils.isEmpty(accountLabel)) {
+ accountLabel =
+ mResources.getString(
+ R.string.call_log_via_number_phone_account, accountLabel, mDetails.viaNumber);
+ } else {
+ accountLabel = mResources.getString(R.string.call_log_via_number, mDetails.viaNumber);
+ }
+ }
+ if (!TextUtils.isEmpty(accountLabel)) {
+ mAccountLabel.setText(accountLabel);
+ mAccountLabel.setContentDescription(accountContentDescription);
+ mAccountLabel.setVisibility(View.VISIBLE);
+ } else {
+ mAccountLabel.setVisibility(View.GONE);
+ }
+
+ final boolean canPlaceCallsTo =
+ PhoneNumberHelper.canPlaceCallsTo(mNumber, mDetails.numberPresentation);
+ mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
+ mCopyNumberActionItem.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
+
+ final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber);
+ final boolean isVoicemailNumber =
+ PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
+ final boolean showEditNumberBeforeCallAction =
+ canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
+ mEditBeforeCallActionItem.setVisibility(
+ showEditNumberBeforeCallAction ? View.VISIBLE : View.GONE);
+
+ final boolean showReportAction =
+ mContactInfoHelper.canReportAsInvalid(mDetails.sourceType, mDetails.objectId);
+ mReportActionItem.setVisibility(showReportAction ? View.VISIBLE : View.GONE);
+
+ invalidateOptionsMenu();
+
+ mHistoryList.setAdapter(
+ new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details));
+
+ updateContactPhoto(mDetails.isSpam);
+
+ findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Determines the location geocode text for a call, or the phone number type (if available).
+ *
+ * @param details The call details.
+ * @return The phone number type or location.
+ */
+ private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) {
+ if (details.isSpam) {
+ return mResources.getString(R.string.spam_number_call_log_label);
+ } else if (details.isBlocked) {
+ return mResources.getString(R.string.blocked_number_call_log_label);
+ } else if (!TextUtils.isEmpty(details.namePrimary)) {
+ return Phone.getTypeLabel(mResources, details.numberType, details.numberLabel);
+ } else {
+ return details.geocode;
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mContext = this;
+ mResources = getResources();
+ mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
+ mContactsPreferences = new ContactsPreferences(mContext);
+ mCallTypeHelper = new CallTypeHelper(getResources());
+ mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mContext);
+
+ mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setContentView(R.layout.call_detail);
+ mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
+
+ mHistoryList = (ListView) findViewById(R.id.history);
+ mHistoryList.addHeaderView(mInflater.inflate(R.layout.call_detail_header, null));
+ mHistoryList.addFooterView(mInflater.inflate(R.layout.call_detail_footer, null), null, false);
+
+ mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo);
+ mQuickContactBadge.setOverlay(null);
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+ }
+ mCallerName = (TextView) findViewById(R.id.caller_name);
+ mCallerNumber = (TextView) findViewById(R.id.caller_number);
+ mAccountLabel = (TextView) findViewById(R.id.phone_account_label);
+ mContactPhotoManager = ContactPhotoManager.getInstance(this);
+
+ mCallButton = findViewById(R.id.call_back_button);
+ mCallButton.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (TextUtils.isEmpty(mNumber)) {
+ return;
+ }
+ DialerUtils.startActivityWithErrorToast(
+ CallDetailActivity.this,
+ new CallIntentBuilder(getDialableNumber(), CallInitiationType.Type.CALL_DETAILS)
+ .build());
+ }
+ });
+
+ mEditBeforeCallActionItem = findViewById(R.id.call_detail_action_edit_before_call);
+ mEditBeforeCallActionItem.setOnClickListener(this);
+ mReportActionItem = findViewById(R.id.call_detail_action_report);
+ mReportActionItem.setOnClickListener(this);
+
+ mCopyNumberActionItem = findViewById(R.id.call_detail_action_copy);
+ mCopyNumberActionItem.setOnClickListener(this);
+
+ if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
+ closeSystemDialogs();
+ }
+
+ Logger.get(this).logScreenView(ScreenEvent.Type.CALL_DETAILS, this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ getCallDetails();
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ public void getCallDetails() {
+ CallLogAsyncTaskUtil.getCallDetails(this, mCallLogAsyncTaskListener, getCallLogEntryUris());
+ }
+
+ /**
+ * Returns the list of URIs to show.
+ *
+ * <p>There are two ways the URIs can be provided to the activity: as the data on the intent, or
+ * as a list of ids in the call log added as an extra on the URI.
+ *
+ * <p>If both are available, the data on the intent takes precedence.
+ */
+ private Uri[] getCallLogEntryUris() {
+ final Uri uri = getIntent().getData();
+ if (uri != null) {
+ // If there is a data on the intent, it takes precedence over the extra.
+ return new Uri[] {uri};
+ }
+ final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS);
+ final int numIds = ids == null ? 0 : ids.length;
+ final Uri[] uris = new Uri[numIds];
+ for (int index = 0; index < numIds; ++index) {
+ uris[index] =
+ ContentUris.withAppendedId(
+ TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]);
+ }
+ return uris;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ final MenuItem deleteMenuItem =
+ menu.add(
+ Menu.NONE, R.id.call_detail_delete_menu_item, Menu.NONE, R.string.call_details_delete);
+ deleteMenuItem.setIcon(R.drawable.ic_delete_24dp);
+ deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ deleteMenuItem.setOnMenuItemClickListener(this);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (item.getItemId() == R.id.call_detail_delete_menu_item) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM);
+ if (hasVoicemail()) {
+ CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener);
+ } else {
+ final StringBuilder callIds = new StringBuilder();
+ for (Uri callUri : getCallLogEntryUris()) {
+ if (callIds.length() != 0) {
+ callIds.append(",");
+ }
+ callIds.append(ContentUris.parseId(callUri));
+ }
+ CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onClick(View view) {
+ int resId = view.getId();
+ if (resId == R.id.call_detail_action_copy) {
+ ClipboardUtils.copyText(mContext, null, mNumber, true);
+ } else if (resId == R.id.call_detail_action_edit_before_call) {
+ Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(getDialableNumber()));
+ DialerUtils.startActivityWithErrorToast(mContext, dialIntent);
+ } else {
+ Assert.fail("Unexpected onClick event from " + view);
+ }
+ }
+
+ // Loads and displays the contact photo.
+ private void updateContactPhoto(boolean isSpam) {
+ if (mDetails == null) {
+ return;
+ }
+
+ mQuickContactBadge.assignContactUri(mDetails.contactUri);
+ final String displayName =
+ TextUtils.isEmpty(mDetails.namePrimary)
+ ? mDetails.displayNumber
+ : mDetails.namePrimary.toString();
+ mQuickContactBadge.setContentDescription(
+ mResources.getString(R.string.description_contact_details, displayName));
+
+ final boolean isVoicemailNumber =
+ PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
+ if (isSpam) {
+ mQuickContactBadge.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact));
+ return;
+ }
+
+ final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType);
+ int contactType = ContactPhotoManager.TYPE_DEFAULT;
+ if (isVoicemailNumber) {
+ contactType = ContactPhotoManager.TYPE_VOICEMAIL;
+ } else if (isBusiness) {
+ contactType = ContactPhotoManager.TYPE_BUSINESS;
+ }
+
+ final String lookupKey =
+ mDetails.contactUri == null ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri);
+
+ final DefaultImageRequest request =
+ new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */);
+
+ mContactPhotoManager.loadDirectoryPhoto(
+ mQuickContactBadge,
+ mDetails.photoUri,
+ false /* darkTheme */,
+ true /* isCircular */,
+ request);
+ }
+
+ private void closeSystemDialogs() {
+ sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ }
+
+ private String getDialableNumber() {
+ return mNumber + mPostDialDigits;
+ }
+
+ public boolean hasVoicemail() {
+ return mVoicemailUri != null;
+ }
+
+ private static boolean hasIncomingCalls(PhoneCallDetails[] details) {
+ for (int i = 0; i < details.length; i++) {
+ if (details[i].hasIncomingCalls()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/app/DialerApplication.java b/java/com/android/dialer/app/DialerApplication.java
new file mode 100644
index 0000000..3b97921
--- /dev/null
+++ b/java/com/android/dialer/app/DialerApplication.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 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.app;
+
+import android.app.Application;
+import android.os.Trace;
+import android.preference.PreferenceManager;
+import com.android.dialer.blocking.BlockedNumbersAutoMigrator;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.inject.ApplicationModule;
+import com.android.dialer.inject.DaggerDialerAppComponent;
+import com.android.dialer.inject.DialerAppComponent;
+
+public class DialerApplication extends Application implements EnrichedCallManager.Factory {
+
+ private static final String TAG = "DialerApplication";
+
+ private volatile DialerAppComponent component;
+
+ @Override
+ public void onCreate() {
+ Trace.beginSection(TAG + " onCreate");
+ super.onCreate();
+ new BlockedNumbersAutoMigrator(
+ this,
+ PreferenceManager.getDefaultSharedPreferences(this),
+ new FilteredNumberAsyncQueryHandler(this))
+ .autoMigrate();
+ Trace.endSection();
+ }
+
+ @Override
+ public EnrichedCallManager getEnrichedCallManager() {
+ return component().enrichedCallManager();
+ }
+
+ protected DialerAppComponent buildApplicationComponent() {
+ return DaggerDialerAppComponent.builder()
+ .applicationModule(new ApplicationModule(this))
+ .build();
+ }
+
+ /**
+ * Returns the application component.
+ *
+ * <p>A single Component is created per application instance. Note that it won't be instantiated
+ * until it's first requested, but guarantees that only one will ever be created.
+ */
+ private final DialerAppComponent component() {
+ // Double-check idiom for lazy initialization
+ DialerAppComponent result = component;
+ if (result == null) {
+ synchronized (this) {
+ result = component;
+ if (result == null) {
+ component = result = buildApplicationComponent();
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
new file mode 100644
index 0000000..4c57cda
--- /dev/null
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -0,0 +1,1484 @@
+/*
+ * Copyright (C) 2013 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.app;
+
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Trace;
+import android.provider.CallLog.Calls;
+import android.speech.RecognizerIntent;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.telecom.PhoneAccount;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnDragListener;
+import android.view.ViewTreeObserver;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.contacts.common.dialog.ClearFrequentsDialog;
+import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
+import com.android.contacts.common.list.PhoneNumberListAdapter;
+import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery;
+import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker;
+import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener;
+import com.android.contacts.common.widget.FloatingActionButtonController;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.animation.AnimationListenerAdapter;
+import com.android.dialer.app.calllog.CallLogFragment;
+import com.android.dialer.app.calllog.CallLogNotificationsService;
+import com.android.dialer.app.calllog.ClearCallLogDialog;
+import com.android.dialer.app.dialpad.DialpadFragment;
+import com.android.dialer.app.list.DragDropController;
+import com.android.dialer.app.list.ListsFragment;
+import com.android.dialer.app.list.OnDragDropListener;
+import com.android.dialer.app.list.OnListFragmentScrolledListener;
+import com.android.dialer.app.list.PhoneFavoriteSquareTileView;
+import com.android.dialer.app.list.RegularSearchFragment;
+import com.android.dialer.app.list.SearchFragment;
+import com.android.dialer.app.list.SmartDialSearchFragment;
+import com.android.dialer.app.list.SpeedDialFragment;
+import com.android.dialer.app.settings.DialerSettingsActivity;
+import com.android.dialer.app.widget.ActionBarController;
+import com.android.dialer.app.widget.SearchEditTextLayout;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.Database;
+import com.android.dialer.database.DialerDatabaseHelper;
+import com.android.dialer.interactions.PhoneNumberInteraction;
+import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.p13n.inference.P13nRanking;
+import com.android.dialer.p13n.inference.protocol.P13nRanker;
+import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener;
+import com.android.dialer.p13n.logging.P13nLogger;
+import com.android.dialer.p13n.logging.P13nLogging;
+import com.android.dialer.proguard.UsedByReflection;
+import com.android.dialer.smartdial.SmartDialNameMatcher;
+import com.android.dialer.smartdial.SmartDialPrefix;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.util.TouchPointManager;
+import com.android.dialer.util.TransactionSafeActivity;
+import com.android.dialer.util.ViewUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/** The dialer tab's title is 'phone', a more common name (see strings.xml). */
+@UsedByReflection(value = "AndroidManifest-app.xml")
+public class DialtactsActivity extends TransactionSafeActivity
+ implements View.OnClickListener,
+ DialpadFragment.OnDialpadQueryChangedListener,
+ OnListFragmentScrolledListener,
+ CallLogFragment.HostInterface,
+ DialpadFragment.HostInterface,
+ ListsFragment.HostInterface,
+ SpeedDialFragment.HostInterface,
+ SearchFragment.HostInterface,
+ OnDragDropListener,
+ OnPhoneNumberPickerActionListener,
+ PopupMenu.OnMenuItemClickListener,
+ ViewPager.OnPageChangeListener,
+ ActionBarController.ActivityUi,
+ PhoneNumberInteraction.InteractionErrorListener,
+ PhoneNumberInteraction.DisambigDialogDismissedListener,
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ public static final boolean DEBUG = false;
+ @VisibleForTesting public static final String TAG_DIALPAD_FRAGMENT = "dialpad";
+ private static final String ACTION_SHOW_TAB = "ACTION_SHOW_TAB";
+ @VisibleForTesting public static final String EXTRA_SHOW_TAB = "EXTRA_SHOW_TAB";
+ public static final String EXTRA_CLEAR_NEW_VOICEMAILS = "EXTRA_CLEAR_NEW_VOICEMAILS";
+ private static final String TAG = "DialtactsActivity";
+ private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
+ private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
+ private static final String KEY_SEARCH_QUERY = "search_query";
+ private static final String KEY_FIRST_LAUNCH = "first_launch";
+ private static final String KEY_WAS_CONFIGURATION_CHANGE = "was_configuration_change";
+ private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown";
+ private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search";
+ private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial";
+ private static final String TAG_FAVORITES_FRAGMENT = "favorites";
+ /** Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. */
+ private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
+
+ private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
+ public static final int ACTIVITY_REQUEST_CODE_CALL_COMPOSE = 2;
+
+ private static final int FAB_SCALE_IN_DELAY_MS = 300;
+ /** Fragment containing the dialpad that slides into view */
+ protected DialpadFragment mDialpadFragment;
+
+ private CoordinatorLayout mParentLayout;
+ /** Fragment for searching phone numbers using the alphanumeric keyboard. */
+ private RegularSearchFragment mRegularSearchFragment;
+
+ /** Fragment for searching phone numbers using the dialpad. */
+ private SmartDialSearchFragment mSmartDialSearchFragment;
+
+ /** Animation that slides in. */
+ private Animation mSlideIn;
+
+ /** Animation that slides out. */
+ private Animation mSlideOut;
+ /** Fragment containing the speed dial list, call history list, and all contacts list. */
+ private ListsFragment mListsFragment;
+ /**
+ * Tracks whether onSaveInstanceState has been called. If true, no fragment transactions can be
+ * commited.
+ */
+ private boolean mStateSaved;
+
+ private boolean mIsRestarting;
+ private boolean mInDialpadSearch;
+ private boolean mInRegularSearch;
+ private boolean mClearSearchOnPause;
+ private boolean mIsDialpadShown;
+ private boolean mShowDialpadOnResume;
+ /** Whether or not the device is in landscape orientation. */
+ private boolean mIsLandscape;
+ /** True if the dialpad is only temporarily showing due to being in call */
+ private boolean mInCallDialpadUp;
+ /** True when this activity has been launched for the first time. */
+ private boolean mFirstLaunch;
+ /**
+ * Search query to be applied to the SearchView in the ActionBar once onCreateOptionsMenu has been
+ * called.
+ */
+ private String mPendingSearchViewQuery;
+
+ private PopupMenu mOverflowMenu;
+ private EditText mSearchView;
+ private View mVoiceSearchButton;
+ private String mSearchQuery;
+ private String mDialpadQuery;
+ private DialerDatabaseHelper mDialerDatabaseHelper;
+ private DragDropController mDragDropController;
+ private ActionBarController mActionBarController;
+ private FloatingActionButtonController mFloatingActionButtonController;
+ private boolean mWasConfigurationChange;
+
+ private P13nLogger mP13nLogger;
+ private P13nRanker mP13nRanker;
+
+ AnimationListenerAdapter mSlideInListener =
+ new AnimationListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ maybeEnterSearchUi();
+ }
+ };
+ /** Listener for after slide out animation completes on dialer fragment. */
+ AnimationListenerAdapter mSlideOutListener =
+ new AnimationListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ commitDialpadFragmentHide();
+ }
+ };
+ /** Listener used to send search queries to the phone search fragment. */
+ private final TextWatcher mPhoneSearchQueryTextListener =
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ final String newText = s.toString();
+ if (newText.equals(mSearchQuery)) {
+ // If the query hasn't changed (perhaps due to activity being destroyed
+ // and restored, or user launching the same DIAL intent twice), then there is
+ // no need to do anything here.
+ return;
+ }
+ if (DEBUG) {
+ LogUtil.v("DialtactsActivity.onTextChanged", "called with new query: " + newText);
+ LogUtil.v("DialtactsActivity.onTextChanged", "previous query: " + mSearchQuery);
+ }
+ mSearchQuery = newText;
+
+ // Show search fragment only when the query string is changed to non-empty text.
+ if (!TextUtils.isEmpty(newText)) {
+ // Call enterSearchUi only if we are switching search modes, or showing a search
+ // fragment for the first time.
+ final boolean sameSearchMode =
+ (mIsDialpadShown && mInDialpadSearch) || (!mIsDialpadShown && mInRegularSearch);
+ if (!sameSearchMode) {
+ enterSearchUi(mIsDialpadShown, mSearchQuery, true /* animate */);
+ }
+ }
+
+ if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
+ mSmartDialSearchFragment.setQueryString(mSearchQuery);
+ } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
+ mRegularSearchFragment.setQueryString(mSearchQuery);
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ };
+ /** Open the search UI when the user clicks on the search box. */
+ private final View.OnClickListener mSearchViewOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isInSearchUi()) {
+ mActionBarController.onSearchBoxTapped();
+ enterSearchUi(
+ false /* smartDialSearch */, mSearchView.getText().toString(), true /* animate */);
+ }
+ }
+ };
+
+ private int mActionBarHeight;
+ private int mPreviouslySelectedTabIndex;
+ /** Handles the user closing the soft keyboard. */
+ private final View.OnKeyListener mSearchEditTextLayoutListener =
+ new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (TextUtils.isEmpty(mSearchView.getText().toString())) {
+ // If the search term is empty, close the search UI.
+ maybeExitSearchUi();
+ } else {
+ // If the search term is not empty, show the dialpad fab.
+ showFabInSearchUi();
+ }
+ }
+ return false;
+ }
+ };
+ /**
+ * The text returned from a voice search query. Set in {@link #onActivityResult} and used in
+ * {@link #onResume()} to populate the search box.
+ */
+ private String mVoiceSearchQuery;
+
+ /**
+ * @param tab the TAB_INDEX_* constant in {@link ListsFragment}
+ * @return A intent that will open the DialtactsActivity into the specified tab. The intent for
+ * each tab will be unique.
+ */
+ public static Intent getShowTabIntent(Context context, int tab) {
+ Intent intent = new Intent(context, DialtactsActivity.class);
+ intent.setAction(ACTION_SHOW_TAB);
+ intent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, tab);
+ intent.setData(
+ new Uri.Builder()
+ .scheme("intent")
+ .authority(context.getPackageName())
+ .appendPath(TAG)
+ .appendQueryParameter(DialtactsActivity.EXTRA_SHOW_TAB, String.valueOf(tab))
+ .build());
+
+ return intent;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Trace.beginSection(TAG + " onCreate");
+ super.onCreate(savedInstanceState);
+
+ mFirstLaunch = true;
+
+ final Resources resources = getResources();
+ mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large);
+
+ Trace.beginSection(TAG + " setContentView");
+ setContentView(R.layout.dialtacts_activity);
+ Trace.endSection();
+ getWindow().setBackgroundDrawable(null);
+
+ Trace.beginSection(TAG + " setup Views");
+ final ActionBar actionBar = getActionBarSafely();
+ actionBar.setCustomView(R.layout.search_edittext);
+ actionBar.setDisplayShowCustomEnabled(true);
+ actionBar.setBackgroundDrawable(null);
+
+ SearchEditTextLayout searchEditTextLayout =
+ (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container);
+ searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener);
+
+ mActionBarController = new ActionBarController(this, searchEditTextLayout);
+
+ mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view);
+ mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
+ mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button);
+ searchEditTextLayout
+ .findViewById(R.id.search_magnifying_glass)
+ .setOnClickListener(mSearchViewOnClickListener);
+ searchEditTextLayout
+ .findViewById(R.id.search_box_start_search)
+ .setOnClickListener(mSearchViewOnClickListener);
+ searchEditTextLayout.setOnClickListener(mSearchViewOnClickListener);
+ searchEditTextLayout.setCallback(
+ new SearchEditTextLayout.Callback() {
+ @Override
+ public void onBackButtonClicked() {
+ onBackPressed();
+ }
+
+ @Override
+ public void onSearchViewClicked() {
+ // Hide FAB, as the keyboard is shown.
+ mFloatingActionButtonController.scaleOut();
+ }
+ });
+
+ mIsLandscape =
+ getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ mPreviouslySelectedTabIndex = ListsFragment.TAB_INDEX_SPEED_DIAL;
+ final View floatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
+ ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
+ floatingActionButton.setOnClickListener(this);
+ mFloatingActionButtonController =
+ new FloatingActionButtonController(
+ this, floatingActionButtonContainer, floatingActionButton);
+
+ ImageButton optionsMenuButton =
+ (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button);
+ optionsMenuButton.setOnClickListener(this);
+ mOverflowMenu = buildOptionsMenu(optionsMenuButton);
+ optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener());
+
+ // Add the favorites fragment but only if savedInstanceState is null. Otherwise the
+ // fragment manager is responsible for recreating it.
+ if (savedInstanceState == null) {
+ getFragmentManager()
+ .beginTransaction()
+ .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT)
+ .commit();
+ } else {
+ mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
+ mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI);
+ mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI);
+ mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
+ mWasConfigurationChange = savedInstanceState.getBoolean(KEY_WAS_CONFIGURATION_CHANGE);
+ mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN);
+ mActionBarController.restoreInstanceState(savedInstanceState);
+ }
+
+ final boolean isLayoutRtl = ViewUtil.isRtl();
+ if (mIsLandscape) {
+ mSlideIn =
+ AnimationUtils.loadAnimation(
+ this, isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right);
+ mSlideOut =
+ AnimationUtils.loadAnimation(
+ this, isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right);
+ } else {
+ mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom);
+ mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom);
+ }
+
+ mSlideIn.setInterpolator(AnimUtils.EASE_IN);
+ mSlideOut.setInterpolator(AnimUtils.EASE_OUT);
+
+ mSlideIn.setAnimationListener(mSlideInListener);
+ mSlideOut.setAnimationListener(mSlideOutListener);
+
+ mParentLayout = (CoordinatorLayout) findViewById(R.id.dialtacts_mainlayout);
+ mParentLayout.setOnDragListener(new LayoutOnDragListener());
+ floatingActionButtonContainer
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ final ViewTreeObserver observer =
+ floatingActionButtonContainer.getViewTreeObserver();
+ if (!observer.isAlive()) {
+ return;
+ }
+ observer.removeOnGlobalLayoutListener(this);
+ int screenWidth = mParentLayout.getWidth();
+ mFloatingActionButtonController.setScreenWidth(screenWidth);
+ mFloatingActionButtonController.align(getFabAlignment(), false /* animate */);
+ }
+ });
+
+ Trace.endSection();
+
+ Trace.beginSection(TAG + " initialize smart dialing");
+ mDialerDatabaseHelper = Database.get(this).getDatabaseHelper(this);
+ SmartDialPrefix.initializeNanpSettings(this);
+ Trace.endSection();
+
+ mP13nLogger = P13nLogging.get(getApplicationContext());
+ mP13nRanker = P13nRanking.get(getApplicationContext());
+ Trace.endSection();
+ }
+
+ @NonNull
+ private ActionBar getActionBarSafely() {
+ return Assert.isNotNull(getSupportActionBar());
+ }
+
+ @Override
+ protected void onResume() {
+ Trace.beginSection(TAG + " onResume");
+ super.onResume();
+
+ mStateSaved = false;
+ if (mFirstLaunch) {
+ displayFragment(getIntent());
+ } else if (!phoneIsInUse() && mInCallDialpadUp) {
+ hideDialpadFragment(false, true);
+ mInCallDialpadUp = false;
+ } else if (mShowDialpadOnResume) {
+ showDialpadFragment(false);
+ mShowDialpadOnResume = false;
+ }
+
+ // If there was a voice query result returned in the {@link #onActivityResult} callback, it
+ // will have been stashed in mVoiceSearchQuery since the search results fragment cannot be
+ // shown until onResume has completed. Active the search UI and set the search term now.
+ if (!TextUtils.isEmpty(mVoiceSearchQuery)) {
+ mActionBarController.onSearchBoxTapped();
+ mSearchView.setText(mVoiceSearchQuery);
+ mVoiceSearchQuery = null;
+ }
+
+ mFirstLaunch = false;
+
+ if (mIsRestarting) {
+ // This is only called when the activity goes from resumed -> paused -> resumed, so it
+ // will not cause an extra view to be sent out on rotation
+ if (mIsDialpadShown) {
+ Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this);
+ }
+ mIsRestarting = false;
+ }
+
+ prepareVoiceSearchButton();
+ if (!mWasConfigurationChange) {
+ mDialerDatabaseHelper.startSmartDialUpdateThread();
+ }
+ mFloatingActionButtonController.align(getFabAlignment(), false /* animate */);
+
+ if (Calls.CONTENT_TYPE.equals(getIntent().getType())) {
+ // Externally specified extras take precedence to EXTRA_SHOW_TAB, which is only
+ // used internally.
+ final Bundle extras = getIntent().getExtras();
+ if (extras != null && extras.getInt(Calls.EXTRA_CALL_TYPE_FILTER) == Calls.VOICEMAIL_TYPE) {
+ mListsFragment.showTab(ListsFragment.TAB_INDEX_VOICEMAIL);
+ } else {
+ mListsFragment.showTab(ListsFragment.TAB_INDEX_HISTORY);
+ }
+ } else if (getIntent().hasExtra(EXTRA_SHOW_TAB)) {
+ int index = getIntent().getIntExtra(EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_SPEED_DIAL);
+ if (index < mListsFragment.getTabCount()) {
+ // Hide dialpad since this is an explicit intent to show a specific tab, which is coming
+ // from missed call or voicemail notification.
+ hideDialpadFragment(false, false);
+ exitSearchUi();
+ mListsFragment.showTab(index);
+ }
+ }
+
+ if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) {
+ CallLogNotificationsService.markNewVoicemailsAsOld(this);
+ }
+
+ setSearchBoxHint();
+
+ mP13nLogger.reset();
+ mP13nRanker.refresh(
+ new P13nRefreshCompleteListener() {
+ @Override
+ public void onP13nRefreshComplete() {
+ // TODO: make zero-query search results visible
+ }
+ });
+ Trace.endSection();
+ }
+
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+ mIsRestarting = true;
+ }
+
+ @Override
+ protected void onPause() {
+ if (mClearSearchOnPause) {
+ hideDialpadAndSearchUi();
+ mClearSearchOnPause = false;
+ }
+ if (mSlideOut.hasStarted() && !mSlideOut.hasEnded()) {
+ commitDialpadFragmentHide();
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
+ outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
+ outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
+ outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
+ outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown);
+ outState.putBoolean(KEY_WAS_CONFIGURATION_CHANGE, isChangingConfigurations());
+ mActionBarController.saveInstanceState(outState);
+ mStateSaved = true;
+ }
+
+ @Override
+ public void onAttachFragment(final Fragment fragment) {
+ if (fragment instanceof DialpadFragment) {
+ mDialpadFragment = (DialpadFragment) fragment;
+ if (!mIsDialpadShown && !mShowDialpadOnResume) {
+ final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ transaction.hide(mDialpadFragment);
+ transaction.commit();
+ }
+ } else if (fragment instanceof SmartDialSearchFragment) {
+ mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
+ mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this);
+ if (!TextUtils.isEmpty(mDialpadQuery)) {
+ mSmartDialSearchFragment.setAddToContactNumber(mDialpadQuery);
+ }
+ } else if (fragment instanceof SearchFragment) {
+ mRegularSearchFragment = (RegularSearchFragment) fragment;
+ mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this);
+ } else if (fragment instanceof ListsFragment) {
+ mListsFragment = (ListsFragment) fragment;
+ mListsFragment.addOnPageChangeListener(this);
+ }
+ if (fragment instanceof SearchFragment) {
+ final SearchFragment searchFragment = (SearchFragment) fragment;
+ searchFragment.setReranker(
+ new CursorReranker() {
+ @Override
+ @MainThread
+ public Cursor rerankCursor(Cursor data) {
+ Assert.isMainThread();
+ return mP13nRanker.rankCursor(data, PhoneQuery.PHONE_NUMBER);
+ }
+ });
+ searchFragment.addOnLoadFinishedListener(
+ new OnLoadFinishedListener() {
+ @Override
+ public void onLoadFinished() {
+ mP13nLogger.onSearchQuery(
+ searchFragment.getQueryString(),
+ (PhoneNumberListAdapter) searchFragment.getAdapter());
+ }
+ });
+ }
+ }
+
+ protected void handleMenuSettings() {
+ final Intent intent = new Intent(this, DialerSettingsActivity.class);
+ startActivity(intent);
+ }
+
+ @Override
+ public void onClick(View view) {
+ int resId = view.getId();
+ if (resId == R.id.floating_action_button) {
+ if (mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_ALL_CONTACTS
+ && !mInRegularSearch
+ && !mInDialpadSearch) {
+ DialerUtils.startActivityWithErrorToast(
+ this, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available);
+ Logger.get(this).logImpression(DialerImpression.Type.NEW_CONTACT_FAB);
+ } else if (!mIsDialpadShown) {
+ mInCallDialpadUp = false;
+ showDialpadFragment(true);
+ }
+ } else if (resId == R.id.voice_search_button) {
+ try {
+ startActivityForResult(
+ new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
+ ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(
+ DialtactsActivity.this, R.string.voice_search_not_available, Toast.LENGTH_SHORT)
+ .show();
+ }
+ } else if (resId == R.id.dialtacts_options_menu_button) {
+ mOverflowMenu.show();
+ } else {
+ Assert.fail("Unexpected onClick event from " + view);
+ }
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (!isSafeToCommitTransactions()) {
+ return true;
+ }
+
+ int resId = item.getItemId();
+ if (item.getItemId() == R.id.menu_delete_all) {
+ ClearCallLogDialog.show(getFragmentManager());
+ return true;
+ } else if (resId == R.id.menu_clear_frequents) {
+ ClearFrequentsDialog.show(getFragmentManager());
+ Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this);
+ return true;
+ } else if (resId == R.id.menu_call_settings) {
+ handleMenuSettings();
+ Logger.get(this).logScreenView(ScreenEvent.Type.SETTINGS, this);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
+ if (resultCode == RESULT_OK) {
+ final ArrayList<String> matches =
+ data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
+ if (matches.size() > 0) {
+ mVoiceSearchQuery = matches.get(0);
+ } else {
+ LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard");
+ }
+ } else {
+ LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed: " + resultCode);
+ }
+ } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) {
+ if (resultCode != RESULT_OK) {
+ LogUtil.i(
+ "DialtactsActivity.onActivityResult",
+ "returned from call composer, error occurred (resultCode=" + resultCode + ")");
+ String message =
+ getString(R.string.call_composer_connection_failed, getString(R.string.share_and_call));
+ Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show();
+ } else {
+ LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error");
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ /**
+ * Update the number of unread voicemails (potentially other tabs) displayed next to the tab icon.
+ */
+ public void updateTabUnreadCounts() {
+ mListsFragment.updateTabUnreadCounts();
+ }
+
+ /**
+ * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual
+ * updates are handled by a callback which is invoked after the dialpad fragment is shown.
+ *
+ * @see #onDialpadShown
+ */
+ private void showDialpadFragment(boolean animate) {
+ if (mIsDialpadShown || mStateSaved) {
+ return;
+ }
+ mIsDialpadShown = true;
+
+ mListsFragment.setUserVisibleHint(false);
+
+ final FragmentTransaction ft = getFragmentManager().beginTransaction();
+ if (mDialpadFragment == null) {
+ mDialpadFragment = new DialpadFragment();
+ ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT);
+ } else {
+ ft.show(mDialpadFragment);
+ }
+
+ mDialpadFragment.setAnimate(animate);
+ Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this);
+ ft.commit();
+
+ if (animate) {
+ mFloatingActionButtonController.scaleOut();
+ } else {
+ mFloatingActionButtonController.setVisible(false);
+ maybeEnterSearchUi();
+ }
+ mActionBarController.onDialpadUp();
+
+ Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer();
+
+ //adjust the title, so the user will know where we're at when the activity start/resumes.
+ setTitle(R.string.launcherDialpadActivityLabel);
+ }
+
+ /** Callback from child DialpadFragment when the dialpad is shown. */
+ public void onDialpadShown() {
+ Assert.isNotNull(mDialpadFragment);
+ if (mDialpadFragment.getAnimate()) {
+ Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn);
+ } else {
+ mDialpadFragment.setYFraction(0);
+ }
+
+ updateSearchFragmentPosition();
+ }
+
+ /**
+ * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in a
+ * callback after the hide animation ends.
+ *
+ * @see #commitDialpadFragmentHide
+ */
+ public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
+ if (mDialpadFragment == null || mDialpadFragment.getView() == null) {
+ return;
+ }
+ if (clearDialpad) {
+ // Temporarily disable accessibility when we clear the dialpad, since it should be
+ // invisible and should not announce anything.
+ mDialpadFragment
+ .getDigitsWidget()
+ .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mDialpadFragment.clearDialpad();
+ mDialpadFragment
+ .getDigitsWidget()
+ .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+ if (!mIsDialpadShown) {
+ return;
+ }
+ mIsDialpadShown = false;
+ mDialpadFragment.setAnimate(animate);
+ mListsFragment.setUserVisibleHint(true);
+ mListsFragment.sendScreenViewForCurrentPosition();
+
+ updateSearchFragmentPosition();
+
+ mFloatingActionButtonController.align(getFabAlignment(), animate);
+ if (animate) {
+ mDialpadFragment.getView().startAnimation(mSlideOut);
+ } else {
+ commitDialpadFragmentHide();
+ }
+
+ mActionBarController.onDialpadDown();
+
+ if (isInSearchUi()) {
+ if (TextUtils.isEmpty(mSearchQuery)) {
+ exitSearchUi();
+ }
+ }
+ //reset the title to normal.
+ setTitle(R.string.launcherActivityLabel);
+ }
+
+ /** Finishes hiding the dialpad fragment after any animations are completed. */
+ private void commitDialpadFragmentHide() {
+ if (!mStateSaved && mDialpadFragment != null && !mDialpadFragment.isHidden()) {
+ final FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.hide(mDialpadFragment);
+ ft.commit();
+ }
+ mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY);
+ }
+
+ private void updateSearchFragmentPosition() {
+ SearchFragment fragment = null;
+ if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
+ fragment = mSmartDialSearchFragment;
+ } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
+ fragment = mRegularSearchFragment;
+ }
+ if (fragment != null && fragment.isVisible()) {
+ fragment.updatePosition(true /* animate */);
+ }
+ }
+
+ @Override
+ public boolean isInSearchUi() {
+ return mInDialpadSearch || mInRegularSearch;
+ }
+
+ @Override
+ public boolean hasSearchQuery() {
+ return !TextUtils.isEmpty(mSearchQuery);
+ }
+
+ @Override
+ public boolean shouldShowActionBar() {
+ return mListsFragment.shouldShowActionBar();
+ }
+
+ private void setNotInSearchUi() {
+ mInDialpadSearch = false;
+ mInRegularSearch = false;
+ }
+
+ private void hideDialpadAndSearchUi() {
+ if (mIsDialpadShown) {
+ hideDialpadFragment(false, true);
+ } else {
+ exitSearchUi();
+ }
+ }
+
+ private void prepareVoiceSearchButton() {
+ final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ if (canIntentBeHandled(voiceIntent)) {
+ mVoiceSearchButton.setVisibility(View.VISIBLE);
+ mVoiceSearchButton.setOnClickListener(this);
+ } else {
+ mVoiceSearchButton.setVisibility(View.GONE);
+ }
+ }
+
+ public boolean isNearbyPlacesSearchEnabled() {
+ return false;
+ }
+
+ protected int getSearchBoxHint() {
+ return R.string.dialer_hint_find_contact;
+ }
+
+ /** Sets the hint text for the contacts search box */
+ private void setSearchBoxHint() {
+ SearchEditTextLayout searchEditTextLayout =
+ (SearchEditTextLayout)
+ getActionBarSafely().getCustomView().findViewById(R.id.search_view_container);
+ ((TextView) searchEditTextLayout.findViewById(R.id.search_box_start_search))
+ .setHint(getSearchBoxHint());
+ }
+
+ protected OptionsPopupMenu buildOptionsMenu(View invoker) {
+ final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker);
+ popupMenu.inflate(R.menu.dialtacts_options);
+ popupMenu.setOnMenuItemClickListener(this);
+ return popupMenu;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mPendingSearchViewQuery != null) {
+ mSearchView.setText(mPendingSearchViewQuery);
+ mPendingSearchViewQuery = null;
+ }
+ if (mActionBarController != null) {
+ mActionBarController.restoreActionBarOffset();
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the intent is due to hitting the green send key (hardware call button:
+ * KEYCODE_CALL) while in a call.
+ *
+ * @param intent the intent that launched this activity
+ * @return true if the intent is due to hitting the green send key while in a call
+ */
+ private boolean isSendKeyWhileInCall(Intent intent) {
+ // If there is a call in progress and the user launched the dialer by hitting the call
+ // button, go straight to the in-call screen.
+ final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction());
+
+ // When KEYCODE_CALL event is handled it dispatches an intent with the ACTION_CALL_BUTTON.
+ // Besides of checking the intent action, we must check if the phone is really during a
+ // call in order to decide whether to ignore the event or continue to display the activity.
+ if (callKey && phoneIsInUse()) {
+ TelecomUtil.showInCallScreen(this, false);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets the current tab based on the intent's request type
+ *
+ * @param intent Intent that contains information about which tab should be selected
+ */
+ private void displayFragment(Intent intent) {
+ // If we got here by hitting send and we're in call forward along to the in-call activity
+ if (isSendKeyWhileInCall(intent)) {
+ finish();
+ return;
+ }
+
+ final boolean showDialpadChooser =
+ !ACTION_SHOW_TAB.equals(intent.getAction())
+ && phoneIsInUse()
+ && !DialpadFragment.isAddCallMode(intent);
+ if (showDialpadChooser || (intent.getData() != null && isDialIntent(intent))) {
+ showDialpadFragment(false);
+ mDialpadFragment.setStartedFromNewIntent(true);
+ if (showDialpadChooser && !mDialpadFragment.isVisible()) {
+ mInCallDialpadUp = true;
+ }
+ }
+ }
+
+ @Override
+ public void onNewIntent(Intent newIntent) {
+ setIntent(newIntent);
+
+ mStateSaved = false;
+ displayFragment(newIntent);
+
+ invalidateOptionsMenu();
+ }
+
+ /** Returns true if the given intent contains a phone number to populate the dialer with */
+ private boolean isDialIntent(Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
+ return true;
+ }
+ if (Intent.ACTION_VIEW.equals(action)) {
+ final Uri data = intent.getData();
+ if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Shows the search fragment */
+ private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) {
+ if (mStateSaved || getFragmentManager().isDestroyed()) {
+ // Weird race condition where fragment is doing work after the activity is destroyed
+ // due to talkback being on (b/10209937). Just return since we can't do any
+ // constructive here.
+ return;
+ }
+
+ if (DEBUG) {
+ LogUtil.v("DialtactsActivity.enterSearchUi", "smart dial " + smartDialSearch);
+ }
+
+ final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ if (mInDialpadSearch && mSmartDialSearchFragment != null) {
+ transaction.remove(mSmartDialSearchFragment);
+ } else if (mInRegularSearch && mRegularSearchFragment != null) {
+ transaction.remove(mRegularSearchFragment);
+ }
+
+ final String tag;
+ if (smartDialSearch) {
+ tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
+ } else {
+ tag = TAG_REGULAR_SEARCH_FRAGMENT;
+ }
+ mInDialpadSearch = smartDialSearch;
+ mInRegularSearch = !smartDialSearch;
+
+ mFloatingActionButtonController.scaleOut();
+
+ SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
+ if (animate) {
+ transaction.setCustomAnimations(android.R.animator.fade_in, 0);
+ } else {
+ transaction.setTransition(FragmentTransaction.TRANSIT_NONE);
+ }
+ if (fragment == null) {
+ if (smartDialSearch) {
+ fragment = new SmartDialSearchFragment();
+ } else {
+ fragment = Bindings.getLegacy(this).newRegularSearchFragment();
+ fragment.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ // Show the FAB when the user touches the lists fragment and the soft
+ // keyboard is hidden.
+ hideDialpadFragment(true, false);
+ showFabInSearchUi();
+ v.performClick();
+ return false;
+ }
+ });
+ }
+ transaction.add(R.id.dialtacts_frame, fragment, tag);
+ } else {
+ transaction.show(fragment);
+ }
+ // DialtactsActivity will provide the options menu
+ fragment.setHasOptionsMenu(false);
+ fragment.setShowEmptyListForNullQuery(true);
+ if (!smartDialSearch) {
+ fragment.setQueryString(query);
+ }
+ transaction.commit();
+
+ if (animate) {
+ Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer();
+ }
+ mListsFragment.setUserVisibleHint(false);
+
+ if (smartDialSearch) {
+ Logger.get(this).logScreenView(ScreenEvent.Type.SMART_DIAL_SEARCH, this);
+ } else {
+ Logger.get(this).logScreenView(ScreenEvent.Type.REGULAR_SEARCH, this);
+ }
+ }
+
+ /** Hides the search fragment */
+ private void exitSearchUi() {
+ // See related bug in enterSearchUI();
+ if (getFragmentManager().isDestroyed() || mStateSaved) {
+ return;
+ }
+
+ mSearchView.setText(null);
+
+ if (mDialpadFragment != null) {
+ mDialpadFragment.clearDialpad();
+ }
+
+ setNotInSearchUi();
+
+ // Restore the FAB for the lists fragment.
+ if (getFabAlignment() != FloatingActionButtonController.ALIGN_END) {
+ mFloatingActionButtonController.setVisible(false);
+ }
+ mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS);
+ onPageScrolled(mListsFragment.getCurrentTabIndex(), 0 /* offset */, 0 /* pixelOffset */);
+ onPageSelected(mListsFragment.getCurrentTabIndex());
+
+ final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ if (mSmartDialSearchFragment != null) {
+ transaction.remove(mSmartDialSearchFragment);
+ }
+ if (mRegularSearchFragment != null) {
+ transaction.remove(mRegularSearchFragment);
+ }
+ transaction.commit();
+
+ Assert.isNotNull(mListsFragment.getView()).animate().alpha(1).withLayer();
+
+ if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
+ // If the dialpad fragment wasn't previously visible, then send a screen view because
+ // we are exiting regular search. Otherwise, the screen view will be sent by
+ // {@link #hideDialpadFragment}.
+ mListsFragment.sendScreenViewForCurrentPosition();
+ mListsFragment.setUserVisibleHint(true);
+ }
+
+ mActionBarController.onSearchUiExited();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mStateSaved) {
+ return;
+ }
+ if (mIsDialpadShown) {
+ if (TextUtils.isEmpty(mSearchQuery)
+ || (mSmartDialSearchFragment != null
+ && mSmartDialSearchFragment.isVisible()
+ && mSmartDialSearchFragment.getAdapter().getCount() == 0)) {
+ exitSearchUi();
+ }
+ hideDialpadFragment(true, false);
+ } else if (isInSearchUi()) {
+ exitSearchUi();
+ DialerUtils.hideInputMethod(mParentLayout);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private void maybeEnterSearchUi() {
+ if (!isInSearchUi()) {
+ enterSearchUi(true /* isSmartDial */, mSearchQuery, false);
+ }
+ }
+
+ /** @return True if the search UI was exited, false otherwise */
+ private boolean maybeExitSearchUi() {
+ if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) {
+ exitSearchUi();
+ DialerUtils.hideInputMethod(mParentLayout);
+ return true;
+ }
+ return false;
+ }
+
+ private void showFabInSearchUi() {
+ mFloatingActionButtonController.changeIcon(
+ getResources().getDrawable(R.drawable.fab_ic_dial, null),
+ getResources().getString(R.string.action_menu_dialpad_button));
+ mFloatingActionButtonController.align(getFabAlignment(), false /* animate */);
+ mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS);
+ }
+
+ @Override
+ public void onDialpadQueryChanged(String query) {
+ mDialpadQuery = query;
+ if (mSmartDialSearchFragment != null) {
+ mSmartDialSearchFragment.setAddToContactNumber(query);
+ }
+ final String normalizedQuery =
+ SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
+
+ if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
+ if (DEBUG) {
+ LogUtil.v("DialtactsActivity.onDialpadQueryChanged", "new query: " + query);
+ }
+ if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
+ // This callback can happen if the dialpad fragment is recreated because of
+ // activity destruction. In that case, don't update the search view because
+ // that would bring the user back to the search fragment regardless of the
+ // previous state of the application. Instead, just return here and let the
+ // fragment manager correctly figure out whatever fragment was last displayed.
+ if (!TextUtils.isEmpty(normalizedQuery)) {
+ mPendingSearchViewQuery = normalizedQuery;
+ }
+ return;
+ }
+ mSearchView.setText(normalizedQuery);
+ }
+
+ try {
+ if (mDialpadFragment != null && mDialpadFragment.isVisible()) {
+ mDialpadFragment.process_quote_emergency_unquote(normalizedQuery);
+ }
+ } catch (Exception ignored) {
+ // Skip any exceptions for this piece of code
+ }
+ }
+
+ @Override
+ public boolean onDialpadSpacerTouchWithEmptyQuery() {
+ if (mInDialpadSearch
+ && mSmartDialSearchFragment != null
+ && !mSmartDialSearchFragment.isShowingPermissionRequest()) {
+ hideDialpadFragment(true /* animate */, true /* clearDialpad */);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onListFragmentScrollStateChange(int scrollState) {
+ if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ hideDialpadFragment(true, false);
+ DialerUtils.hideInputMethod(mParentLayout);
+ }
+ }
+
+ @Override
+ public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ // TODO: No-op for now. This should eventually show/hide the actionBar based on
+ // interactions with the ListsFragments.
+ }
+
+ private boolean phoneIsInUse() {
+ return TelecomUtil.isInCall(this);
+ }
+
+ private boolean canIntentBeHandled(Intent intent) {
+ final PackageManager packageManager = getPackageManager();
+ final List<ResolveInfo> resolveInfo =
+ packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ return resolveInfo != null && resolveInfo.size() > 0;
+ }
+
+ /** Called when the user has long-pressed a contact tile to start a drag operation. */
+ @Override
+ public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
+ mListsFragment.showRemoveView(true);
+ }
+
+ @Override
+ public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {}
+
+ /** Called when the user has released a contact tile after long-pressing it. */
+ @Override
+ public void onDragFinished(int x, int y) {
+ mListsFragment.showRemoveView(false);
+ }
+
+ @Override
+ public void onDroppedOnRemove() {}
+
+ /**
+ * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer once it has
+ * been attached to the activity.
+ */
+ @Override
+ public void setDragDropController(DragDropController dragController) {
+ mDragDropController = dragController;
+ mListsFragment.getRemoveView().setDragDropController(dragController);
+ }
+
+ /** Implemented to satisfy {@link SpeedDialFragment.HostInterface} */
+ @Override
+ public void showAllContactsTab() {
+ if (mListsFragment != null) {
+ mListsFragment.showTab(ListsFragment.TAB_INDEX_ALL_CONTACTS);
+ }
+ }
+
+ /** Implemented to satisfy {@link CallLogFragment.HostInterface} */
+ @Override
+ public void showDialpad() {
+ showDialpadFragment(true);
+ }
+
+ @Override
+ public void enableFloatingButton(boolean enabled) {
+ LogUtil.d("DialtactsActivity.enableFloatingButton", "enable: %b", enabled);
+ // Floating button shouldn't be enabled when dialpad is shown.
+ if (!isDialpadShown() || !enabled) {
+ mFloatingActionButtonController.setVisible(enabled);
+ }
+ }
+
+ @Override
+ public void onPickDataUri(
+ Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData) {
+ mClearSearchOnPause = true;
+ PhoneNumberInteraction.startInteractionForPhoneCall(
+ DialtactsActivity.this, dataUri, isVideoCall, callSpecificAppData);
+ }
+
+ @Override
+ public void onPickPhoneNumber(
+ String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData) {
+ if (phoneNumber == null) {
+ // Invalid phone number, but let the call go through so that InCallUI can show
+ // an error message.
+ phoneNumber = "";
+ }
+
+ Intent intent =
+ new CallIntentBuilder(phoneNumber, callSpecificAppData).setIsVideoCall(isVideoCall).build();
+
+ DialerUtils.startActivityWithErrorToast(this, intent);
+ mClearSearchOnPause = true;
+ }
+
+ @Override
+ public void onHomeInActionBarSelected() {
+ exitSearchUi();
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ int tabIndex = mListsFragment.getCurrentTabIndex();
+
+ // Scroll the button from center to end when moving from the Speed Dial to Call History tab.
+ // In RTL, scroll when the current tab is Call History instead, since the order of the tabs
+ // is reversed and the ViewPager returns the left tab position during scroll.
+ boolean isRtl = ViewUtil.isRtl();
+ if (!isRtl && tabIndex == ListsFragment.TAB_INDEX_SPEED_DIAL && !mIsLandscape) {
+ mFloatingActionButtonController.onPageScrolled(positionOffset);
+ } else if (isRtl && tabIndex == ListsFragment.TAB_INDEX_HISTORY && !mIsLandscape) {
+ mFloatingActionButtonController.onPageScrolled(1 - positionOffset);
+ } else if (tabIndex != ListsFragment.TAB_INDEX_SPEED_DIAL) {
+ mFloatingActionButtonController.onPageScrolled(1);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ updateMissedCalls();
+ int tabIndex = mListsFragment.getCurrentTabIndex();
+ mPreviouslySelectedTabIndex = tabIndex;
+ mFloatingActionButtonController.setVisible(true);
+ if (tabIndex == ListsFragment.TAB_INDEX_ALL_CONTACTS
+ && !mInRegularSearch
+ && !mInDialpadSearch) {
+ mFloatingActionButtonController.changeIcon(
+ getResources().getDrawable(R.drawable.ic_person_add_24dp, null),
+ getResources().getString(R.string.search_shortcut_create_new_contact));
+ } else {
+ mFloatingActionButtonController.changeIcon(
+ getResources().getDrawable(R.drawable.fab_ic_dial, null),
+ getResources().getString(R.string.action_menu_dialpad_button));
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {}
+
+ @Override
+ public boolean isActionBarShowing() {
+ return mActionBarController.isActionBarShowing();
+ }
+
+ @Override
+ public ActionBarController getActionBarController() {
+ return mActionBarController;
+ }
+
+ @Override
+ public boolean isDialpadShown() {
+ return mIsDialpadShown;
+ }
+
+ @Override
+ public int getDialpadHeight() {
+ if (mDialpadFragment != null) {
+ return mDialpadFragment.getDialpadHeight();
+ }
+ return 0;
+ }
+
+ @Override
+ public int getActionBarHideOffset() {
+ return getActionBarSafely().getHideOffset();
+ }
+
+ @Override
+ public void setActionBarHideOffset(int offset) {
+ getActionBarSafely().setHideOffset(offset);
+ }
+
+ @Override
+ public int getActionBarHeight() {
+ return mActionBarHeight;
+ }
+
+ private int getFabAlignment() {
+ if (!mIsLandscape
+ && !isInSearchUi()
+ && mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_SPEED_DIAL) {
+ return FloatingActionButtonController.ALIGN_MIDDLE;
+ }
+ return FloatingActionButtonController.ALIGN_END;
+ }
+
+ private void updateMissedCalls() {
+ if (mPreviouslySelectedTabIndex == ListsFragment.TAB_INDEX_HISTORY) {
+ mListsFragment.markMissedCallsAsReadAndRemoveNotifications();
+ }
+ }
+
+ @Override
+ public void onDisambigDialogDismissed() {
+ // Don't do anything; the app will remain open with favorites tiles displayed.
+ }
+
+ @Override
+ public void interactionError(@InteractionErrorCode int interactionErrorCode) {
+ switch (interactionErrorCode) {
+ case InteractionErrorCode.USER_LEAVING_ACTIVITY:
+ // This is expected to happen if the user exits the activity before the interaction occurs.
+ return;
+ case InteractionErrorCode.CONTACT_NOT_FOUND:
+ case InteractionErrorCode.CONTACT_HAS_NO_NUMBER:
+ case InteractionErrorCode.OTHER_ERROR:
+ default:
+ // All other error codes are unexpected. For example, it should be impossible to start an
+ // interaction with an invalid contact from the Dialtacts activity.
+ Assert.fail("PhoneNumberInteraction error: " + interactionErrorCode);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ // This should never happen; it should be impossible to start an interaction without the
+ // contacts permission from the Dialtacts activity.
+ Assert.fail(
+ String.format(
+ Locale.US,
+ "Permissions requested unexpectedly: %d/%s/%s",
+ requestCode,
+ Arrays.toString(permissions),
+ Arrays.toString(grantResults)));
+ }
+
+ protected class OptionsPopupMenu extends PopupMenu {
+
+ public OptionsPopupMenu(Context context, View anchor) {
+ super(context, anchor, Gravity.END);
+ }
+
+ @Override
+ public void show() {
+ final boolean hasContactsPermission =
+ PermissionsUtil.hasContactsPermissions(DialtactsActivity.this);
+ final Menu menu = getMenu();
+ final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
+ clearFrequents.setVisible(
+ mListsFragment != null
+ && mListsFragment.getSpeedDialFragment() != null
+ && mListsFragment.getSpeedDialFragment().hasFrequents()
+ && hasContactsPermission);
+
+ menu.findItem(R.id.menu_delete_all)
+ .setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this));
+ super.show();
+ }
+ }
+
+ /**
+ * Listener that listens to drag events and sends their x and y coordinates to a {@link
+ * DragDropController}.
+ */
+ private class LayoutOnDragListener implements OnDragListener {
+
+ @Override
+ public boolean onDrag(View v, DragEvent event) {
+ if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
+ mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY());
+ }
+ return true;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/FloatingActionButtonBehavior.java b/java/com/android/dialer/app/FloatingActionButtonBehavior.java
new file mode 100644
index 0000000..d4a79ca
--- /dev/null
+++ b/java/com/android/dialer/app/FloatingActionButtonBehavior.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 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.app;
+
+import android.content.Context;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.design.widget.Snackbar.SnackbarLayout;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import com.android.dialer.proguard.UsedByReflection;
+
+/**
+ * Implements custom behavior for the movement of the FAB in response to the Snackbar. Because we
+ * are not using the design framework FloatingActionButton widget, we need to manually implement the
+ * Material Design behavior of having the FAB translate upward and downward with the appearance and
+ * disappearance of a Snackbar.
+ */
+@UsedByReflection(value = "dialtacts_activity.xml")
+public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FrameLayout> {
+
+ @UsedByReflection(value = "dialtacts_activity.xml")
+ public FloatingActionButtonBehavior(Context context, AttributeSet attrs) {}
+
+ @Override
+ public boolean layoutDependsOn(CoordinatorLayout parent, FrameLayout child, View dependency) {
+ return dependency instanceof SnackbarLayout;
+ }
+
+ @Override
+ public boolean onDependentViewChanged(
+ CoordinatorLayout parent, FrameLayout child, View dependency) {
+ float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
+ child.setTranslationY(translationY);
+ return true;
+ }
+}
diff --git a/java/com/android/dialer/app/PhoneCallDetails.java b/java/com/android/dialer/app/PhoneCallDetails.java
new file mode 100644
index 0000000..436f68e
--- /dev/null
+++ b/java/com/android/dialer/app/PhoneCallDetails.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2011 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.app;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.calllog.PhoneNumberDisplayUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+
+/** The details of a phone call to be shown in the UI. */
+public class PhoneCallDetails {
+
+ // The number of the other party involved in the call.
+ public CharSequence number;
+ // Post-dial digits associated with the outgoing call.
+ public String postDialDigits;
+ // The secondary line number the call was received via.
+ public String viaNumber;
+ // The number presenting rules set by the network, e.g., {@link Calls#PRESENTATION_ALLOWED}
+ public int numberPresentation;
+ // The country corresponding with the phone number.
+ public String countryIso;
+ // The geocoded location for the phone number.
+ public String geocode;
+
+ /**
+ * The type of calls, as defined in the call log table, e.g., {@link Calls#INCOMING_TYPE}.
+ *
+ * <p>There might be multiple types if this represents a set of entries grouped together.
+ */
+ public int[] callTypes;
+
+ // The date of the call, in milliseconds since the epoch.
+ public long date;
+ // The duration of the call in milliseconds, or 0 for missed calls.
+ public long duration;
+ // The name of the contact, or the empty string.
+ public CharSequence namePrimary;
+ // The alternative name of the contact, e.g. last name first, or the empty string
+ public CharSequence nameAlternative;
+ /**
+ * The user's preference on name display order, last name first or first time first. {@see
+ * ContactsPreferences}
+ */
+ public int nameDisplayOrder;
+ // The type of phone, e.g., {@link Phone#TYPE_HOME}, 0 if not available.
+ public int numberType;
+ // The custom label associated with the phone number in the contact, or the empty string.
+ public CharSequence numberLabel;
+ // The URI of the contact associated with this phone call.
+ public Uri contactUri;
+
+ /**
+ * The photo URI of the picture of the contact that is associated with this phone call or null if
+ * there is none.
+ *
+ * <p>This is meant to store the high-res photo only.
+ */
+ public Uri photoUri;
+
+ // The source type of the contact associated with this call.
+ public int sourceType;
+
+ // The object id type of the contact associated with this call.
+ public String objectId;
+
+ // The unique identifier for the account associated with the call.
+ public PhoneAccountHandle accountHandle;
+
+ // Features applicable to this call.
+ public int features;
+
+ // Total data usage for this call.
+ public Long dataUsage;
+
+ // Voicemail transcription
+ public String transcription;
+
+ // The display string for the number.
+ public String displayNumber;
+
+ // Whether the contact number is a voicemail number.
+ public boolean isVoicemail;
+
+ /** The {@link UserType} of the contact */
+ public @UserType long contactUserType;
+
+ /**
+ * If this is a voicemail, whether the message is read. For other types of calls, this defaults to
+ * {@code true}.
+ */
+ public boolean isRead = true;
+
+ // If this call is a spam number.
+ public boolean isSpam = false;
+
+ // If this call is a blocked number.
+ public boolean isBlocked = false;
+
+ // Call location and date text.
+ public CharSequence callLocationAndDate;
+
+ // Call description.
+ public CharSequence callDescription;
+ public String accountComponentName;
+ public String accountId;
+ public ContactInfo cachedContactInfo;
+ public int voicemailId;
+ public int previousGroup;
+
+ /**
+ * Constructor with required fields for the details of a call with a number associated with a
+ * contact.
+ */
+ public PhoneCallDetails(
+ CharSequence number, int numberPresentation, CharSequence postDialDigits) {
+ this.number = number;
+ this.numberPresentation = numberPresentation;
+ this.postDialDigits = postDialDigits.toString();
+ }
+ /**
+ * Construct the "on {accountLabel} via {viaNumber}" accessibility description for the account
+ * list item, depending on the existence of the accountLabel and viaNumber.
+ *
+ * @param viaNumber The number that this call is being placed via.
+ * @param accountLabel The {@link PhoneAccount} label that this call is being placed with.
+ * @return The description of the account that this call has been placed on.
+ */
+ public static CharSequence createAccountLabelDescription(
+ Resources resources, @Nullable String viaNumber, @Nullable CharSequence accountLabel) {
+
+ if ((!TextUtils.isEmpty(viaNumber)) && !TextUtils.isEmpty(accountLabel)) {
+ String msg =
+ resources.getString(
+ R.string.description_via_number_phone_account, accountLabel, viaNumber);
+ CharSequence accountNumberLabel =
+ ContactDisplayUtils.getTelephoneTtsSpannable(msg, viaNumber);
+ return (accountNumberLabel == null) ? msg : accountNumberLabel;
+ } else if (!TextUtils.isEmpty(viaNumber)) {
+ CharSequence viaNumberLabel =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ resources, R.string.description_via_number, viaNumber);
+ return (viaNumberLabel == null) ? viaNumber : viaNumberLabel;
+ } else if (!TextUtils.isEmpty(accountLabel)) {
+ return TextUtils.expandTemplate(
+ resources.getString(R.string.description_phone_account), accountLabel);
+ }
+ return "";
+ }
+
+ /**
+ * Returns the preferred name for the call details as specified by the {@link #nameDisplayOrder}
+ *
+ * @return the preferred name
+ */
+ public CharSequence getPreferredName() {
+ if (nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+ || TextUtils.isEmpty(nameAlternative)) {
+ return namePrimary;
+ }
+ return nameAlternative;
+ }
+
+ public void updateDisplayNumber(
+ Context context, CharSequence formattedNumber, boolean isVoicemail) {
+ displayNumber =
+ PhoneNumberDisplayUtil.getDisplayNumber(
+ context, number, numberPresentation, formattedNumber, postDialDigits, isVoicemail)
+ .toString();
+ }
+
+ public boolean hasIncomingCalls() {
+ for (int i = 0; i < callTypes.length; i++) {
+ if (callTypes[i] == CallLog.Calls.INCOMING_TYPE
+ || callTypes[i] == CallLog.Calls.MISSED_TYPE
+ || callTypes[i] == CallLog.Calls.VOICEMAIL_TYPE
+ || callTypes[i] == CallLog.Calls.REJECTED_TYPE
+ || callTypes[i] == CallLog.Calls.BLOCKED_TYPE) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/app/SpecialCharSequenceMgr.java b/java/com/android/dialer/app/SpecialCharSequenceMgr.java
new file mode 100644
index 0000000..2ae1970
--- /dev/null
+++ b/java/com/android/dialer/app/SpecialCharSequenceMgr.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2006 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.app;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.app.KeyguardManager;
+import android.app.ProgressDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Looper;
+import android.provider.Settings;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.Toast;
+import com.android.common.io.MoreCloseables;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
+import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.telecom.TelecomUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to listen for some magic character sequences that are handled specially by the
+ * dialer.
+ *
+ * <p>Note the Phone app also handles these sequences too (in a couple of relatively obscure places
+ * in the UI), so there's a separate version of this class under apps/Phone.
+ *
+ * <p>TODO: there's lots of duplicated code between this class and the corresponding class under
+ * apps/Phone. Let's figure out a way to unify these two classes (in the framework? in a common
+ * shared library?)
+ */
+public class SpecialCharSequenceMgr {
+
+ private static final String TAG = "SpecialCharSequenceMgr";
+
+ private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment";
+
+ private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE";
+ private static final String MMI_IMEI_DISPLAY = "*#06#";
+ private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#";
+ /** ***** This code is used to handle SIM Contact queries ***** */
+ private static final String ADN_PHONE_NUMBER_COLUMN_NAME = "number";
+
+ private static final String ADN_NAME_COLUMN_NAME = "name";
+ private static final int ADN_QUERY_TOKEN = -1;
+ /**
+ * Remembers the previous {@link QueryHandler} and cancel the operation when needed, to prevent
+ * possible crash.
+ *
+ * <p>QueryHandler may call {@link ProgressDialog#dismiss()} when the screen is already gone,
+ * which will cause the app crash. This variable enables the class to prevent the crash on {@link
+ * #cleanup()}.
+ *
+ * <p>TODO: Remove this and replace it (and {@link #cleanup()}) with better implementation. One
+ * complication is that we have SpecialCharSequenceMgr in Phone package too, which has *slightly*
+ * different implementation. Note that Phone package doesn't have this problem, so the class on
+ * Phone side doesn't have this functionality. Fundamental fix would be to have one shared
+ * implementation and resolve this corner case more gracefully.
+ */
+ private static QueryHandler sPreviousAdnQueryHandler;
+
+ /** This class is never instantiated. */
+ private SpecialCharSequenceMgr() {}
+
+ public static boolean handleChars(Context context, String input, EditText textField) {
+ //get rid of the separators so that the string gets parsed correctly
+ String dialString = PhoneNumberUtils.stripSeparators(input);
+
+ return handleDeviceIdDisplay(context, dialString)
+ || handleRegulatoryInfoDisplay(context, dialString)
+ || handlePinEntry(context, dialString)
+ || handleAdnEntry(context, dialString, textField)
+ || handleSecretCode(context, dialString);
+
+ }
+
+ /**
+ * Cleanup everything around this class. Must be run inside the main thread.
+ *
+ * <p>This should be called when the screen becomes background.
+ */
+ public static void cleanup() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ Log.wtf(TAG, "cleanup() is called outside the main thread");
+ return;
+ }
+
+ if (sPreviousAdnQueryHandler != null) {
+ sPreviousAdnQueryHandler.cancel();
+ sPreviousAdnQueryHandler = null;
+ }
+ }
+
+ /**
+ * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*. If a secret
+ * code is encountered an Intent is started with the android_secret_code://<code> URI.
+ *
+ * @param context the context to use
+ * @param input the text to check for a secret code in
+ * @return true if a secret code was encountered
+ */
+ static boolean handleSecretCode(Context context, String input) {
+ // Secret codes are in the form *#*#<code>#*#*
+ int len = input.length();
+ if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
+ final Intent intent =
+ new Intent(
+ SECRET_CODE_ACTION,
+ Uri.parse("android_secret_code://" + input.substring(4, len - 4)));
+ context.sendBroadcast(intent);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle ADN requests by filling in the SIM contact number into the requested EditText.
+ *
+ * <p>This code works alongside the Asynchronous query handler {@link QueryHandler} and query
+ * cancel handler implemented in {@link SimContactQueryCookie}.
+ */
+ static boolean handleAdnEntry(Context context, String input, EditText textField) {
+ /* ADN entries are of the form "N(N)(N)#" */
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (telephonyManager == null
+ || telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) {
+ return false;
+ }
+
+ // if the phone is keyguard-restricted, then just ignore this
+ // input. We want to make sure that sim card contacts are NOT
+ // exposed unless the phone is unlocked, and this code can be
+ // accessed from the emergency dialer.
+ KeyguardManager keyguardManager =
+ (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
+ if (keyguardManager.inKeyguardRestrictedInputMode()) {
+ return false;
+ }
+
+ int len = input.length();
+ if ((len > 1) && (len < 5) && (input.endsWith("#"))) {
+ try {
+ // get the ordinal number of the sim contact
+ final int index = Integer.parseInt(input.substring(0, len - 1));
+
+ // The original code that navigated to a SIM Contacts list view did not
+ // highlight the requested contact correctly, a requirement for PTCRB
+ // certification. This behaviour is consistent with the UI paradigm
+ // for touch-enabled lists, so it does not make sense to try to work
+ // around it. Instead we fill in the the requested phone number into
+ // the dialer text field.
+
+ // create the async query handler
+ final QueryHandler handler = new QueryHandler(context.getContentResolver());
+
+ // create the cookie object
+ final SimContactQueryCookie sc =
+ new SimContactQueryCookie(index - 1, handler, ADN_QUERY_TOKEN);
+
+ // setup the cookie fields
+ sc.contactNum = index - 1;
+ sc.setTextField(textField);
+
+ // create the progress dialog
+ sc.progressDialog = new ProgressDialog(context);
+ sc.progressDialog.setTitle(R.string.simContacts_title);
+ sc.progressDialog.setMessage(context.getText(R.string.simContacts_emptyLoading));
+ sc.progressDialog.setIndeterminate(true);
+ sc.progressDialog.setCancelable(true);
+ sc.progressDialog.setOnCancelListener(sc);
+ sc.progressDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ List<PhoneAccountHandle> subscriptionAccountHandles =
+ PhoneAccountUtils.getSubscriptionPhoneAccounts(context);
+ Context applicationContext = context.getApplicationContext();
+ boolean hasUserSelectedDefault =
+ subscriptionAccountHandles.contains(
+ TelecomUtil.getDefaultOutgoingPhoneAccount(
+ applicationContext, PhoneAccount.SCHEME_TEL));
+
+ if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) {
+ Uri uri = TelecomUtil.getAdnUriForPhoneAccount(applicationContext, null);
+ handleAdnQuery(handler, sc, uri);
+ } else {
+ SelectPhoneAccountListener callback =
+ new HandleAdnEntryAccountSelectedCallback(applicationContext, handler, sc);
+
+ DialogFragment dialogFragment =
+ SelectPhoneAccountDialogFragment.newInstance(
+ subscriptionAccountHandles, callback, null);
+ dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT);
+ }
+
+ return true;
+ } catch (NumberFormatException ex) {
+ // Ignore
+ }
+ }
+ return false;
+ }
+
+ private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri) {
+ if (handler == null || cookie == null || uri == null) {
+ Log.w(TAG, "queryAdn parameters incorrect");
+ return;
+ }
+
+ // display the progress dialog
+ cookie.progressDialog.show();
+
+ // run the query.
+ handler.startQuery(
+ ADN_QUERY_TOKEN,
+ cookie,
+ uri,
+ new String[] {ADN_PHONE_NUMBER_COLUMN_NAME},
+ null,
+ null,
+ null);
+
+ if (sPreviousAdnQueryHandler != null) {
+ // It is harmless to call cancel() even after the handler's gone.
+ sPreviousAdnQueryHandler.cancel();
+ }
+ sPreviousAdnQueryHandler = handler;
+ }
+
+ static boolean handlePinEntry(final Context context, final String input) {
+ if ((input.startsWith("**04") || input.startsWith("**05")) && input.endsWith("#")) {
+ List<PhoneAccountHandle> subscriptionAccountHandles =
+ PhoneAccountUtils.getSubscriptionPhoneAccounts(context);
+ boolean hasUserSelectedDefault =
+ subscriptionAccountHandles.contains(
+ TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL));
+
+ if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) {
+ // Don't bring up the dialog for single-SIM or if the default outgoing account is
+ // a subscription account.
+ return TelecomUtil.handleMmi(context, input, null);
+ } else {
+ SelectPhoneAccountListener listener = new HandleMmiAccountSelectedCallback(context, input);
+
+ DialogFragment dialogFragment =
+ SelectPhoneAccountDialogFragment.newInstance(
+ subscriptionAccountHandles, listener, null);
+ dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ // TODO: Use TelephonyCapabilities.getDeviceIdLabel() to get the device id label instead of a
+ // hard-coded string.
+ static boolean handleDeviceIdDisplay(Context context, String input) {
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+
+ if (telephonyManager != null && input.equals(MMI_IMEI_DISPLAY)) {
+ int labelResId =
+ (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM)
+ ? R.string.imei
+ : R.string.meid;
+
+ List<String> deviceIds = new ArrayList<String>();
+ if (TelephonyManagerCompat.getPhoneCount(telephonyManager) > 1
+ && CompatUtils.isMethodAvailable(
+ TelephonyManagerCompat.TELEPHONY_MANAGER_CLASS, "getDeviceId", Integer.TYPE)) {
+ for (int slot = 0; slot < telephonyManager.getPhoneCount(); slot++) {
+ String deviceId = telephonyManager.getDeviceId(slot);
+ if (!TextUtils.isEmpty(deviceId)) {
+ deviceIds.add(deviceId);
+ }
+ }
+ } else {
+ deviceIds.add(telephonyManager.getDeviceId());
+ }
+
+ new AlertDialog.Builder(context)
+ .setTitle(labelResId)
+ .setItems(deviceIds.toArray(new String[deviceIds.size()]), null)
+ .setPositiveButton(android.R.string.ok, null)
+ .setCancelable(false)
+ .show();
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean handleRegulatoryInfoDisplay(Context context, String input) {
+ if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) {
+ Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app");
+ Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO);
+ try {
+ context.startActivity(showRegInfoIntent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "startActivity() failed: " + e);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public static class HandleAdnEntryAccountSelectedCallback extends SelectPhoneAccountListener {
+
+ private final Context mContext;
+ private final QueryHandler mQueryHandler;
+ private final SimContactQueryCookie mCookie;
+
+ public HandleAdnEntryAccountSelectedCallback(
+ Context context, QueryHandler queryHandler, SimContactQueryCookie cookie) {
+ mContext = context;
+ mQueryHandler = queryHandler;
+ mCookie = cookie;
+ }
+
+ @Override
+ public void onPhoneAccountSelected(
+ PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {
+ Uri uri = TelecomUtil.getAdnUriForPhoneAccount(mContext, selectedAccountHandle);
+ handleAdnQuery(mQueryHandler, mCookie, uri);
+ // TODO: Show error dialog if result isn't valid.
+ }
+ }
+
+ public static class HandleMmiAccountSelectedCallback extends SelectPhoneAccountListener {
+
+ private final Context mContext;
+ private final String mInput;
+
+ public HandleMmiAccountSelectedCallback(Context context, String input) {
+ mContext = context.getApplicationContext();
+ mInput = input;
+ }
+
+ @Override
+ public void onPhoneAccountSelected(
+ PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {
+ TelecomUtil.handleMmi(mContext, mInput, selectedAccountHandle);
+ }
+ }
+
+ /**
+ * Cookie object that contains everything we need to communicate to the handler's onQuery
+ * Complete, as well as what we need in order to cancel the query (if requested).
+ *
+ * <p>Note, access to the textField field is going to be synchronized, because the user can
+ * request a cancel at any time through the UI.
+ */
+ private static class SimContactQueryCookie implements DialogInterface.OnCancelListener {
+
+ public ProgressDialog progressDialog;
+ public int contactNum;
+
+ // Used to identify the query request.
+ private int mToken;
+ private QueryHandler mHandler;
+
+ // The text field we're going to update
+ private EditText textField;
+
+ public SimContactQueryCookie(int number, QueryHandler handler, int token) {
+ contactNum = number;
+ mHandler = handler;
+ mToken = token;
+ }
+
+ /** Synchronized getter for the EditText. */
+ public synchronized EditText getTextField() {
+ return textField;
+ }
+
+ /** Synchronized setter for the EditText. */
+ public synchronized void setTextField(EditText text) {
+ textField = text;
+ }
+
+ /**
+ * Cancel the ADN query by stopping the operation and signaling the cookie that a cancel request
+ * is made.
+ */
+ @Override
+ public synchronized void onCancel(DialogInterface dialog) {
+ // close the progress dialog
+ if (progressDialog != null) {
+ progressDialog.dismiss();
+ }
+
+ // setting the textfield to null ensures that the UI does NOT get
+ // updated.
+ textField = null;
+
+ // Cancel the operation if possible.
+ mHandler.cancelOperation(mToken);
+ }
+ }
+
+ /**
+ * Asynchronous query handler that services requests to look up ADNs
+ *
+ * <p>Queries originate from {@link #handleAdnEntry}.
+ */
+ private static class QueryHandler extends NoNullCursorAsyncQueryHandler {
+
+ private boolean mCanceled;
+
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ /** Override basic onQueryComplete to fill in the textfield when we're handed the ADN cursor. */
+ @Override
+ protected void onNotNullableQueryComplete(int token, Object cookie, Cursor c) {
+ try {
+ sPreviousAdnQueryHandler = null;
+ if (mCanceled) {
+ return;
+ }
+
+ SimContactQueryCookie sc = (SimContactQueryCookie) cookie;
+
+ // close the progress dialog.
+ sc.progressDialog.dismiss();
+
+ // get the EditText to update or see if the request was cancelled.
+ EditText text = sc.getTextField();
+
+ // if the TextView is valid, and the cursor is valid and positionable on the
+ // Nth number, then we update the text field and display a toast indicating the
+ // caller name.
+ if ((c != null) && (text != null) && (c.moveToPosition(sc.contactNum))) {
+ String name = c.getString(c.getColumnIndexOrThrow(ADN_NAME_COLUMN_NAME));
+ String number = c.getString(c.getColumnIndexOrThrow(ADN_PHONE_NUMBER_COLUMN_NAME));
+
+ // fill the text in.
+ text.getText().replace(0, 0, number);
+
+ // display the name as a toast
+ Context context = sc.progressDialog.getContext();
+ CharSequence msg =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ context.getResources(), R.string.menu_callNumber, name);
+ Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+ }
+ } finally {
+ MoreCloseables.closeQuietly(c);
+ }
+ }
+
+ public void cancel() {
+ mCanceled = true;
+ // Ask AsyncQueryHandler to cancel the whole request. This will fail when the query is
+ // already started.
+ cancelOperation(ADN_QUERY_TOKEN);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/alert/AlertManager.java b/java/com/android/dialer/app/alert/AlertManager.java
new file mode 100644
index 0000000..ec61802
--- /dev/null
+++ b/java/com/android/dialer/app/alert/AlertManager.java
@@ -0,0 +1,30 @@
+/*
+ * 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.app.alert;
+
+import android.view.View;
+
+/** Manages "alerts" to gain the user's attention. */
+public interface AlertManager {
+
+ /** Inflates <code>layoutId</code> into a view that is ready to be inserted as an alert. */
+ View inflate(int layoutId);
+
+ void add(View view);
+
+ void clear();
+}
diff --git a/java/com/android/dialer/app/bindings/DialerBindings.java b/java/com/android/dialer/app/bindings/DialerBindings.java
new file mode 100644
index 0000000..e1f5178
--- /dev/null
+++ b/java/com/android/dialer/app/bindings/DialerBindings.java
@@ -0,0 +1,25 @@
+/*
+ * 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.app.bindings;
+
+import com.android.dialer.common.ConfigProvider;
+
+/** This interface allows the container application to customize the dialer. */
+public interface DialerBindings {
+
+ ConfigProvider getConfigProvider();
+}
diff --git a/java/com/android/dialer/app/bindings/DialerBindingsFactory.java b/java/com/android/dialer/app/bindings/DialerBindingsFactory.java
new file mode 100644
index 0000000..9f209f9
--- /dev/null
+++ b/java/com/android/dialer/app/bindings/DialerBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.app.bindings;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the dialer module
+ * to get references to the DialerBindings.
+ */
+public interface DialerBindingsFactory {
+
+ DialerBindings newDialerBindings();
+}
diff --git a/java/com/android/dialer/app/bindings/DialerBindingsStub.java b/java/com/android/dialer/app/bindings/DialerBindingsStub.java
new file mode 100644
index 0000000..f56743f
--- /dev/null
+++ b/java/com/android/dialer/app/bindings/DialerBindingsStub.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.app.bindings;
+
+import com.android.dialer.common.ConfigProvider;
+
+/** Default implementation for dialer bindings. */
+public class DialerBindingsStub implements DialerBindings {
+ private ConfigProvider configProvider;
+
+ @Override
+ public ConfigProvider getConfigProvider() {
+ if (configProvider == null) {
+ configProvider =
+ new ConfigProvider() {
+ @Override
+ public String getString(String key, String defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public long getLong(String key, long defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue) {
+ return defaultValue;
+ }
+ };
+ }
+ return configProvider;
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/BlockReportSpamListener.java b/java/com/android/dialer/app/calllog/BlockReportSpamListener.java
new file mode 100644
index 0000000..66f40bc
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/BlockReportSpamListener.java
@@ -0,0 +1,212 @@
+/*
+ * 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.app.calllog;
+
+import android.app.FragmentManager;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.support.v7.widget.RecyclerView;
+import com.android.dialer.blocking.BlockReportSpamDialogs;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.ReportingLocation;
+import com.android.dialer.spam.Spam;
+
+/** Listener to show dialogs for block and report spam actions. */
+public class BlockReportSpamListener implements CallLogListItemViewHolder.OnClickListener {
+
+ private final Context mContext;
+ private final FragmentManager mFragmentManager;
+ private final RecyclerView.Adapter mAdapter;
+ private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+
+ public BlockReportSpamListener(
+ Context context,
+ FragmentManager fragmentManager,
+ RecyclerView.Adapter adapter,
+ FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
+ mContext = context;
+ mFragmentManager = fragmentManager;
+ mAdapter = adapter;
+ mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler;
+ }
+
+ @Override
+ public void onBlockReportSpam(
+ String displayNumber,
+ final String number,
+ final String countryIso,
+ final int callType,
+ final int contactSourceType) {
+ BlockReportSpamDialogs.BlockReportSpamDialogFragment.newInstance(
+ displayNumber,
+ Spam.get(mContext).isDialogReportSpamCheckedByDefault(),
+ new BlockReportSpamDialogs.OnSpamDialogClickListener() {
+ @Override
+ public void onClick(boolean isSpamChecked) {
+ LogUtil.i("BlockReportSpamListener.onBlockReportSpam", "onClick");
+ if (isSpamChecked && Spam.get(mContext).isSpamEnabled()) {
+ Logger.get(mContext)
+ .logImpression(
+ DialerImpression.Type
+ .REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG);
+ Spam.get(mContext)
+ .reportSpamFromCallHistory(
+ number,
+ countryIso,
+ callType,
+ ReportingLocation.Type.CALL_LOG_HISTORY,
+ contactSourceType);
+ }
+ mFilteredNumberAsyncQueryHandler.blockNumber(
+ new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
+ @Override
+ public void onBlockComplete(Uri uri) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER);
+ mAdapter.notifyDataSetChanged();
+ }
+ },
+ number,
+ countryIso);
+ }
+ },
+ null)
+ .show(mFragmentManager, BlockReportSpamDialogs.BLOCK_REPORT_SPAM_DIALOG_TAG);
+ }
+
+ @Override
+ public void onBlock(
+ String displayNumber,
+ final String number,
+ final String countryIso,
+ final int callType,
+ final int contactSourceType) {
+ BlockReportSpamDialogs.BlockDialogFragment.newInstance(
+ displayNumber,
+ Spam.get(mContext).isSpamEnabled(),
+ new BlockReportSpamDialogs.OnConfirmListener() {
+ @Override
+ public void onClick() {
+ LogUtil.i("BlockReportSpamListener.onBlock", "onClick");
+ if (Spam.get(mContext).isSpamEnabled()) {
+ Logger.get(mContext)
+ .logImpression(
+ DialerImpression.Type
+ .DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER);
+ Spam.get(mContext)
+ .reportSpamFromCallHistory(
+ number,
+ countryIso,
+ callType,
+ ReportingLocation.Type.CALL_LOG_HISTORY,
+ contactSourceType);
+ }
+ mFilteredNumberAsyncQueryHandler.blockNumber(
+ new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
+ @Override
+ public void onBlockComplete(Uri uri) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER);
+ mAdapter.notifyDataSetChanged();
+ }
+ },
+ number,
+ countryIso);
+ }
+ },
+ null)
+ .show(mFragmentManager, BlockReportSpamDialogs.BLOCK_DIALOG_TAG);
+ }
+
+ @Override
+ public void onUnblock(
+ String displayNumber,
+ final String number,
+ final String countryIso,
+ final int callType,
+ final int contactSourceType,
+ final boolean isSpam,
+ final Integer blockId) {
+ BlockReportSpamDialogs.UnblockDialogFragment.newInstance(
+ displayNumber,
+ isSpam,
+ new BlockReportSpamDialogs.OnConfirmListener() {
+ @Override
+ public void onClick() {
+ LogUtil.i("BlockReportSpamListener.onUnblock", "onClick");
+ if (isSpam && Spam.get(mContext).isSpamEnabled()) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER);
+ Spam.get(mContext)
+ .reportNotSpamFromCallHistory(
+ number,
+ countryIso,
+ callType,
+ ReportingLocation.Type.CALL_LOG_HISTORY,
+ contactSourceType);
+ }
+ mFilteredNumberAsyncQueryHandler.unblock(
+ new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() {
+ @Override
+ public void onUnblockComplete(int rows, ContentValues values) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.USER_ACTION_UNBLOCKED_NUMBER);
+ mAdapter.notifyDataSetChanged();
+ }
+ },
+ blockId);
+ }
+ },
+ null)
+ .show(mFragmentManager, BlockReportSpamDialogs.UNBLOCK_DIALOG_TAG);
+ }
+
+ @Override
+ public void onReportNotSpam(
+ String displayNumber,
+ final String number,
+ final String countryIso,
+ final int callType,
+ final int contactSourceType) {
+ BlockReportSpamDialogs.ReportNotSpamDialogFragment.newInstance(
+ displayNumber,
+ new BlockReportSpamDialogs.OnConfirmListener() {
+ @Override
+ public void onClick() {
+ LogUtil.i("BlockReportSpamListener.onReportNotSpam", "onClick");
+ if (Spam.get(mContext).isSpamEnabled()) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM);
+ Spam.get(mContext)
+ .reportNotSpamFromCallHistory(
+ number,
+ countryIso,
+ callType,
+ ReportingLocation.Type.CALL_LOG_HISTORY,
+ contactSourceType);
+ }
+ mAdapter.notifyDataSetChanged();
+ }
+ },
+ null)
+ .show(mFragmentManager, BlockReportSpamDialogs.NOT_SPAM_DIALOG_TAG);
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java
new file mode 100644
index 0000000..ab6ef73
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.Context;
+import android.icu.lang.UCharacter;
+import android.icu.text.BreakIterator;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog.Calls;
+import android.text.format.DateUtils;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.app.R;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/** Adapter for a ListView containing history items from the details of a call. */
+public class CallDetailHistoryAdapter extends BaseAdapter {
+
+ /** Each history item shows the detail of a call. */
+ private static final int VIEW_TYPE_HISTORY_ITEM = 1;
+
+ private final Context mContext;
+ private final LayoutInflater mLayoutInflater;
+ private final CallTypeHelper mCallTypeHelper;
+ private final PhoneCallDetails[] mPhoneCallDetails;
+
+ /** List of items to be concatenated together for duration strings. */
+ private ArrayList<CharSequence> mDurationItems = new ArrayList<>();
+
+ public CallDetailHistoryAdapter(
+ Context context,
+ LayoutInflater layoutInflater,
+ CallTypeHelper callTypeHelper,
+ PhoneCallDetails[] phoneCallDetails) {
+ mContext = context;
+ mLayoutInflater = layoutInflater;
+ mCallTypeHelper = callTypeHelper;
+ mPhoneCallDetails = phoneCallDetails;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ // None of history will be clickable.
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ return mPhoneCallDetails.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mPhoneCallDetails[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return VIEW_TYPE_HISTORY_ITEM;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Make sure we have a valid convertView to start with
+ final View result =
+ convertView == null
+ ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false)
+ : convertView;
+
+ PhoneCallDetails details = mPhoneCallDetails[position];
+ CallTypeIconsView callTypeIconView =
+ (CallTypeIconsView) result.findViewById(R.id.call_type_icon);
+ TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text);
+ TextView dateView = (TextView) result.findViewById(R.id.date);
+ TextView durationView = (TextView) result.findViewById(R.id.duration);
+
+ int callType = details.callTypes[0];
+ boolean isVideoCall =
+ (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO
+ && CallUtil.isVideoEnabled(mContext);
+ boolean isPulledCall =
+ (details.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY;
+
+ callTypeIconView.clear();
+ callTypeIconView.add(callType);
+ callTypeIconView.setShowVideo(isVideoCall);
+ callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall));
+ // Set the date.
+ dateView.setText(formatDate(details.date));
+ // Set the duration
+ if (Calls.VOICEMAIL_TYPE == callType || CallTypeHelper.isMissedCallType(callType)) {
+ durationView.setVisibility(View.GONE);
+ } else {
+ durationView.setVisibility(View.VISIBLE);
+ durationView.setText(formatDurationAndDataUsage(details.duration, details.dataUsage));
+ }
+
+ return result;
+ }
+
+ /**
+ * Formats the provided date into a value suitable for display in the current locale.
+ *
+ * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
+ * may 25,20:02".
+ *
+ * <p>For pre-N devices, the returned value may not start with a capital if the local convention
+ * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
+ */
+ private CharSequence formatDate(long callDateMillis) {
+ CharSequence dateValue =
+ DateUtils.formatDateRange(
+ mContext,
+ callDateMillis /* startDate */,
+ callDateMillis /* endDate */,
+ DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_WEEKDAY
+ | DateUtils.FORMAT_SHOW_YEAR);
+
+ // We want the beginning of the date string to be capitalized, even if the word at the beginning
+ // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba”
+ // (not capitalized). To handle this issue we apply title casing to the start of the sentence so
+ // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02".
+ //
+ // The ICU library was not available in Android until N, so we can only do this in N+ devices.
+ // Pre-N devices will still see incorrect capitalization in some languages.
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ return dateValue;
+ }
+
+ // Using the ICU library is safer than just applying toUpperCase() on the first letter of the
+ // word because in some languages, there can be multiple starting characters which should be
+ // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be
+ // capitalized together.
+
+ // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the
+ // month ("May") are not lower-cased as part of the conversion.
+ return UCharacter.toTitleCase(
+ Locale.getDefault(),
+ dateValue.toString(),
+ BreakIterator.getSentenceInstance(),
+ UCharacter.TITLECASE_NO_LOWERCASE);
+ }
+
+ private CharSequence formatDuration(long elapsedSeconds) {
+ long minutes = 0;
+ long seconds = 0;
+
+ if (elapsedSeconds >= 60) {
+ minutes = elapsedSeconds / 60;
+ elapsedSeconds -= minutes * 60;
+ seconds = elapsedSeconds;
+ return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds);
+ } else {
+ seconds = elapsedSeconds;
+ return mContext.getString(R.string.callDetailsShortDurationFormat, seconds);
+ }
+ }
+
+ /**
+ * Formats a string containing the call duration and the data usage (if specified).
+ *
+ * @param elapsedSeconds Total elapsed seconds.
+ * @param dataUsage Data usage in bytes, or null if not specified.
+ * @return String containing call duration and data usage.
+ */
+ private CharSequence formatDurationAndDataUsage(long elapsedSeconds, Long dataUsage) {
+ CharSequence duration = formatDuration(elapsedSeconds);
+
+ if (dataUsage != null) {
+ mDurationItems.clear();
+ mDurationItems.add(duration);
+ mDurationItems.add(Formatter.formatShortFileSize(mContext, dataUsage));
+
+ return DialerUtils.join(mDurationItems);
+ } else {
+ return duration;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
new file mode 100644
index 0000000..ea09a8c
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -0,0 +1,915 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Trace;
+import android.provider.CallLog;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.dialer.app.Bindings;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator;
+import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.app.contactinfo.ContactInfoCache;
+import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.phonenumbercache.CallLogQuery;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.spam.Spam;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.Map;
+import java.util.Set;
+
+/** Adapter class to fill in data for the Call Log. */
+public class CallLogAdapter extends GroupingListAdapter
+ implements GroupCreator, OnVoicemailDeletedListener, CapabilitiesListener {
+
+ // Types of activities the call log adapter is used for
+ public static final int ACTIVITY_TYPE_CALL_LOG = 1;
+ public static final int ACTIVITY_TYPE_DIALTACTS = 2;
+ private static final int NO_EXPANDED_LIST_ITEM = -1;
+ public static final int ALERT_POSITION = 0;
+ private static final int VIEW_TYPE_ALERT = 1;
+ private static final int VIEW_TYPE_CALLLOG = 2;
+
+ private static final String KEY_EXPANDED_POSITION = "expanded_position";
+ private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
+
+ public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data";
+
+ protected final Activity mActivity;
+ protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+ /** Cache for repeated requests to Telecom/Telephony. */
+ protected final CallLogCache mCallLogCache;
+
+ private final CallFetcher mCallFetcher;
+ private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+ private final int mActivityType;
+
+ /** Instance of helper class for managing views. */
+ private final CallLogListItemHelper mCallLogListItemHelper;
+ /** Helper to group call log entries. */
+ private final CallLogGroupBuilder mCallLogGroupBuilder;
+
+ private final AsyncTaskExecutor mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
+ private ContactInfoCache mContactInfoCache;
+ // Tracks the position of the currently expanded list item.
+ private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
+ // Tracks the rowId of the currently expanded list item, so the position can be updated if there
+ // are any changes to the call log entries, such as additions or removals.
+ private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
+
+ private final CallLogAlertManager mCallLogAlertManager;
+ /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */
+ private final View.OnClickListener mExpandCollapseListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
+ if (viewHolder == null) {
+ return;
+ }
+
+ if (mVoicemailPlaybackPresenter != null) {
+ // Always reset the voicemail playback state on expand or collapse.
+ mVoicemailPlaybackPresenter.resetAll();
+ }
+
+ if (viewHolder.rowId == mCurrentlyExpandedRowId) {
+ // Hide actions, if the clicked item is the expanded item.
+ viewHolder.showActions(false);
+
+ mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
+ mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
+ } else {
+ if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
+ CallLogAsyncTaskUtil.markCallAsRead(mActivity, viewHolder.callIds);
+ if (mActivityType == ACTIVITY_TYPE_DIALTACTS) {
+ ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
+ }
+ }
+ expandViewHolderActions(viewHolder);
+ }
+ }
+ };
+
+ /**
+ * A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead
+ * if removing an item, it will be shown as an invisible view. This simplifies the calculation of
+ * item position.
+ */
+ @NonNull private Set<Long> mHiddenRowIds = new ArraySet<>();
+ /**
+ * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo
+ * timeout, all of the pending URIs will be deleted.
+ *
+ * <p>TODO: move this and OnVoicemailDeletedListener to somewhere like {@link
+ * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with
+ * hidden item or what to hide.
+ */
+ @NonNull private final Set<Uri> mHiddenItemUris = new ArraySet<>();
+
+ private CallLogListItemViewHolder.OnClickListener mBlockReportSpamListener;
+ /**
+ * Map, keyed by call Id, used to track the day group for a call. As call log entries are put into
+ * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are
+ * also assigned a secondary "day group". This map tracks the day group assigned to all calls in
+ * the call log. This information is used to trigger the display of a day group header above the
+ * call log entry at the start of a day group. Note: Multiple calls are grouped into a single
+ * primary "call group" in the call log, and the cursor used to bind rows includes all of these
+ * calls. When determining if a day group change has occurred it is necessary to look at the last
+ * entry in the call log to determine its day group. This map provides a means of determining the
+ * previous day group without having to reverse the cursor to the start of the previous day call
+ * log entry.
+ */
+ private Map<Long, Integer> mDayGroups = new ArrayMap<>();
+
+ private boolean mLoading = true;
+ private ContactsPreferences mContactsPreferences;
+
+ private boolean mIsSpamEnabled;
+
+ @NonNull private final EnrichedCallManager mEnrichedCallManager;
+
+ public CallLogAdapter(
+ Activity activity,
+ ViewGroup alertContainer,
+ CallFetcher callFetcher,
+ CallLogCache callLogCache,
+ ContactInfoCache contactInfoCache,
+ VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ int activityType) {
+ super();
+
+ mActivity = activity;
+ mCallFetcher = callFetcher;
+ mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+ if (mVoicemailPlaybackPresenter != null) {
+ mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
+ }
+
+ mActivityType = activityType;
+
+ mContactInfoCache = contactInfoCache;
+
+ if (!PermissionsUtil.hasContactsPermissions(activity)) {
+ mContactInfoCache.disableRequestProcessing();
+ }
+
+ Resources resources = mActivity.getResources();
+
+ mCallLogCache = callLogCache;
+
+ PhoneCallDetailsHelper phoneCallDetailsHelper =
+ new PhoneCallDetailsHelper(mActivity, resources, mCallLogCache);
+ mCallLogListItemHelper =
+ new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
+ mCallLogGroupBuilder = new CallLogGroupBuilder(this);
+ mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mActivity);
+
+ mContactsPreferences = new ContactsPreferences(mActivity);
+
+ mBlockReportSpamListener =
+ new BlockReportSpamListener(
+ mActivity,
+ ((Activity) mActivity).getFragmentManager(),
+ this,
+ mFilteredNumberAsyncQueryHandler);
+ setHasStableIds(true);
+
+ mCallLogAlertManager =
+ new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer);
+ mEnrichedCallManager = EnrichedCallManager.Accessor.getInstance(activity.getApplication());
+ }
+
+ private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
+ if (!TextUtils.isEmpty(viewHolder.voicemailUri)) {
+ Logger.get(mActivity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY);
+ }
+
+ int lastExpandedPosition = mCurrentlyExpandedPosition;
+ // Show the actions for the clicked list item.
+ viewHolder.showActions(true);
+ mCurrentlyExpandedPosition = viewHolder.getAdapterPosition();
+ mCurrentlyExpandedRowId = viewHolder.rowId;
+
+ // If another item is expanded, notify it that it has changed. Its actions will be
+ // hidden when it is re-binded because we change mCurrentlyExpandedRowId above.
+ if (lastExpandedPosition != RecyclerView.NO_POSITION) {
+ notifyItemChanged(lastExpandedPosition);
+ }
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition);
+ outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId);
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mCurrentlyExpandedPosition =
+ savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
+ mCurrentlyExpandedRowId =
+ savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
+ }
+ }
+
+ /** Requery on background thread when {@link Cursor} changes. */
+ @Override
+ protected void onContentChanged() {
+ mCallFetcher.fetchCalls();
+ }
+
+ public void setLoading(boolean loading) {
+ mLoading = loading;
+ }
+
+ public boolean isEmpty() {
+ if (mLoading) {
+ // We don't want the empty state to show when loading.
+ return false;
+ } else {
+ return getItemCount() == 0;
+ }
+ }
+
+ public void clearFilteredNumbersCache() {
+ mFilteredNumberAsyncQueryHandler.clearCache();
+ }
+
+ public void onResume() {
+ if (PermissionsUtil.hasPermission(mActivity, android.Manifest.permission.READ_CONTACTS)) {
+ mContactInfoCache.start();
+ }
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled();
+ mEnrichedCallManager.registerCapabilitiesListener(this);
+ notifyDataSetChanged();
+ }
+
+ public void onPause() {
+ pauseCache();
+ for (Uri uri : mHiddenItemUris) {
+ CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null);
+ }
+ mEnrichedCallManager.unregisterCapabilitiesListener(this);
+ }
+
+ public void onStop() {
+ mEnrichedCallManager.clearCachedData();
+ }
+
+ public CallLogAlertManager getAlertManager() {
+ return mCallLogAlertManager;
+ }
+
+ @VisibleForTesting
+ /* package */ void pauseCache() {
+ mContactInfoCache.stop();
+ mCallLogCache.reset();
+ }
+
+ @Override
+ protected void addGroups(Cursor cursor) {
+ mCallLogGroupBuilder.addGroups(cursor);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (viewType == VIEW_TYPE_ALERT) {
+ return mCallLogAlertManager.createViewHolder(parent);
+ }
+ return createCallLogEntryViewHolder(parent);
+ }
+
+ /**
+ * Creates a new call log entry {@link ViewHolder}.
+ *
+ * @param parent the parent view.
+ * @return The {@link ViewHolder}.
+ */
+ private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(mActivity);
+ View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+ CallLogListItemViewHolder viewHolder =
+ CallLogListItemViewHolder.create(
+ view,
+ mActivity,
+ mBlockReportSpamListener,
+ mExpandCollapseListener,
+ mCallLogCache,
+ mCallLogListItemHelper,
+ mVoicemailPlaybackPresenter);
+
+ viewHolder.callLogEntryView.setTag(viewHolder);
+
+ viewHolder.primaryActionView.setTag(viewHolder);
+
+ return viewHolder;
+ }
+
+ /**
+ * Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times
+ * when Dialer starts up for a single call log entry and should not. It invokes cross-process
+ * methods and the repeat execution can get costly.
+ *
+ * @param viewHolder The view corresponding to this entry.
+ * @param position The position of the entry.
+ */
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, int position) {
+ Trace.beginSection("onBindViewHolder: " + position);
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ALERT:
+ //Do nothing
+ break;
+ default:
+ bindCallLogListViewHolder(viewHolder, position);
+ break;
+ }
+ Trace.endSection();
+ }
+
+ @Override
+ public void onViewRecycled(ViewHolder viewHolder) {
+ if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
+ CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
+ if (views.asyncTask != null) {
+ views.asyncTask.cancel(true);
+ }
+ }
+ }
+
+ @Override
+ public void onViewAttachedToWindow(ViewHolder viewHolder) {
+ if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
+ ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true;
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(ViewHolder viewHolder) {
+ if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
+ ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false;
+ }
+ }
+
+ /**
+ * Binds the view holder for the call log list item view.
+ *
+ * @param viewHolder The call log list item view holder.
+ * @param position The position of the list item.
+ */
+ private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) {
+ Cursor c = (Cursor) getItem(position);
+ if (c == null) {
+ return;
+ }
+ CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
+ views.isLoaded = false;
+ PhoneCallDetails details = createPhoneCallDetails(c, getGroupSize(position), views);
+ if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) {
+ views.callLogEntryView.setVisibility(View.GONE);
+ views.dayGroupHeader.setVisibility(View.GONE);
+ return;
+ } else {
+ views.callLogEntryView.setVisibility(View.VISIBLE);
+ // dayGroupHeader will be restored after loadAndRender() if it is needed.
+ }
+ if (mCurrentlyExpandedRowId == views.rowId) {
+ views.inflateActionViewStub();
+ }
+ loadAndRender(views, views.rowId, details);
+ }
+
+ private void loadAndRender(
+ final CallLogListItemViewHolder views, final long rowId, final PhoneCallDetails details) {
+ // Reset block and spam information since this view could be reused which may contain
+ // outdated data.
+ views.isSpam = false;
+ views.blockId = null;
+ views.isSpamFeatureEnabled = false;
+ views.isCallComposerCapable =
+ isCallComposerCapable(PhoneNumberUtils.formatNumberToE164(views.number, views.countryIso));
+ final AsyncTask<Void, Void, Boolean> loadDataTask =
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ views.blockId =
+ mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly(
+ views.number, views.countryIso);
+ details.isBlocked = views.blockId != null;
+ if (isCancelled()) {
+ return false;
+ }
+ if (mIsSpamEnabled) {
+ views.isSpamFeatureEnabled = true;
+ // Only display the call as a spam call if there are incoming calls in the list.
+ // Call log cards with only outgoing calls should never be displayed as spam.
+ views.isSpam =
+ details.hasIncomingCalls()
+ && Spam.get(mActivity)
+ .checkSpamStatusSynchronous(views.number, views.countryIso);
+ details.isSpam = views.isSpam;
+ if (isCancelled()) {
+ return false;
+ }
+ return loadData(views, rowId, details);
+ } else {
+ return loadData(views, rowId, details);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ views.isLoaded = true;
+ if (success) {
+ int currentGroup = getDayGroupForCall(views.rowId);
+ if (currentGroup != details.previousGroup) {
+ views.dayGroupHeaderVisibility = View.VISIBLE;
+ views.dayGroupHeaderText = getGroupDescription(currentGroup);
+ } else {
+ views.dayGroupHeaderVisibility = View.GONE;
+ }
+ render(views, details, rowId);
+ }
+ }
+ };
+
+ views.asyncTask = loadDataTask;
+ mAsyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask);
+ }
+
+ @MainThread
+ private boolean isCallComposerCapable(@Nullable String e164Number) {
+ if (e164Number == null) {
+ return false;
+ }
+
+ EnrichedCallCapabilities capabilities = mEnrichedCallManager.getCapabilities(e164Number);
+ if (capabilities == null) {
+ mEnrichedCallManager.requestCapabilities(e164Number);
+ return false;
+ }
+ return capabilities.supportsCallComposer();
+ }
+
+ /**
+ * Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main
+ * thread since cursor is not thread safe.
+ */
+ @MainThread
+ private PhoneCallDetails createPhoneCallDetails(
+ Cursor cursor, int count, final CallLogListItemViewHolder views) {
+ Assert.isMainThread();
+ final String number = cursor.getString(CallLogQuery.NUMBER);
+ final String postDialDigits =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ final String viaNumber =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : "";
+ final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
+ final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor);
+ final PhoneCallDetails details =
+ new PhoneCallDetails(number, numberPresentation, postDialDigits);
+ details.viaNumber = viaNumber;
+ details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
+ details.date = cursor.getLong(CallLogQuery.DATE);
+ details.duration = cursor.getLong(CallLogQuery.DURATION);
+ details.features = getCallFeatures(cursor, count);
+ details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION);
+ details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION);
+ details.callTypes = getCallTypes(cursor, count);
+
+ details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+ details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+ details.cachedContactInfo = cachedContactInfo;
+
+ if (!cursor.isNull(CallLogQuery.DATA_USAGE)) {
+ details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE);
+ }
+
+ views.rowId = cursor.getLong(CallLogQuery.ID);
+ // Stash away the Ids of the calls so that we can support deleting a row in the call log.
+ views.callIds = getCallIds(cursor, count);
+ details.previousGroup = getPreviousDayGroup(cursor);
+
+ // Store values used when the actions ViewStub is inflated on expansion.
+ views.number = number;
+ views.countryIso = details.countryIso;
+ views.postDialDigits = details.postDialDigits;
+ views.numberPresentation = numberPresentation;
+
+ if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE
+ || details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
+ details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1;
+ }
+ views.callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI);
+
+ return details;
+ }
+
+ /**
+ * Load data for call log. Any expensive operation should be put here to avoid blocking main
+ * thread. Do NOT put any cursor operation here since it's not thread safe.
+ */
+ @WorkerThread
+ private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) {
+ Assert.isWorkerThread();
+ if (rowId != views.rowId) {
+ LogUtil.i(
+ "CallLogAdapter.loadData",
+ "rowId of viewHolder changed after load task is issued, aborting load");
+ return false;
+ }
+
+ final PhoneAccountHandle accountHandle =
+ PhoneAccountUtils.getAccount(details.accountComponentName, details.accountId);
+
+ final boolean isVoicemailNumber =
+ mCallLogCache.isVoicemailNumber(accountHandle, details.number);
+
+ // Note: Binding of the action buttons is done as required in configureActionViews when the
+ // user expands the actions ViewStub.
+
+ ContactInfo info = ContactInfo.EMPTY;
+ if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation)
+ && !isVoicemailNumber) {
+ // Lookup contacts with this number
+ // Only do remote lookup in first 5 rows.
+ info =
+ mContactInfoCache.getValue(
+ details.number + details.postDialDigits,
+ details.countryIso,
+ details.cachedContactInfo,
+ rowId
+ < Bindings.get(mActivity)
+ .getConfigProvider()
+ .getLong("number_of_call_to_do_remote_lookup", 5L));
+ }
+ CharSequence formattedNumber =
+ info.formattedNumber == null
+ ? null
+ : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber);
+ details.updateDisplayNumber(mActivity, formattedNumber, isVoicemailNumber);
+
+ views.displayNumber = details.displayNumber;
+ views.accountHandle = accountHandle;
+ details.accountHandle = accountHandle;
+
+ if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
+ details.contactUri = info.lookupUri;
+ details.namePrimary = info.name;
+ details.nameAlternative = info.nameAlternative;
+ details.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
+ details.numberType = info.type;
+ details.numberLabel = info.label;
+ details.photoUri = info.photoUri;
+ details.sourceType = info.sourceType;
+ details.objectId = info.objectId;
+ details.contactUserType = info.userType;
+ }
+
+ views.info = info;
+ views.numberType =
+ (String)
+ Phone.getTypeLabel(mActivity.getResources(), details.numberType, details.numberLabel);
+
+ mCallLogListItemHelper.updatePhoneCallDetails(details);
+ return true;
+ }
+
+ /**
+ * Render item view given position. This is running on UI thread so DO NOT put any expensive
+ * operation into it.
+ */
+ @MainThread
+ private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) {
+ Assert.isMainThread();
+ if (rowId != views.rowId) {
+ LogUtil.i(
+ "CallLogAdapter.render",
+ "rowId of viewHolder changed after load task is issued, aborting render");
+ return;
+ }
+
+ // Default case: an item in the call log.
+ views.primaryActionView.setVisibility(View.VISIBLE);
+ views.workIconView.setVisibility(
+ details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
+
+ mCallLogListItemHelper.setPhoneCallDetails(views, details);
+ if (mCurrentlyExpandedRowId == views.rowId) {
+ // In case ViewHolders were added/removed, update the expanded position if the rowIds
+ // match so that we can restore the correct expanded state on rebind.
+ mCurrentlyExpandedPosition = views.getAdapterPosition();
+ views.showActions(true);
+ } else {
+ views.showActions(false);
+ }
+ views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility);
+ views.dayGroupHeader.setText(views.dayGroupHeaderText);
+ }
+
+ @Override
+ public int getItemCount() {
+ return super.getItemCount() + (mCallLogAlertManager.isEmpty() ? 0 : 1);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == ALERT_POSITION && !mCallLogAlertManager.isEmpty()) {
+ return VIEW_TYPE_ALERT;
+ }
+ return VIEW_TYPE_CALLLOG;
+ }
+
+ /**
+ * Retrieves an item at the specified position, taking into account the presence of a promo card.
+ *
+ * @param position The position to retrieve.
+ * @return The item at that position.
+ */
+ @Override
+ public Object getItem(int position) {
+ return super.getItem(position - (mCallLogAlertManager.isEmpty() ? 0 : 1));
+ }
+
+ @Override
+ public long getItemId(int position) {
+ Cursor cursor = (Cursor) getItem(position);
+ if (cursor != null) {
+ return cursor.getLong(CallLogQuery.ID);
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int getGroupSize(int position) {
+ return super.getGroupSize(position - (mCallLogAlertManager.isEmpty() ? 0 : 1));
+ }
+
+ protected boolean isCallLogActivity() {
+ return mActivityType == ACTIVITY_TYPE_CALL_LOG;
+ }
+
+ /**
+ * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
+ * clicks the delete button, the deleted item is temporarily hidden from the list. If a user
+ * clicks delete on a second item before the first item's undo option has expired, the first item
+ * is immediately deleted so that only one item can be "undoed" at a time.
+ */
+ @Override
+ public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) {
+ mHiddenRowIds.add(viewHolder.rowId);
+ // Save the new hidden item uri in case the activity is suspend before the undo has timed out.
+ mHiddenItemUris.add(uri);
+
+ collapseExpandedCard();
+ notifyItemChanged(viewHolder.getAdapterPosition());
+ // The next item might have to update its day group label
+ notifyItemChanged(viewHolder.getAdapterPosition() + 1);
+ }
+
+ private void collapseExpandedCard() {
+ mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
+ mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
+ }
+
+ /** When the list is changing all stored position is no longer valid. */
+ public void invalidatePositions() {
+ mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
+ }
+
+ /** When the user clicks "undo", the hidden item is unhidden. */
+ @Override
+ public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) {
+ mHiddenItemUris.remove(uri);
+ mHiddenRowIds.remove(rowId);
+ notifyItemChanged(adapterPosition);
+ // The next item might have to update its day group label
+ notifyItemChanged(adapterPosition + 1);
+ }
+
+ /** This callback signifies that a database deletion has completed. */
+ @Override
+ public void onVoicemailDeletedInDatabase(long rowId, Uri uri) {
+ mHiddenItemUris.remove(uri);
+ }
+
+ /**
+ * Retrieves the day group of the previous call in the call log. Used to determine if the day
+ * group has changed and to trigger display of the day group text.
+ *
+ * @param cursor The call log cursor.
+ * @return The previous day group, or DAY_GROUP_NONE if this is the first call.
+ */
+ private int getPreviousDayGroup(Cursor cursor) {
+ // We want to restore the position in the cursor at the end.
+ int startingPosition = cursor.getPosition();
+ moveToPreviousNonHiddenRow(cursor);
+ if (cursor.isBeforeFirst()) {
+ cursor.moveToPosition(startingPosition);
+ return CallLogGroupBuilder.DAY_GROUP_NONE;
+ }
+ int result = getDayGroupForCall(cursor.getLong(CallLogQuery.ID));
+ cursor.moveToPosition(startingPosition);
+ return result;
+ }
+
+ private void moveToPreviousNonHiddenRow(Cursor cursor) {
+ while (cursor.moveToPrevious() && mHiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {}
+ }
+
+ /**
+ * Given a call Id, look up the day group that the call belongs to. The day group data is
+ * populated in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}.
+ *
+ * @param callId The call to retrieve the day group for.
+ * @return The day group for the call.
+ */
+ @MainThread
+ private int getDayGroupForCall(long callId) {
+ Integer result = mDayGroups.get(callId);
+ if (result != null) {
+ return result;
+ }
+ return CallLogGroupBuilder.DAY_GROUP_NONE;
+ }
+
+ /**
+ * Returns the call types for the given number of items in the cursor.
+ *
+ * <p>It uses the next {@code count} rows in the cursor to extract the types.
+ *
+ * <p>It position in the cursor is unchanged by this function.
+ */
+ private static int[] getCallTypes(Cursor cursor, int count) {
+ int position = cursor.getPosition();
+ int[] callTypes = new int[count];
+ for (int index = 0; index < count; ++index) {
+ callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
+ cursor.moveToNext();
+ }
+ cursor.moveToPosition(position);
+ return callTypes;
+ }
+
+ /**
+ * Determine the features which were enabled for any of the calls that make up a call log entry.
+ *
+ * @param cursor The cursor.
+ * @param count The number of calls for the current call log entry.
+ * @return The features.
+ */
+ private int getCallFeatures(Cursor cursor, int count) {
+ int features = 0;
+ int position = cursor.getPosition();
+ for (int index = 0; index < count; ++index) {
+ features |= cursor.getInt(CallLogQuery.FEATURES);
+ cursor.moveToNext();
+ }
+ cursor.moveToPosition(position);
+ return features;
+ }
+
+ /**
+ * Sets whether processing of requests for contact details should be enabled.
+ *
+ * <p>This method should be called in tests to disable such processing of requests when not
+ * needed.
+ */
+ @VisibleForTesting
+ void disableRequestProcessingForTest() {
+ // TODO: Remove this and test the cache directly.
+ mContactInfoCache.disableRequestProcessing();
+ }
+
+ @VisibleForTesting
+ void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
+ // TODO: Remove this and test the cache directly.
+ mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
+ }
+
+ /**
+ * Stores the day group associated with a call in the call log.
+ *
+ * @param rowId The row Id of the current call.
+ * @param dayGroup The day group the call belongs in.
+ */
+ @Override
+ @MainThread
+ public void setDayGroup(long rowId, int dayGroup) {
+ if (!mDayGroups.containsKey(rowId)) {
+ mDayGroups.put(rowId, dayGroup);
+ }
+ }
+
+ /** Clears the day group associations on re-bind of the call log. */
+ @Override
+ @MainThread
+ public void clearDayGroups() {
+ mDayGroups.clear();
+ }
+
+ /**
+ * Retrieves the call Ids represented by the current call log row.
+ *
+ * @param cursor Call log cursor to retrieve call Ids from.
+ * @param groupSize Number of calls associated with the current call log row.
+ * @return Array of call Ids.
+ */
+ private long[] getCallIds(final Cursor cursor, final int groupSize) {
+ // We want to restore the position in the cursor at the end.
+ int startingPosition = cursor.getPosition();
+ long[] ids = new long[groupSize];
+ // Copy the ids of the rows in the group.
+ for (int index = 0; index < groupSize; ++index) {
+ ids[index] = cursor.getLong(CallLogQuery.ID);
+ cursor.moveToNext();
+ }
+ cursor.moveToPosition(startingPosition);
+ return ids;
+ }
+
+ /**
+ * Determines the description for a day group.
+ *
+ * @param group The day group to retrieve the description for.
+ * @return The day group description.
+ */
+ private CharSequence getGroupDescription(int group) {
+ if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
+ return mActivity.getResources().getString(R.string.call_log_header_today);
+ } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
+ return mActivity.getResources().getString(R.string.call_log_header_yesterday);
+ } else {
+ return mActivity.getResources().getString(R.string.call_log_header_other);
+ }
+ }
+
+ @Override
+ public void onCapabilitiesUpdated() {
+ notifyDataSetChanged();
+ }
+
+ /** Interface used to initiate a refresh of the content. */
+ public interface CallFetcher {
+
+ void fetchCalls();
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogAlertManager.java b/java/com/android/dialer/app/calllog/CallLogAlertManager.java
new file mode 100644
index 0000000..40b30f0
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogAlertManager.java
@@ -0,0 +1,90 @@
+/*
+ * 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.app.calllog;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.app.R;
+import com.android.dialer.app.alert.AlertManager;
+import com.android.dialer.common.Assert;
+
+/** Manages "alerts" to be shown at the top of an call log to gain the user's attention. */
+public class CallLogAlertManager implements AlertManager {
+
+ private final CallLogAdapter adapter;
+ private final View view;
+ private final LayoutInflater inflater;
+ private final ViewGroup parent;
+ private final ViewGroup container;
+
+ public CallLogAlertManager(CallLogAdapter adapter, LayoutInflater inflater, ViewGroup parent) {
+ this.adapter = adapter;
+ this.inflater = inflater;
+ this.parent = parent;
+ view = inflater.inflate(R.layout.call_log_alert_item, parent, false);
+ container = (ViewGroup) view.findViewById(R.id.container);
+ }
+
+ @Override
+ public View inflate(int layoutId) {
+ return inflater.inflate(layoutId, container, false);
+ }
+
+ public RecyclerView.ViewHolder createViewHolder(ViewGroup parent) {
+ Assert.checkArgument(
+ parent == this.parent,
+ "createViewHolder should be called with the same parent in constructor");
+ return new AlertViewHolder(view);
+ }
+
+ public boolean isEmpty() {
+ return container.getChildCount() == 0;
+ }
+
+ public boolean contains(View view) {
+ return container.indexOfChild(view) != -1;
+ }
+
+ @Override
+ public void clear() {
+ container.removeAllViews();
+ adapter.notifyItemRemoved(CallLogAdapter.ALERT_POSITION);
+ }
+
+ @Override
+ public void add(View view) {
+ if (contains(view)) {
+ return;
+ }
+ container.addView(view);
+ if (container.getChildCount() == 1) {
+ // Was empty before
+ adapter.notifyItemInserted(CallLogAdapter.ALERT_POSITION);
+ }
+ }
+
+ /**
+ * Does nothing. The view this ViewHolder show is directly managed by {@link CallLogAlertManager}
+ */
+ private static class AlertViewHolder extends RecyclerView.ViewHolder {
+ private AlertViewHolder(View view) {
+ super(view);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogAsync.java b/java/com/android/dialer/app/calllog/CallLogAsync.java
new file mode 100644
index 0000000..f62deca
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogAsync.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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.app.calllog;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import com.android.dialer.common.Assert;
+
+/**
+ * Class to access the call log asynchronously to avoid carrying out database operations on the UI
+ * thread, using an {@link AsyncTask}.
+ *
+ * <pre class="prettyprint"> Typical usage: ==============
+ *
+ * // From an activity... String mLastNumber = "";
+ *
+ * CallLogAsync log = new CallLogAsync();
+ *
+ * CallLogAsync.GetLastOutgoingCallArgs lastCallArgs = new CallLogAsync.GetLastOutgoingCallArgs(
+ * this, new CallLogAsync.OnLastOutgoingCallComplete() { public void lastOutgoingCall(String number)
+ * { mLastNumber = number; } }); log.getLastOutgoingCall(lastCallArgs); </pre>
+ */
+public class CallLogAsync {
+
+ /** CallLog.getLastOutgoingCall(...) */
+ public AsyncTask getLastOutgoingCall(GetLastOutgoingCallArgs args) {
+ Assert.isMainThread();
+ return new GetLastOutgoingCallTask(args.callback).execute(args);
+ }
+
+ /** Interface to retrieve the last dialed number asynchronously. */
+ public interface OnLastOutgoingCallComplete {
+
+ /** @param number The last dialed number or an empty string if none exists yet. */
+ void lastOutgoingCall(String number);
+ }
+
+ /** Parameter object to hold the args to get the last outgoing call from the call log DB. */
+ public static class GetLastOutgoingCallArgs {
+
+ public final Context context;
+ public final OnLastOutgoingCallComplete callback;
+
+ public GetLastOutgoingCallArgs(Context context, OnLastOutgoingCallComplete callback) {
+ this.context = context;
+ this.callback = callback;
+ }
+ }
+
+ /** AsyncTask to get the last outgoing call from the DB. */
+ private class GetLastOutgoingCallTask extends AsyncTask<GetLastOutgoingCallArgs, Void, String> {
+
+ private final OnLastOutgoingCallComplete mCallback;
+
+ public GetLastOutgoingCallTask(OnLastOutgoingCallComplete callback) {
+ mCallback = callback;
+ }
+
+ // Happens on a background thread. We cannot run the callback
+ // here because only the UI thread can modify the view
+ // hierarchy (e.g enable/disable the dial button). The
+ // callback is ran rom the post execute method.
+ @Override
+ protected String doInBackground(GetLastOutgoingCallArgs... list) {
+ String number = "";
+ for (GetLastOutgoingCallArgs args : list) {
+ // May block. Select only the last one.
+ number = Calls.getLastOutgoingCall(args.context);
+ }
+ return number; // passed to the onPostExecute method.
+ }
+
+ // Happens on the UI thread, it is safe to run the callback
+ // that may do some work on the views.
+ @Override
+ protected void onPostExecute(String number) {
+ Assert.isMainThread();
+ mCallback.lastOutgoingCall(number);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
new file mode 100644
index 0000000..b4e6fc5
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2015 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.app.calllog;
+
+import android.Manifest.permission;
+import android.annotation.TargetApi;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@TargetApi(VERSION_CODES.M)
+public class CallLogAsyncTaskUtil {
+
+ private static final String TAG = "CallLogAsyncTaskUtil";
+ private static AsyncTaskExecutor sAsyncTaskExecutor;
+
+ private static void initTaskExecutor() {
+ sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+ }
+
+ public static void getCallDetails(
+ @NonNull final Context context,
+ @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener,
+ @NonNull final Uri... callUris) {
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(
+ Tasks.GET_CALL_DETAILS,
+ new AsyncTask<Void, Void, PhoneCallDetails[]>() {
+ @Override
+ public PhoneCallDetails[] doInBackground(Void... params) {
+ if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.w("CallLogAsyncTaskUtil.getCallDetails", "missing READ_CALL_LOG permission");
+ return null;
+ }
+ // TODO: All calls correspond to the same person, so make a single lookup.
+ final int numCalls = callUris.length;
+ PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
+ try {
+ for (int index = 0; index < numCalls; ++index) {
+ details[index] = getPhoneCallDetailsForUri(context, callUris[index]);
+ }
+ return details;
+ } catch (IllegalArgumentException e) {
+ // Something went wrong reading in our primary data.
+ LogUtil.e(
+ "CallLogAsyncTaskUtil.getCallDetails", "invalid URI starting call details", e);
+ return null;
+ }
+ }
+
+ @Override
+ public void onPostExecute(PhoneCallDetails[] phoneCallDetails) {
+ if (callLogAsyncTaskListener != null) {
+ callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails);
+ }
+ }
+ });
+ }
+
+ /** Return the phone call details for a given call log URI. */
+ private static PhoneCallDetails getPhoneCallDetailsForUri(
+ @NonNull Context context, @NonNull Uri callUri) {
+ Cursor cursor =
+ context
+ .getContentResolver()
+ .query(callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null);
+
+ try {
+ if (cursor == null || !cursor.moveToFirst()) {
+ throw new IllegalArgumentException("Cannot find content: " + callUri);
+ }
+
+ // Read call log.
+ final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX);
+ final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX);
+ final String postDialDigits =
+ (VERSION.SDK_INT >= VERSION_CODES.N)
+ ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS)
+ : "";
+ final String viaNumber =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallDetailQuery.VIA_NUMBER) : "";
+ final int numberPresentation =
+ cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX);
+
+ final PhoneAccountHandle accountHandle =
+ PhoneAccountUtils.getAccount(
+ cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME),
+ cursor.getString(CallDetailQuery.ACCOUNT_ID));
+
+ // If this is not a regular number, there is no point in looking it up in the contacts.
+ ContactInfoHelper contactInfoHelper =
+ new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context));
+ boolean isVoicemail = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number);
+ boolean shouldLookupNumber =
+ PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) && !isVoicemail;
+ ContactInfo info = ContactInfo.EMPTY;
+
+ if (shouldLookupNumber) {
+ ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso);
+ info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY;
+ }
+
+ PhoneCallDetails details = new PhoneCallDetails(number, numberPresentation, postDialDigits);
+ details.updateDisplayNumber(context, info.formattedNumber, isVoicemail);
+
+ details.viaNumber = viaNumber;
+ details.accountHandle = accountHandle;
+ details.contactUri = info.lookupUri;
+ details.namePrimary = info.name;
+ details.nameAlternative = info.nameAlternative;
+ details.numberType = info.type;
+ details.numberLabel = info.label;
+ details.photoUri = info.photoUri;
+ details.sourceType = info.sourceType;
+ details.objectId = info.objectId;
+
+ details.callTypes = new int[] {cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX)};
+ details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX);
+ details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX);
+ details.features = cursor.getInt(CallDetailQuery.FEATURES);
+ details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX);
+ details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX);
+
+ details.countryIso =
+ !TextUtils.isEmpty(countryIso) ? countryIso : GeoUtil.getCurrentCountryIso(context);
+
+ if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) {
+ details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE);
+ }
+
+ return details;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Delete specified calls from the call log.
+ *
+ * @param context The context.
+ * @param callIds String of the callIds to delete from the call log, delimited by commas (",").
+ * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted.
+ */
+ public static void deleteCalls(
+ @NonNull final Context context,
+ final String callIds,
+ @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) {
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(
+ Tasks.DELETE_CALL,
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ context
+ .getContentResolver()
+ .delete(
+ TelecomUtil.getCallLogUri(context),
+ CallLog.Calls._ID + " IN (" + callIds + ")",
+ null);
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Void result) {
+ if (callLogAsyncTaskListener != null) {
+ callLogAsyncTaskListener.onDeleteCall();
+ }
+ }
+ });
+ }
+
+ public static void markVoicemailAsRead(
+ @NonNull final Context context, @NonNull final Uri voicemailUri) {
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(
+ Tasks.MARK_VOICEMAIL_READ,
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ ContentValues values = new ContentValues();
+ values.put(Voicemails.IS_READ, true);
+ context
+ .getContentResolver()
+ .update(voicemailUri, values, Voicemails.IS_READ + " = 0", null);
+
+ Intent intent = new Intent(context, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+ context.startService(intent);
+ return null;
+ }
+ });
+ }
+
+ public static void deleteVoicemail(
+ @NonNull final Context context,
+ final Uri voicemailUri,
+ @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) {
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(
+ Tasks.DELETE_VOICEMAIL,
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ context.getContentResolver().delete(voicemailUri, null, null);
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Void result) {
+ if (callLogAsyncTaskListener != null) {
+ callLogAsyncTaskListener.onDeleteVoicemail();
+ }
+ }
+ });
+ }
+
+ public static void markCallAsRead(@NonNull final Context context, @NonNull final long[] callIds) {
+ if (!PermissionsUtil.hasPhonePermissions(context)) {
+ return;
+ }
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(
+ Tasks.MARK_CALL_READ,
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+
+ StringBuilder where = new StringBuilder();
+ where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE);
+ where.append(" AND ");
+
+ Long[] callIdLongs = new Long[callIds.length];
+ for (int i = 0; i < callIds.length; i++) {
+ callIdLongs[i] = callIds[i];
+ }
+ where
+ .append(CallLog.Calls._ID)
+ .append(" IN (" + TextUtils.join(",", callIdLongs) + ")");
+
+ ContentValues values = new ContentValues(1);
+ values.put(CallLog.Calls.IS_READ, "1");
+ context
+ .getContentResolver()
+ .update(CallLog.Calls.CONTENT_URI, values, where.toString(), null);
+ return null;
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public static void resetForTest() {
+ sAsyncTaskExecutor = null;
+ }
+
+ /** The enumeration of {@link AsyncTask} objects used in this class. */
+ public enum Tasks {
+ DELETE_VOICEMAIL,
+ DELETE_CALL,
+ MARK_VOICEMAIL_READ,
+ MARK_CALL_READ,
+ GET_CALL_DETAILS,
+ UPDATE_DURATION,
+ }
+
+ public interface CallLogAsyncTaskListener {
+
+ void onDeleteCall();
+
+ void onDeleteVoicemail();
+
+ void onGetCallDetails(PhoneCallDetails[] details);
+ }
+
+ private static final class CallDetailQuery {
+
+ public static final String[] CALL_LOG_PROJECTION;
+ static final int DATE_COLUMN_INDEX = 0;
+ static final int DURATION_COLUMN_INDEX = 1;
+ static final int NUMBER_COLUMN_INDEX = 2;
+ static final int CALL_TYPE_COLUMN_INDEX = 3;
+ static final int COUNTRY_ISO_COLUMN_INDEX = 4;
+ static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
+ static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6;
+ static final int ACCOUNT_COMPONENT_NAME = 7;
+ static final int ACCOUNT_ID = 8;
+ static final int FEATURES = 9;
+ static final int DATA_USAGE = 10;
+ static final int TRANSCRIPTION_COLUMN_INDEX = 11;
+ static final int POST_DIAL_DIGITS = 12;
+ static final int VIA_NUMBER = 13;
+ private static final String[] CALL_LOG_PROJECTION_INTERNAL =
+ new String[] {
+ CallLog.Calls.DATE,
+ CallLog.Calls.DURATION,
+ CallLog.Calls.NUMBER,
+ CallLog.Calls.TYPE,
+ CallLog.Calls.COUNTRY_ISO,
+ CallLog.Calls.GEOCODED_LOCATION,
+ CallLog.Calls.NUMBER_PRESENTATION,
+ CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
+ CallLog.Calls.PHONE_ACCOUNT_ID,
+ CallLog.Calls.FEATURES,
+ CallLog.Calls.DATA_USAGE,
+ CallLog.Calls.TRANSCRIPTION
+ };
+
+ static {
+ ArrayList<String> projectionList = new ArrayList<>();
+ projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL));
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ projectionList.add(CallLog.Calls.POST_DIAL_DIGITS);
+ projectionList.add(CallLog.Calls.VIA_NUMBER);
+ }
+ projectionList.trimToSize();
+ CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogFragment.java b/java/com/android/dialer/app/calllog/CallLogFragment.java
new file mode 100644
index 0000000..1ae68cd
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogFragment.java
@@ -0,0 +1,528 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import static android.Manifest.permission.READ_CALL_LOG;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.KeyguardManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
+import android.support.v13.app.FragmentCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.app.Bindings;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.app.contactinfo.ContactInfoCache;
+import com.android.dialer.app.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
+import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment;
+import com.android.dialer.app.list.ListsFragment;
+import com.android.dialer.app.list.ListsFragment.ListsPage;
+import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.app.widget.EmptyContentView;
+import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.CallLogQueryHandler;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.util.PermissionsUtil;
+
+/**
+ * Displays a list of call log entries. To filter for a particular kind of call (all, missed or
+ * voicemails), specify it in the constructor.
+ */
+public class CallLogFragment extends Fragment
+ implements ListsPage,
+ CallLogQueryHandler.Listener,
+ CallLogAdapter.CallFetcher,
+ OnEmptyViewActionButtonClickedListener,
+ FragmentCompat.OnRequestPermissionsResultCallback,
+ CallLogModalAlertManager.Listener {
+ private static final String KEY_FILTER_TYPE = "filter_type";
+ private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission";
+ private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required";
+
+ private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
+
+ private static final int EVENT_UPDATE_DISPLAY = 1;
+
+ private static final long MILLIS_IN_MINUTE = 60 * 1000;
+ private final Handler mHandler = new Handler();
+ // See issue 6363009
+ private final ContentObserver mCallLogObserver = new CustomContentObserver();
+ private final ContentObserver mContactsObserver = new CustomContentObserver();
+ private RecyclerView mRecyclerView;
+ private LinearLayoutManager mLayoutManager;
+ private CallLogAdapter mAdapter;
+ private CallLogQueryHandler mCallLogQueryHandler;
+ private boolean mScrollToTop;
+ private EmptyContentView mEmptyListView;
+ private KeyguardManager mKeyguardManager;
+ private ContactInfoCache mContactInfoCache;
+ private final OnContactInfoChangedListener mOnContactInfoChangedListener =
+ new OnContactInfoChangedListener() {
+ @Override
+ public void onContactInfoChanged() {
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+ private boolean mRefreshDataRequired;
+ private boolean mHasReadCallLogPermission;
+ // Exactly same variable is in Fragment as a package private.
+ private boolean mMenuVisible = true;
+ // Default to all calls.
+ protected int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
+
+ private final Handler mDisplayUpdateHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_UPDATE_DISPLAY:
+ refreshData();
+ rescheduleDisplayUpdate();
+ break;
+ }
+ }
+ };
+ protected CallLogModalAlertManager mModalAlertManager;
+ private ViewGroup mModalAlertView;
+
+ @Override
+ public void onCreate(Bundle state) {
+ LogUtil.d("CallLogFragment.onCreate", toString());
+ super.onCreate(state);
+ mRefreshDataRequired = true;
+ if (state != null) {
+ mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
+ mHasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false);
+ mRefreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired);
+ }
+
+ final Activity activity = getActivity();
+ final ContentResolver resolver = activity.getContentResolver();
+ mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this);
+ mKeyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
+ resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
+ resolver.registerContentObserver(
+ ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
+ setHasOptionsMenu(true);
+ }
+
+ /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
+ @Override
+ public boolean onCallsFetched(Cursor cursor) {
+ if (getActivity() == null || getActivity().isFinishing()) {
+ // Return false; we did not take ownership of the cursor
+ return false;
+ }
+ mAdapter.invalidatePositions();
+ mAdapter.setLoading(false);
+ mAdapter.changeCursor(cursor);
+ // This will update the state of the "Clear call log" menu item.
+ getActivity().invalidateOptionsMenu();
+
+ if (cursor != null && cursor.getCount() > 0) {
+ mRecyclerView.setPaddingRelative(
+ mRecyclerView.getPaddingStart(),
+ 0,
+ mRecyclerView.getPaddingEnd(),
+ getResources().getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding));
+ mEmptyListView.setVisibility(View.GONE);
+ } else {
+ mRecyclerView.setPaddingRelative(
+ mRecyclerView.getPaddingStart(), 0, mRecyclerView.getPaddingEnd(), 0);
+ mEmptyListView.setVisibility(View.VISIBLE);
+ }
+ if (mScrollToTop) {
+ // The smooth-scroll animation happens over a fixed time period.
+ // As a result, if it scrolls through a large portion of the list,
+ // each frame will jump so far from the previous one that the user
+ // will not experience the illusion of downward motion. Instead,
+ // if we're not already near the top of the list, we instantly jump
+ // near the top, and animate from there.
+ if (mLayoutManager.findFirstVisibleItemPosition() > 5) {
+ // TODO: Jump to near the top, then begin smooth scroll.
+ mRecyclerView.smoothScrollToPosition(0);
+ }
+ // Workaround for framework issue: the smooth-scroll doesn't
+ // occur if setSelection() is called immediately before.
+ mHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || getActivity().isFinishing()) {
+ return;
+ }
+ mRecyclerView.smoothScrollToPosition(0);
+ }
+ });
+
+ mScrollToTop = false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {}
+
+ @Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {}
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {}
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ View view = inflater.inflate(R.layout.call_log_fragment, container, false);
+ setupView(view);
+ return view;
+ }
+
+ protected void setupView(View view) {
+ mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
+ mRecyclerView.setHasFixedSize(true);
+ mLayoutManager = new LinearLayoutManager(getActivity());
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
+ mEmptyListView.setImage(R.drawable.empty_call_log);
+ mEmptyListView.setActionClickedListener(this);
+ mModalAlertView = (ViewGroup) view.findViewById(R.id.modal_message_container);
+ mModalAlertManager =
+ new CallLogModalAlertManager(LayoutInflater.from(getContext()), mModalAlertView, this);
+ }
+
+ protected void setupData() {
+ int activityType = CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
+ String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
+
+ mContactInfoCache =
+ new ContactInfoCache(
+ ExpirableCacheHeadlessFragment.attach((AppCompatActivity) getActivity())
+ .getRetainedCache(),
+ new ContactInfoHelper(getActivity(), currentCountryIso),
+ mOnContactInfoChangedListener);
+ mAdapter =
+ Bindings.getLegacy(getActivity())
+ .newCallLogAdapter(
+ getActivity(),
+ mRecyclerView,
+ this,
+ CallLogCache.getCallLogCache(getActivity()),
+ mContactInfoCache,
+ getVoicemailPlaybackPresenter(),
+ activityType);
+ mRecyclerView.setAdapter(mAdapter);
+ fetchCalls();
+ }
+
+ @Nullable
+ protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() {
+ return null;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ setupData();
+ mAdapter.onRestoreInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ updateEmptyMessage(mCallTypeFilter);
+ }
+
+ @Override
+ public void onResume() {
+ LogUtil.d("CallLogFragment.onResume", toString());
+ super.onResume();
+ final boolean hasReadCallLogPermission =
+ PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG);
+ if (!mHasReadCallLogPermission && hasReadCallLogPermission) {
+ // We didn't have the permission before, and now we do. Force a refresh of the call log.
+ // Note that this code path always happens on a fresh start, but mRefreshDataRequired
+ // is already true in that case anyway.
+ mRefreshDataRequired = true;
+ updateEmptyMessage(mCallTypeFilter);
+ }
+
+ mHasReadCallLogPermission = hasReadCallLogPermission;
+
+ /*
+ * Always clear the filtered numbers cache since users could have blocked/unblocked numbers
+ * from the settings page
+ */
+ mAdapter.clearFilteredNumbersCache();
+ refreshData();
+ mAdapter.onResume();
+
+ rescheduleDisplayUpdate();
+ }
+
+ @Override
+ public void onPause() {
+ LogUtil.d("CallLogFragment.onPause", toString());
+ cancelDisplayUpdate();
+ mAdapter.onPause();
+ super.onPause();
+ }
+
+ @Override
+ public void onStop() {
+ updateOnTransition();
+
+ super.onStop();
+ mAdapter.onStop();
+ }
+
+ @Override
+ public void onDestroy() {
+ LogUtil.d("CallLogFragment.onDestroy", toString());
+ mAdapter.changeCursor(null);
+
+ getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
+ getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
+ outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, mHasReadCallLogPermission);
+ outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired);
+
+ mContactInfoCache.stop();
+
+ mAdapter.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void fetchCalls() {
+ mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
+ ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+ }
+
+ private void updateEmptyMessage(int filterType) {
+ final Context context = getActivity();
+ if (context == null) {
+ return;
+ }
+
+ if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
+ mEmptyListView.setDescription(R.string.permission_no_calllog);
+ mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
+ return;
+ }
+
+ final int messageId;
+ switch (filterType) {
+ case Calls.MISSED_TYPE:
+ messageId = R.string.call_log_missed_empty;
+ break;
+ case Calls.VOICEMAIL_TYPE:
+ messageId = R.string.call_log_voicemail_empty;
+ break;
+ case CallLogQueryHandler.CALL_TYPE_ALL:
+ messageId = R.string.call_log_all_empty;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected filter type in CallLogFragment: " + filterType);
+ }
+ mEmptyListView.setDescription(messageId);
+ if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
+ mEmptyListView.setActionLabel(R.string.call_log_all_empty_action);
+ }
+ }
+
+ public CallLogAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public void setMenuVisibility(boolean menuVisible) {
+ super.setMenuVisibility(menuVisible);
+ if (mMenuVisible != menuVisible) {
+ mMenuVisible = menuVisible;
+ if (!menuVisible) {
+ updateOnTransition();
+ } else if (isResumed()) {
+ refreshData();
+ }
+ }
+ }
+
+ /** Requests updates to the data to be shown. */
+ private void refreshData() {
+ // Prevent unnecessary refresh.
+ if (mRefreshDataRequired) {
+ // Mark all entries in the contact info cache as out of date, so they will be looked up
+ // again once being shown.
+ mContactInfoCache.invalidate();
+ mAdapter.setLoading(true);
+
+ fetchCalls();
+ mCallLogQueryHandler.fetchVoicemailStatus();
+ mCallLogQueryHandler.fetchMissedCallsUnreadCount();
+ updateOnTransition();
+ mRefreshDataRequired = false;
+ } else {
+ // Refresh the display of the existing data to update the timestamp text descriptions.
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Updates the voicemail notification state.
+ *
+ * <p>TODO: Move to CallLogActivity
+ */
+ private void updateOnTransition() {
+ // We don't want to update any call data when keyguard is on because the user has likely not
+ // seen the new calls yet.
+ // This might be called before onCreate() and thus we need to check null explicitly.
+ if (mKeyguardManager != null
+ && !mKeyguardManager.inKeyguardRestrictedInputMode()
+ && mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
+ CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
+ }
+ }
+
+ @Override
+ public void onEmptyViewActionButtonClicked() {
+ final Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
+ FragmentCompat.requestPermissions(
+ this, new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE);
+ } else {
+ ((HostInterface) activity).showDialpad();
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+ // Force a refresh of the data since we were missing the permission before this.
+ mRefreshDataRequired = true;
+ }
+ }
+ }
+
+ /** Schedules an update to the relative call times (X mins ago). */
+ private void rescheduleDisplayUpdate() {
+ if (!mDisplayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) {
+ long time = System.currentTimeMillis();
+ // This value allows us to change the display relatively close to when the time changes
+ // from one minute to the next.
+ long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE);
+ mDisplayUpdateHandler.sendEmptyMessageDelayed(EVENT_UPDATE_DISPLAY, millisUtilNextMinute);
+ }
+ }
+
+ /** Cancels any pending update requests to update the relative call times (X mins ago). */
+ private void cancelDisplayUpdate() {
+ mDisplayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY);
+ }
+
+ @Override
+ @CallSuper
+ public void onPageResume(@Nullable Activity activity) {
+ LogUtil.d("CallLogFragment.onPageResume", "frag: %s", this);
+ if (activity != null) {
+ ((HostInterface) activity)
+ .enableFloatingButton(mModalAlertManager == null || mModalAlertManager.isEmpty());
+ }
+ }
+
+ @Override
+ @CallSuper
+ public void onPagePause(@Nullable Activity activity) {
+ LogUtil.d("CallLogFragment.onPagePause", "frag: %s", this);
+ }
+
+ @Override
+ public void onShowModalAlert(boolean show) {
+ LogUtil.d(
+ "CallLogFragment.onShowModalAlert",
+ "show: %b, fragment: %s, isVisible: %b",
+ show,
+ this,
+ getUserVisibleHint());
+ getAdapter().notifyDataSetChanged();
+ HostInterface hostInterface = (HostInterface) getActivity();
+ if (show) {
+ mRecyclerView.setVisibility(View.GONE);
+ mModalAlertView.setVisibility(View.VISIBLE);
+ if (hostInterface != null && getUserVisibleHint()) {
+ hostInterface.enableFloatingButton(false);
+ }
+ } else {
+ mRecyclerView.setVisibility(View.VISIBLE);
+ mModalAlertView.setVisibility(View.GONE);
+ if (hostInterface != null && getUserVisibleHint()) {
+ hostInterface.enableFloatingButton(true);
+ }
+ }
+ }
+
+ public interface HostInterface {
+
+ void showDialpad();
+
+ void enableFloatingButton(boolean enabled);
+ }
+
+ protected class CustomContentObserver extends ContentObserver {
+
+ public CustomContentObserver() {
+ super(mHandler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mRefreshDataRequired = true;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
new file mode 100644
index 0000000..45ff378
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.database.Cursor;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.text.format.Time;
+import com.android.contacts.common.util.DateUtils;
+import com.android.dialer.compat.AppCompatConstants;
+import com.android.dialer.phonenumbercache.CallLogQuery;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import java.util.Objects;
+
+/**
+ * Groups together calls in the call log. The primary grouping attempts to group together calls to
+ * and from the same number into a single row on the call log. A secondary grouping assigns calls,
+ * grouped via the primary grouping, to "day groups". The day groups provide a means of identifying
+ * the calls which occurred "Today", "Yesterday", "Last week", or "Other".
+ *
+ * <p>This class is meant to be used in conjunction with {@link GroupingListAdapter}.
+ */
+public class CallLogGroupBuilder {
+
+ /**
+ * Day grouping for call log entries used to represent no associated day group. Used primarily
+ * when retrieving the previous day group, but there is no previous day group (i.e. we are at the
+ * start of the list).
+ */
+ public static final int DAY_GROUP_NONE = -1;
+ /** Day grouping for calls which occurred today. */
+ public static final int DAY_GROUP_TODAY = 0;
+ /** Day grouping for calls which occurred yesterday. */
+ public static final int DAY_GROUP_YESTERDAY = 1;
+ /** Day grouping for calls which occurred before last week. */
+ public static final int DAY_GROUP_OTHER = 2;
+ /** Instance of the time object used for time calculations. */
+ private static final Time TIME = new Time();
+ /** The object on which the groups are created. */
+ private final GroupCreator mGroupCreator;
+
+ public CallLogGroupBuilder(GroupCreator groupCreator) {
+ mGroupCreator = groupCreator;
+ }
+
+ /**
+ * Finds all groups of adjacent entries in the call log which should be grouped together and calls
+ * {@link GroupCreator#addGroup(int, int)} on {@link #mGroupCreator} for each of them.
+ *
+ * <p>For entries that are not grouped with others, we do not need to create a group of size one.
+ *
+ * <p>It assumes that the cursor will not change during its execution.
+ *
+ * @see GroupingListAdapter#addGroups(Cursor)
+ */
+ public void addGroups(Cursor cursor) {
+ final int count = cursor.getCount();
+ if (count == 0) {
+ return;
+ }
+
+ // Clear any previous day grouping information.
+ mGroupCreator.clearDayGroups();
+
+ // Get current system time, used for calculating which day group calls belong to.
+ long currentTime = System.currentTimeMillis();
+ cursor.moveToFirst();
+
+ // Determine the day group for the first call in the cursor.
+ final long firstDate = cursor.getLong(CallLogQuery.DATE);
+ final long firstRowId = cursor.getLong(CallLogQuery.ID);
+ int groupDayGroup = getDayGroup(firstDate, currentTime);
+ mGroupCreator.setDayGroup(firstRowId, groupDayGroup);
+
+ // Instantiate the group values to those of the first call in the cursor.
+ String groupNumber = cursor.getString(CallLogQuery.NUMBER);
+ String groupPostDialDigits =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ String groupViaNumbers =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : "";
+ int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+ String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+ int groupSize = 1;
+
+ String number;
+ String numberPostDialDigits;
+ String numberViaNumbers;
+ int callType;
+ String accountComponentName;
+ String accountId;
+
+ while (cursor.moveToNext()) {
+ // Obtain the values for the current call to group.
+ number = cursor.getString(CallLogQuery.NUMBER);
+ numberPostDialDigits =
+ (VERSION.SDK_INT >= VERSION_CODES.N)
+ ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS)
+ : "";
+ numberViaNumbers =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : "";
+ callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+ accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+
+ final boolean isSameNumber = equalNumbers(groupNumber, number);
+ final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits);
+ final boolean isSameViaNumbers = groupViaNumbers.equals(numberViaNumbers);
+ final boolean isSameAccount =
+ isSameAccount(groupAccountComponentName, accountComponentName, groupAccountId, accountId);
+
+ // Group with the same number and account. Never group voicemails. Only group blocked
+ // calls with other blocked calls.
+ if (isSameNumber
+ && isSameAccount
+ && isSamePostDialDigits
+ && isSameViaNumbers
+ && areBothNotVoicemail(callType, groupCallType)
+ && (areBothNotBlocked(callType, groupCallType)
+ || areBothBlocked(callType, groupCallType))) {
+ // Increment the size of the group to include the current call, but do not create
+ // the group until finding a call that does not match.
+ groupSize++;
+ } else {
+ // The call group has changed. Determine the day group for the new call group.
+ final long date = cursor.getLong(CallLogQuery.DATE);
+ groupDayGroup = getDayGroup(date, currentTime);
+
+ // Create a group for the previous group of calls, which does not include the
+ // current call.
+ mGroupCreator.addGroup(cursor.getPosition() - groupSize, groupSize);
+
+ // Start a new group; it will include at least the current call.
+ groupSize = 1;
+
+ // Update the group values to those of the current call.
+ groupNumber = number;
+ groupPostDialDigits = numberPostDialDigits;
+ groupViaNumbers = numberViaNumbers;
+ groupCallType = callType;
+ groupAccountComponentName = accountComponentName;
+ groupAccountId = accountId;
+ }
+
+ // Save the day group associated with the current call.
+ final long currentCallId = cursor.getLong(CallLogQuery.ID);
+ mGroupCreator.setDayGroup(currentCallId, groupDayGroup);
+ }
+
+ // Create a group for the last set of calls.
+ mGroupCreator.addGroup(count - groupSize, groupSize);
+ }
+
+ @VisibleForTesting
+ boolean equalNumbers(@Nullable String number1, @Nullable String number2) {
+ if (PhoneNumberHelper.isUriNumber(number1) || PhoneNumberHelper.isUriNumber(number2)) {
+ return compareSipAddresses(number1, number2);
+ } else {
+ return PhoneNumberUtils.compare(number1, number2);
+ }
+ }
+
+ private boolean isSameAccount(String name1, String name2, String id1, String id2) {
+ return TextUtils.equals(name1, name2) && TextUtils.equals(id1, id2);
+ }
+
+ @VisibleForTesting
+ boolean compareSipAddresses(@Nullable String number1, @Nullable String number2) {
+ if (number1 == null || number2 == null) {
+ return Objects.equals(number1, number2);
+ }
+
+ int index1 = number1.indexOf('@');
+ final String userinfo1;
+ final String rest1;
+ if (index1 != -1) {
+ userinfo1 = number1.substring(0, index1);
+ rest1 = number1.substring(index1);
+ } else {
+ userinfo1 = number1;
+ rest1 = "";
+ }
+
+ int index2 = number2.indexOf('@');
+ final String userinfo2;
+ final String rest2;
+ if (index2 != -1) {
+ userinfo2 = number2.substring(0, index2);
+ rest2 = number2.substring(index2);
+ } else {
+ userinfo2 = number2;
+ rest2 = "";
+ }
+
+ return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2);
+ }
+
+ /**
+ * Given a call date and the current date, determine which date group the call belongs in.
+ *
+ * @param date The call date.
+ * @param now The current date.
+ * @return The date group the call belongs in.
+ */
+ private int getDayGroup(long date, long now) {
+ int days = DateUtils.getDayDifference(TIME, date, now);
+
+ if (days == 0) {
+ return DAY_GROUP_TODAY;
+ } else if (days == 1) {
+ return DAY_GROUP_YESTERDAY;
+ } else {
+ return DAY_GROUP_OTHER;
+ }
+ }
+
+ private boolean areBothNotVoicemail(int callType, int groupCallType) {
+ return callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE
+ && groupCallType != AppCompatConstants.CALLS_VOICEMAIL_TYPE;
+ }
+
+ private boolean areBothNotBlocked(int callType, int groupCallType) {
+ return callType != AppCompatConstants.CALLS_BLOCKED_TYPE
+ && groupCallType != AppCompatConstants.CALLS_BLOCKED_TYPE;
+ }
+
+ private boolean areBothBlocked(int callType, int groupCallType) {
+ return callType == AppCompatConstants.CALLS_BLOCKED_TYPE
+ && groupCallType == AppCompatConstants.CALLS_BLOCKED_TYPE;
+ }
+
+ public interface GroupCreator {
+
+ /**
+ * Defines the interface for adding a group to the call log. The primary group for a call log
+ * groups the calls together based on the number which was dialed.
+ *
+ * @param cursorPosition The starting position of the group in the cursor.
+ * @param size The size of the group.
+ */
+ void addGroup(int cursorPosition, int size);
+
+ /**
+ * Defines the interface for tracking the day group each call belongs to. Calls in a call group
+ * are assigned the same day group as the first call in the group. The day group assigns calls
+ * to the buckets: Today, Yesterday, Last week, and Other
+ *
+ * @param rowId The row Id of the current call.
+ * @param dayGroup The day group the call belongs in.
+ */
+ void setDayGroup(long rowId, int dayGroup);
+
+ /** Defines the interface for clearing the day groupings information on rebind/regroup. */
+ void clearDayGroups();
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
new file mode 100644
index 0000000..ea2119c
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.res.Resources;
+import android.provider.CallLog.Calls;
+import android.support.annotation.WorkerThread;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.common.Assert;
+import com.android.dialer.compat.AppCompatConstants;
+
+/** Helper class to fill in the views of a call log entry. */
+/* package */ class CallLogListItemHelper {
+
+ private static final String TAG = "CallLogListItemHelper";
+
+ /** Helper for populating the details of a phone call. */
+ private final PhoneCallDetailsHelper mPhoneCallDetailsHelper;
+ /** Resources to look up strings. */
+ private final Resources mResources;
+
+ private final CallLogCache mCallLogCache;
+
+ /**
+ * Creates a new helper instance.
+ *
+ * @param phoneCallDetailsHelper used to set the details of a phone call
+ * @param resources The object from which resources can be retrieved
+ * @param callLogCache A cache for values retrieved from telecom/telephony
+ */
+ public CallLogListItemHelper(
+ PhoneCallDetailsHelper phoneCallDetailsHelper,
+ Resources resources,
+ CallLogCache callLogCache) {
+ mPhoneCallDetailsHelper = phoneCallDetailsHelper;
+ mResources = resources;
+ mCallLogCache = callLogCache;
+ }
+
+ /**
+ * Update phone call details. This is called before any drawing to avoid expensive operation on UI
+ * thread.
+ *
+ * @param details
+ */
+ @WorkerThread
+ public void updatePhoneCallDetails(PhoneCallDetails details) {
+ Assert.isWorkerThread();
+ details.callLocationAndDate = mPhoneCallDetailsHelper.getCallLocationAndDate(details);
+ details.callDescription = getCallDescription(details);
+ }
+
+ /**
+ * Sets the name, label, and number for a contact.
+ *
+ * @param views the views to populate
+ * @param details the details of a phone call needed to fill in the data
+ */
+ public void setPhoneCallDetails(CallLogListItemViewHolder views, PhoneCallDetails details) {
+ mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details);
+
+ // Set the accessibility text for the contact badge
+ views.quickContactView.setContentDescription(getContactBadgeDescription(details));
+
+ // Set the primary action accessibility description
+ views.primaryActionView.setContentDescription(details.callDescription);
+
+ // Cache name or number of caller. Used when setting the content descriptions of buttons
+ // when the actions ViewStub is inflated.
+ views.nameOrNumber = getNameOrNumber(details);
+
+ // The call type or Location associated with the call. Use when setting text for a
+ // voicemail log's call button
+ views.callTypeOrLocation = mPhoneCallDetailsHelper.getCallTypeOrLocation(details);
+
+ // Cache country iso. Used for number filtering.
+ views.countryIso = details.countryIso;
+
+ views.updatePhoto();
+ }
+
+ /**
+ * Sets the accessibility descriptions for the action buttons in the action button ViewStub.
+ *
+ * @param views The views associated with the current call log entry.
+ */
+ public void setActionContentDescriptions(CallLogListItemViewHolder views) {
+ if (views.nameOrNumber == null) {
+ Log.e(TAG, "setActionContentDescriptions; name or number is null.");
+ }
+
+ // Calling expandTemplate with a null parameter will cause a NullPointerException.
+ // Although we don't expect a null name or number, it is best to protect against it.
+ CharSequence nameOrNumber = views.nameOrNumber == null ? "" : views.nameOrNumber;
+
+ views.videoCallButtonView.setContentDescription(
+ TextUtils.expandTemplate(
+ mResources.getString(R.string.description_video_call_action), nameOrNumber));
+
+ views.createNewContactButtonView.setContentDescription(
+ TextUtils.expandTemplate(
+ mResources.getString(R.string.description_create_new_contact_action), nameOrNumber));
+
+ views.addToExistingContactButtonView.setContentDescription(
+ TextUtils.expandTemplate(
+ mResources.getString(R.string.description_add_to_existing_contact_action),
+ nameOrNumber));
+
+ views.detailsButtonView.setContentDescription(
+ TextUtils.expandTemplate(
+ mResources.getString(R.string.description_details_action), nameOrNumber));
+ }
+
+ /**
+ * Returns the accessibility description for the contact badge for a call log entry.
+ *
+ * @param details Details of call.
+ * @return Accessibility description.
+ */
+ private CharSequence getContactBadgeDescription(PhoneCallDetails details) {
+ if (details.isSpam) {
+ return mResources.getString(
+ R.string.description_spam_contact_details, getNameOrNumber(details));
+ }
+ return mResources.getString(R.string.description_contact_details, getNameOrNumber(details));
+ }
+
+ /**
+ * Returns the accessibility description of the "return call/call" action for a call log entry.
+ * Accessibility text is a combination of: {Voicemail Prefix}. {Number of Calls}. {Caller
+ * information} {Phone Account}. If most recent call is a voicemail, {Voicemail Prefix} is "New
+ * Voicemail.", otherwise "".
+ *
+ * <p>If more than one call for the caller, {Number of Calls} is: "{number of calls} calls.",
+ * otherwise "".
+ *
+ * <p>The {Caller Information} references the most recent call associated with the caller. For
+ * incoming calls: If missed call: Missed call from {Name/Number} {Call Type} {Call Time}. If
+ * answered call: Answered call from {Name/Number} {Call Type} {Call Time}.
+ *
+ * <p>For outgoing calls: If outgoing: Call to {Name/Number] {Call Type} {Call Time}.
+ *
+ * <p>Where: {Name/Number} is the name or number of the caller (as shown in call log). {Call type}
+ * is the contact phone number type (eg mobile) or location. {Call Time} is the time since the
+ * last call for the contact occurred.
+ *
+ * <p>The {Phone Account} refers to the account/SIM through which the call was placed or received
+ * in multi-SIM devices.
+ *
+ * <p>Examples: 3 calls. New Voicemail. Missed call from Joe Smith mobile 2 hours ago on SIM 1.
+ *
+ * <p>2 calls. Answered call from John Doe mobile 1 hour ago.
+ *
+ * @param context The application context.
+ * @param details Details of call.
+ * @return Return call action description.
+ */
+ public CharSequence getCallDescription(PhoneCallDetails details) {
+ // Get the name or number of the caller.
+ final CharSequence nameOrNumber = getNameOrNumber(details);
+
+ // Get the call type or location of the caller; null if not applicable
+ final CharSequence typeOrLocation = mPhoneCallDetailsHelper.getCallTypeOrLocation(details);
+
+ // Get the time/date of the call
+ final CharSequence timeOfCall = mPhoneCallDetailsHelper.getCallDate(details);
+
+ SpannableStringBuilder callDescription = new SpannableStringBuilder();
+
+ // Add number of calls if more than one.
+ if (details.callTypes.length > 1) {
+ callDescription.append(
+ mResources.getString(R.string.description_num_calls, details.callTypes.length));
+ }
+
+ // If call had video capabilities, add the "Video Call" string.
+ if ((details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
+ callDescription.append(mResources.getString(R.string.description_video_call));
+ }
+
+ String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle);
+ CharSequence onAccountLabel =
+ PhoneCallDetails.createAccountLabelDescription(mResources, details.viaNumber, accountLabel);
+
+ int stringID = getCallDescriptionStringID(details.callTypes, details.isRead);
+ callDescription.append(
+ TextUtils.expandTemplate(
+ mResources.getString(stringID),
+ nameOrNumber,
+ typeOrLocation == null ? "" : typeOrLocation,
+ timeOfCall,
+ onAccountLabel));
+
+ return callDescription;
+ }
+
+ /**
+ * Determine the appropriate string ID to describe a call for accessibility purposes.
+ *
+ * @param callTypes The type of call corresponding to this entry or multiple if this entry
+ * represents multiple calls grouped together.
+ * @param isRead If the entry is a voicemail, {@code true} if the voicemail is read.
+ * @return String resource ID to use.
+ */
+ public int getCallDescriptionStringID(int[] callTypes, boolean isRead) {
+ int lastCallType = getLastCallType(callTypes);
+ int stringID;
+
+ if (lastCallType == AppCompatConstants.CALLS_MISSED_TYPE) {
+ //Message: Missed call from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
+ //<PhoneAccount>.
+ stringID = R.string.description_incoming_missed_call;
+ } else if (lastCallType == AppCompatConstants.CALLS_INCOMING_TYPE) {
+ //Message: Answered call from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
+ //<PhoneAccount>.
+ stringID = R.string.description_incoming_answered_call;
+ } else if (lastCallType == AppCompatConstants.CALLS_VOICEMAIL_TYPE) {
+ //Message: (Unread) [V/v]oicemail from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
+ //<PhoneAccount>.
+ stringID =
+ isRead ? R.string.description_read_voicemail : R.string.description_unread_voicemail;
+ } else {
+ //Message: Call to <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>, <PhoneAccount>.
+ stringID = R.string.description_outgoing_call;
+ }
+ return stringID;
+ }
+
+ /**
+ * Determine the call type for the most recent call.
+ *
+ * @param callTypes Call types to check.
+ * @return Call type.
+ */
+ private int getLastCallType(int[] callTypes) {
+ if (callTypes.length > 0) {
+ return callTypes[0];
+ } else {
+ return Calls.MISSED_TYPE;
+ }
+ }
+
+ /**
+ * Return the name or number of the caller specified by the details.
+ *
+ * @param details Call details
+ * @return the name (if known) of the caller, otherwise the formatted number.
+ */
+ private CharSequence getNameOrNumber(PhoneCallDetails details) {
+ final CharSequence recipient;
+ if (!TextUtils.isEmpty(details.getPreferredName())) {
+ recipient = details.getPreferredName();
+ } else {
+ recipient = details.displayNumber + details.postDialDigits;
+ }
+ return recipient;
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
new file mode 100644
index 0000000..6abd360
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -0,0 +1,966 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewStub;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import com.android.contacts.common.ClipboardUtils;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.dialog.CallSubjectDialog;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.app.voicemail.VoicemailPlaybackLayout;
+import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.blocking.BlockedNumbersMigrator;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.callcomposer.CallComposerActivity;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+
+/**
+ * This is an object containing references to views contained by the call log list item. This
+ * improves performance by reducing the frequency with which we need to find views by IDs.
+ *
+ * <p>This object also contains UI logic pertaining to the view, to isolate it from the
+ * CallLogAdapter.
+ */
+public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
+ implements View.OnClickListener,
+ MenuItem.OnMenuItemClickListener,
+ View.OnCreateContextMenuListener {
+ private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed";
+
+ /** The root view of the call log list item */
+ public final View rootView;
+ /** The quick contact badge for the contact. */
+ public final QuickContactBadge quickContactView;
+ /** The primary action view of the entry. */
+ public final View primaryActionView;
+ /** The details of the phone call. */
+ public final PhoneCallDetailsViews phoneCallDetailsViews;
+ /** The text of the header for a day grouping. */
+ public final TextView dayGroupHeader;
+ /** The view containing the details for the call log row, including the action buttons. */
+ public final CardView callLogEntryView;
+ /** The actionable view which places a call to the number corresponding to the call log row. */
+ public final ImageView primaryActionButtonView;
+
+ private final Context mContext;
+ private final CallLogCache mCallLogCache;
+ private final CallLogListItemHelper mCallLogListItemHelper;
+ private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+ private final OnClickListener mBlockReportListener;
+ private final int mPhotoSize;
+ /** Whether the data fields are populated by the worker thread, ready to be shown. */
+ public boolean isLoaded;
+ /** The view containing call log item actions. Null until the ViewStub is inflated. */
+ public View actionsView;
+ /** The button views below are assigned only when the action section is expanded. */
+ public VoicemailPlaybackLayout voicemailPlaybackView;
+
+ public View callButtonView;
+ public View videoCallButtonView;
+ public View createNewContactButtonView;
+ public View addToExistingContactButtonView;
+ public View sendMessageView;
+ public View blockReportView;
+ public View blockView;
+ public View unblockView;
+ public View reportNotSpamView;
+ public View detailsButtonView;
+ public View callWithNoteButtonView;
+ public View callComposeButtonView;
+ public View sendVoicemailButtonView;
+ public ImageView workIconView;
+ /**
+ * The row Id for the first call associated with the call log entry. Used as a key for the map
+ * used to track which call log entries have the action button section expanded.
+ */
+ public long rowId;
+ /**
+ * The call Ids for the calls represented by the current call log entry. Used when the user
+ * deletes a call log entry.
+ */
+ public long[] callIds;
+ /**
+ * The callable phone number for the current call log entry. Cached here as the call back intent
+ * is set only when the actions ViewStub is inflated.
+ */
+ public String number;
+ /** The post-dial numbers that are dialed following the phone number. */
+ public String postDialDigits;
+ /** The formatted phone number to display. */
+ public String displayNumber;
+ /**
+ * The phone number presentation for the current call log entry. Cached here as the call back
+ * intent is set only when the actions ViewStub is inflated.
+ */
+ public int numberPresentation;
+ /** The type of the phone number (e.g. main, work, etc). */
+ public String numberType;
+ /**
+ * The country iso for the call. Cached here as the call back intent is set only when the actions
+ * ViewStub is inflated.
+ */
+ public String countryIso;
+ /**
+ * The type of call for the current call log entry. Cached here as the call back intent is set
+ * only when the actions ViewStub is inflated.
+ */
+ public int callType;
+ /**
+ * ID for blocked numbers database. Set when context menu is created, if the number is blocked.
+ */
+ public Integer blockId;
+ /**
+ * The account for the current call log entry. Cached here as the call back intent is set only
+ * when the actions ViewStub is inflated.
+ */
+ public PhoneAccountHandle accountHandle;
+ /**
+ * If the call has an associated voicemail message, the URI of the voicemail message for playback.
+ * Cached here as the voicemail intent is only set when the actions ViewStub is inflated.
+ */
+ public String voicemailUri;
+ /**
+ * The name or number associated with the call. Cached here for use when setting content
+ * descriptions on buttons in the actions ViewStub when it is inflated.
+ */
+ public CharSequence nameOrNumber;
+ /**
+ * The call type or Location associated with the call. Cached here for use when setting text for a
+ * voicemail log's call button
+ */
+ public CharSequence callTypeOrLocation;
+ /** Whether this row is for a business or not. */
+ public boolean isBusiness;
+ /** The contact info for the contact displayed in this list item. */
+ public volatile ContactInfo info;
+ /** Whether spam feature is enabled, which affects UI. */
+ public boolean isSpamFeatureEnabled;
+ /** Whether the current log entry is a spam number or not. */
+ public boolean isSpam;
+
+ public boolean isCallComposerCapable;
+
+ private View.OnClickListener mExpandCollapseListener;
+ private boolean mVoicemailPrimaryActionButtonClicked;
+
+ public int dayGroupHeaderVisibility;
+ public CharSequence dayGroupHeaderText;
+ public boolean isAttachedToWindow;
+
+ public AsyncTask<Void, Void, ?> asyncTask;
+
+ private CallLogListItemViewHolder(
+ Context context,
+ OnClickListener blockReportListener,
+ View.OnClickListener expandCollapseListener,
+ CallLogCache callLogCache,
+ CallLogListItemHelper callLogListItemHelper,
+ VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ View rootView,
+ QuickContactBadge quickContactView,
+ View primaryActionView,
+ PhoneCallDetailsViews phoneCallDetailsViews,
+ CardView callLogEntryView,
+ TextView dayGroupHeader,
+ ImageView primaryActionButtonView) {
+ super(rootView);
+
+ mContext = context;
+ mExpandCollapseListener = expandCollapseListener;
+ mCallLogCache = callLogCache;
+ mCallLogListItemHelper = callLogListItemHelper;
+ mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+ mBlockReportListener = blockReportListener;
+
+ this.rootView = rootView;
+ this.quickContactView = quickContactView;
+ this.primaryActionView = primaryActionView;
+ this.phoneCallDetailsViews = phoneCallDetailsViews;
+ this.callLogEntryView = callLogEntryView;
+ this.dayGroupHeader = dayGroupHeader;
+ this.primaryActionButtonView = primaryActionButtonView;
+ this.workIconView = (ImageView) rootView.findViewById(R.id.work_profile_icon);
+ mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
+
+ // Set text height to false on the TextViews so they don't have extra padding.
+ phoneCallDetailsViews.nameView.setElegantTextHeight(false);
+ phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false);
+
+ quickContactView.setOverlay(null);
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+ }
+ primaryActionButtonView.setOnClickListener(this);
+ primaryActionView.setOnClickListener(mExpandCollapseListener);
+ primaryActionView.setOnCreateContextMenuListener(this);
+ }
+
+ public static CallLogListItemViewHolder create(
+ View view,
+ Context context,
+ OnClickListener blockReportListener,
+ View.OnClickListener expandCollapseListener,
+ CallLogCache callLogCache,
+ CallLogListItemHelper callLogListItemHelper,
+ VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+
+ return new CallLogListItemViewHolder(
+ context,
+ blockReportListener,
+ expandCollapseListener,
+ callLogCache,
+ callLogListItemHelper,
+ voicemailPlaybackPresenter,
+ view,
+ (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
+ view.findViewById(R.id.primary_action_view),
+ PhoneCallDetailsViews.fromView(view),
+ (CardView) view.findViewById(R.id.call_log_row),
+ (TextView) view.findViewById(R.id.call_log_day_group_label),
+ (ImageView) view.findViewById(R.id.primary_action_button));
+ }
+
+ public static CallLogListItemViewHolder createForTest(Context context) {
+ Resources resources = context.getResources();
+ CallLogCache callLogCache = CallLogCache.getCallLogCache(context);
+ PhoneCallDetailsHelper phoneCallDetailsHelper =
+ new PhoneCallDetailsHelper(context, resources, callLogCache);
+
+ CallLogListItemViewHolder viewHolder =
+ new CallLogListItemViewHolder(
+ context,
+ null,
+ null /* expandCollapseListener */,
+ callLogCache,
+ new CallLogListItemHelper(phoneCallDetailsHelper, resources, callLogCache),
+ null /* voicemailPlaybackPresenter */,
+ new View(context),
+ new QuickContactBadge(context),
+ new View(context),
+ PhoneCallDetailsViews.createForTest(context),
+ new CardView(context),
+ new TextView(context),
+ new ImageView(context));
+ viewHolder.detailsButtonView = new TextView(context);
+ viewHolder.actionsView = new View(context);
+ viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context);
+ viewHolder.workIconView = new ImageButton(context);
+ return viewHolder;
+ }
+
+ @Override
+ public void onCreateContextMenu(
+ final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ if (TextUtils.isEmpty(number)) {
+ return;
+ }
+
+ if (callType == CallLog.Calls.VOICEMAIL_TYPE) {
+ menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail));
+ } else {
+ menu.setHeaderTitle(
+ PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance().unicodeWrap(number, TextDirectionHeuristics.LTR)));
+ }
+
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_copy_to_clipboard,
+ ContextMenu.NONE,
+ R.string.action_copy_number_text)
+ .setOnMenuItemClickListener(this);
+
+ // The edit number before call does not show up if any of the conditions apply:
+ // 1) Number cannot be called
+ // 2) Number is the voicemail number
+ // 3) Number is a SIP address
+
+ if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)
+ && !mCallLogCache.isVoicemailNumber(accountHandle, number)
+ && !PhoneNumberHelper.isSipNumber(number)) {
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_edit_before_call,
+ ContextMenu.NONE,
+ R.string.action_edit_number_before_call)
+ .setOnMenuItemClickListener(this);
+ }
+
+ if (callType == CallLog.Calls.VOICEMAIL_TYPE
+ && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) {
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_copy_transcript_to_clipboard,
+ ContextMenu.NONE,
+ R.string.copy_transcript_text)
+ .setOnMenuItemClickListener(this);
+ }
+
+ String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
+ if (!isVoicemailNumber
+ && FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number)
+ && FilteredNumberCompat.canAttemptBlockOperations(mContext)) {
+ boolean isBlocked = blockId != null;
+ if (isBlocked) {
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_unblock,
+ ContextMenu.NONE,
+ R.string.call_log_action_unblock_number)
+ .setOnMenuItemClickListener(this);
+ } else {
+ if (isSpamFeatureEnabled) {
+ if (isSpam) {
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_report_not_spam,
+ ContextMenu.NONE,
+ R.string.call_log_action_remove_spam)
+ .setOnMenuItemClickListener(this);
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_block,
+ ContextMenu.NONE,
+ R.string.call_log_action_block_number)
+ .setOnMenuItemClickListener(this);
+ } else {
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_block_report_spam,
+ ContextMenu.NONE,
+ R.string.call_log_action_block_report_number)
+ .setOnMenuItemClickListener(this);
+ }
+ } else {
+ menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_block,
+ ContextMenu.NONE,
+ R.string.call_log_action_block_number)
+ .setOnMenuItemClickListener(this);
+ }
+ }
+ }
+
+ Logger.get(mContext).logScreenView(ScreenEvent.Type.CALL_LOG_CONTEXT_MENU, (Activity) mContext);
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ int resId = item.getItemId();
+ if (resId == R.id.context_menu_copy_to_clipboard) {
+ ClipboardUtils.copyText(mContext, null, number, true);
+ return true;
+ } else if (resId == R.id.context_menu_copy_transcript_to_clipboard) {
+ ClipboardUtils.copyText(
+ mContext, null, phoneCallDetailsViews.voicemailTranscriptionView.getText(), true);
+ return true;
+ } else if (resId == R.id.context_menu_edit_before_call) {
+ final Intent intent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number));
+ intent.setClass(mContext, DialtactsActivity.class);
+ DialerUtils.startActivityWithErrorToast(mContext, intent);
+ return true;
+ } else if (resId == R.id.context_menu_block_report_spam) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_REPORT_SPAM);
+ maybeShowBlockNumberMigrationDialog(
+ new BlockedNumbersMigrator.Listener() {
+ @Override
+ public void onComplete() {
+ mBlockReportListener.onBlockReportSpam(
+ displayNumber, number, countryIso, callType, info.sourceType);
+ }
+ });
+ } else if (resId == R.id.context_menu_block) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_NUMBER);
+ maybeShowBlockNumberMigrationDialog(
+ new BlockedNumbersMigrator.Listener() {
+ @Override
+ public void onComplete() {
+ mBlockReportListener.onBlock(
+ displayNumber, number, countryIso, callType, info.sourceType);
+ }
+ });
+ } else if (resId == R.id.context_menu_unblock) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_UNBLOCK_NUMBER);
+ mBlockReportListener.onUnblock(
+ displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId);
+ } else if (resId == R.id.context_menu_report_not_spam) {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_REPORT_AS_NOT_SPAM);
+ mBlockReportListener.onReportNotSpam(
+ displayNumber, number, countryIso, callType, info.sourceType);
+ }
+ return false;
+ }
+
+ /**
+ * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not inflated
+ * during initial binding, so click handlers, tags and accessibility text must be set here, if
+ * necessary.
+ */
+ public void inflateActionViewStub() {
+ ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub);
+ if (stub != null) {
+ actionsView = stub.inflate();
+
+ voicemailPlaybackView =
+ (VoicemailPlaybackLayout) actionsView.findViewById(R.id.voicemail_playback_layout);
+ voicemailPlaybackView.setViewHolder(this);
+
+ callButtonView = actionsView.findViewById(R.id.call_action);
+ callButtonView.setOnClickListener(this);
+
+ videoCallButtonView = actionsView.findViewById(R.id.video_call_action);
+ videoCallButtonView.setOnClickListener(this);
+
+ createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action);
+ createNewContactButtonView.setOnClickListener(this);
+
+ addToExistingContactButtonView =
+ actionsView.findViewById(R.id.add_to_existing_contact_action);
+ addToExistingContactButtonView.setOnClickListener(this);
+
+ sendMessageView = actionsView.findViewById(R.id.send_message_action);
+ sendMessageView.setOnClickListener(this);
+
+ blockReportView = actionsView.findViewById(R.id.block_report_action);
+ blockReportView.setOnClickListener(this);
+
+ blockView = actionsView.findViewById(R.id.block_action);
+ blockView.setOnClickListener(this);
+
+ unblockView = actionsView.findViewById(R.id.unblock_action);
+ unblockView.setOnClickListener(this);
+
+ reportNotSpamView = actionsView.findViewById(R.id.report_not_spam_action);
+ reportNotSpamView.setOnClickListener(this);
+
+ detailsButtonView = actionsView.findViewById(R.id.details_action);
+ detailsButtonView.setOnClickListener(this);
+
+ callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action);
+ callWithNoteButtonView.setOnClickListener(this);
+
+ callComposeButtonView = actionsView.findViewById(R.id.call_compose_action);
+ callComposeButtonView.setOnClickListener(this);
+
+ sendVoicemailButtonView = actionsView.findViewById(R.id.share_voicemail);
+ sendVoicemailButtonView.setOnClickListener(this);
+ }
+ }
+
+ private void updatePrimaryActionButton(boolean isExpanded) {
+
+ if (nameOrNumber == null) {
+ LogUtil.e("CallLogListItemViewHolder.updatePrimaryActionButton", "name or number is null");
+ }
+
+ // Calling expandTemplate with a null parameter will cause a NullPointerException.
+ CharSequence validNameOrNumber = nameOrNumber == null ? "" : nameOrNumber;
+
+ if (!TextUtils.isEmpty(voicemailUri)) {
+ // Treat as voicemail list item; show play button if not expanded.
+ if (!isExpanded) {
+ primaryActionButtonView.setImageResource(R.drawable.ic_play_arrow_24dp);
+ primaryActionButtonView.setContentDescription(
+ TextUtils.expandTemplate(
+ mContext.getString(R.string.description_voicemail_action), validNameOrNumber));
+ primaryActionButtonView.setVisibility(View.VISIBLE);
+ } else {
+ primaryActionButtonView.setVisibility(View.GONE);
+ }
+ } else {
+ // Treat as normal list item; show call button, if possible.
+ if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) {
+ boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
+ if (isVoicemailNumber) {
+ // Call to generic voicemail number, in case there are multiple accounts.
+ primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider());
+ } else {
+ primaryActionButtonView.setTag(
+ IntentProvider.getReturnCallIntentProvider(number + postDialDigits));
+ }
+
+ primaryActionButtonView.setContentDescription(
+ TextUtils.expandTemplate(
+ mContext.getString(R.string.description_call_action), validNameOrNumber));
+ primaryActionButtonView.setImageResource(R.drawable.ic_call_24dp);
+ primaryActionButtonView.setVisibility(View.VISIBLE);
+ } else {
+ primaryActionButtonView.setTag(null);
+ primaryActionButtonView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private static boolean isShareVoicemailAllowed(Context context) {
+ return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true);
+ }
+
+ /**
+ * Binds text titles, click handlers and intents to the voicemail, details and callback action
+ * buttons.
+ */
+ private void bindActionButtons() {
+ boolean canPlaceCallToNumber = PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation);
+
+ if (isFullyUndialableVoicemail()) {
+ // Sometimes the voicemail server will report the message is from some non phone number
+ // source. If the number does not contains any dialable digit treat it as it is from a unknown
+ // number, remove all action buttons but still show the voicemail playback layout.
+ callButtonView.setVisibility(View.GONE);
+ videoCallButtonView.setVisibility(View.GONE);
+ detailsButtonView.setVisibility(View.GONE);
+ createNewContactButtonView.setVisibility(View.GONE);
+ addToExistingContactButtonView.setVisibility(View.GONE);
+ sendMessageView.setVisibility(View.GONE);
+ callWithNoteButtonView.setVisibility(View.GONE);
+ callComposeButtonView.setVisibility(View.GONE);
+ blockReportView.setVisibility(View.GONE);
+ blockView.setVisibility(View.GONE);
+ unblockView.setVisibility(View.GONE);
+ reportNotSpamView.setVisibility(View.GONE);
+
+ if (isShareVoicemailAllowed(mContext)) {
+ sendVoicemailButtonView.setVisibility(View.VISIBLE);
+ }
+ voicemailPlaybackView.setVisibility(View.VISIBLE);
+ Uri uri = Uri.parse(voicemailUri);
+ mVoicemailPlaybackPresenter.setPlaybackView(
+ voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked);
+ mVoicemailPrimaryActionButtonClicked = false;
+ CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
+ return;
+ }
+
+ if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) {
+ callButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number));
+ ((TextView) callButtonView.findViewById(R.id.call_action_text))
+ .setText(
+ TextUtils.expandTemplate(
+ mContext.getString(R.string.call_log_action_call),
+ nameOrNumber == null ? "" : nameOrNumber));
+ TextView callTypeOrLocationView =
+ ((TextView) callButtonView.findViewById(R.id.call_type_or_location_text));
+ if (callType == Calls.VOICEMAIL_TYPE && !TextUtils.isEmpty(callTypeOrLocation)) {
+ callTypeOrLocationView.setText(callTypeOrLocation);
+ callTypeOrLocationView.setVisibility(View.VISIBLE);
+ } else {
+ callTypeOrLocationView.setVisibility(View.GONE);
+ }
+ callButtonView.setVisibility(View.VISIBLE);
+ } else {
+ callButtonView.setVisibility(View.GONE);
+ }
+
+ if (shouldShowVideoCallActionButton(canPlaceCallToNumber)) {
+ videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number));
+ videoCallButtonView.setVisibility(View.VISIBLE);
+ } else {
+ videoCallButtonView.setVisibility(View.GONE);
+ }
+
+ // For voicemail calls, show the voicemail playback layout; hide otherwise.
+ if (callType == Calls.VOICEMAIL_TYPE
+ && mVoicemailPlaybackPresenter != null
+ && !TextUtils.isEmpty(voicemailUri)) {
+ voicemailPlaybackView.setVisibility(View.VISIBLE);
+ if (isShareVoicemailAllowed(mContext)) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE);
+ sendVoicemailButtonView.setVisibility(View.VISIBLE);
+ }
+
+ Uri uri = Uri.parse(voicemailUri);
+ mVoicemailPlaybackPresenter.setPlaybackView(
+ voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked);
+ mVoicemailPrimaryActionButtonClicked = false;
+ CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
+ } else {
+ voicemailPlaybackView.setVisibility(View.GONE);
+ sendVoicemailButtonView.setVisibility(View.GONE);
+ }
+
+ if (callType == Calls.VOICEMAIL_TYPE) {
+ detailsButtonView.setVisibility(View.GONE);
+ } else {
+ detailsButtonView.setVisibility(View.VISIBLE);
+ detailsButtonView.setTag(IntentProvider.getCallDetailIntentProvider(rowId, callIds, null));
+ }
+
+ boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam);
+
+ if (!isBlockedOrSpam && info != null && UriUtils.isEncodedContactUri(info.lookupUri)) {
+ createNewContactButtonView.setTag(
+ IntentProvider.getAddContactIntentProvider(
+ info.lookupUri, info.name, info.number, info.type, true /* isNewContact */));
+ createNewContactButtonView.setVisibility(View.VISIBLE);
+
+ addToExistingContactButtonView.setTag(
+ IntentProvider.getAddContactIntentProvider(
+ info.lookupUri, info.name, info.number, info.type, false /* isNewContact */));
+ addToExistingContactButtonView.setVisibility(View.VISIBLE);
+ } else {
+ createNewContactButtonView.setVisibility(View.GONE);
+ addToExistingContactButtonView.setVisibility(View.GONE);
+ }
+
+ if (canPlaceCallToNumber && !isBlockedOrSpam) {
+ sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number));
+ sendMessageView.setVisibility(View.VISIBLE);
+ } else {
+ sendMessageView.setVisibility(View.GONE);
+ }
+
+ mCallLogListItemHelper.setActionContentDescriptions(this);
+
+ boolean supportsCallSubject = mCallLogCache.doesAccountSupportCallSubject(accountHandle);
+ boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
+ callWithNoteButtonView.setVisibility(
+ supportsCallSubject && !isVoicemailNumber && info != null ? View.VISIBLE : View.GONE);
+
+ callComposeButtonView.setVisibility(isCallComposerCapable ? View.VISIBLE : View.GONE);
+
+ updateBlockReportActions(isVoicemailNumber);
+ }
+
+ private boolean isFullyUndialableVoicemail() {
+ if (callType == Calls.VOICEMAIL_TYPE) {
+ if (!hasDialableChar(number)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasDialableChar(CharSequence number) {
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+ for (char c : number.toString().toCharArray()) {
+ if (PhoneNumberUtils.isDialable(c)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean shouldShowVideoCallActionButton(boolean canPlaceCallToNumber) {
+ return canPlaceCallToNumber && (hasPlacedVideoCall() || canSupportVideoCall());
+ }
+
+ private boolean hasPlacedVideoCall() {
+ return phoneCallDetailsViews.callTypeIcons.isVideoShown();
+ }
+
+ private boolean canSupportVideoCall() {
+ return mCallLogCache.canRelyOnVideoPresence()
+ && info != null
+ && (info.carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
+ }
+
+ /**
+ * Show or hide the action views, such as voicemail, details, and add contact.
+ *
+ * <p>If the action views have never been shown yet for this view, inflate the view stub.
+ */
+ public void showActions(boolean show) {
+ showOrHideVoicemailTranscriptionView(show);
+
+ if (show) {
+ if (!isLoaded) {
+ // b/31268128 for some unidentified reason showActions() can be called before the item is
+ // loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will
+ // be called again once the item is loaded.
+ LogUtil.e(
+ "CallLogListItemViewHolder.showActions",
+ "called before item is loaded",
+ new Exception());
+ return;
+ }
+
+ // Inflate the view stub if necessary, and wire up the event handlers.
+ inflateActionViewStub();
+ bindActionButtons();
+ actionsView.setVisibility(View.VISIBLE);
+ actionsView.setAlpha(1.0f);
+ } else {
+ // When recycling a view, it is possible the actionsView ViewStub was previously
+ // inflated so we should hide it in this case.
+ if (actionsView != null) {
+ actionsView.setVisibility(View.GONE);
+ }
+ }
+
+ updatePrimaryActionButton(show);
+ }
+
+ public void showOrHideVoicemailTranscriptionView(boolean isExpanded) {
+ if (callType != Calls.VOICEMAIL_TYPE) {
+ return;
+ }
+
+ final TextView view = phoneCallDetailsViews.voicemailTranscriptionView;
+ if (!isExpanded || TextUtils.isEmpty(view.getText())) {
+ view.setVisibility(View.GONE);
+ return;
+ }
+ view.setVisibility(View.VISIBLE);
+ }
+
+ public void updatePhoto() {
+ quickContactView.assignContactUri(info.lookupUri);
+
+ if (isSpamFeatureEnabled && isSpam) {
+ quickContactView.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact));
+ return;
+ }
+ final boolean isVoicemail = mCallLogCache.isVoicemailNumber(accountHandle, number);
+ int contactType = ContactPhotoManager.TYPE_DEFAULT;
+ if (isVoicemail) {
+ contactType = ContactPhotoManager.TYPE_VOICEMAIL;
+ } else if (isBusiness) {
+ contactType = ContactPhotoManager.TYPE_BUSINESS;
+ }
+
+ final String lookupKey =
+ info.lookupUri != null ? UriUtils.getLookupKeyFromUri(info.lookupUri) : null;
+ final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name;
+ final DefaultImageRequest request =
+ new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */);
+
+ if (info.photoId == 0 && info.photoUri != null) {
+ ContactPhotoManager.getInstance(mContext)
+ .loadPhoto(
+ quickContactView,
+ info.photoUri,
+ mPhotoSize,
+ false /* darkTheme */,
+ true /* isCircular */,
+ request);
+ } else {
+ ContactPhotoManager.getInstance(mContext)
+ .loadThumbnail(
+ quickContactView,
+ info.photoId,
+ false /* darkTheme */,
+ true /* isCircular */,
+ request);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_DIRECTLY);
+ mVoicemailPrimaryActionButtonClicked = true;
+ mExpandCollapseListener.onClick(primaryActionView);
+ } else if (view.getId() == R.id.call_with_note_action) {
+ CallSubjectDialog.start(
+ (Activity) mContext,
+ info.photoId,
+ info.photoUri,
+ info.lookupUri,
+ (String) nameOrNumber /* top line of contact view in call subject dialog */,
+ isBusiness,
+ number,
+ TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact
+ view in dialog. */
+ numberType, /* phone number type (e.g. mobile) in second line of contact view */
+ accountHandle);
+ } else if (view.getId() == R.id.block_report_action) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_REPORT_SPAM);
+ maybeShowBlockNumberMigrationDialog(
+ new BlockedNumbersMigrator.Listener() {
+ @Override
+ public void onComplete() {
+ mBlockReportListener.onBlockReportSpam(
+ displayNumber, number, countryIso, callType, info.sourceType);
+ }
+ });
+ } else if (view.getId() == R.id.block_action) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_NUMBER);
+ maybeShowBlockNumberMigrationDialog(
+ new BlockedNumbersMigrator.Listener() {
+ @Override
+ public void onComplete() {
+ mBlockReportListener.onBlock(
+ displayNumber, number, countryIso, callType, info.sourceType);
+ }
+ });
+ } else if (view.getId() == R.id.unblock_action) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_UNBLOCK_NUMBER);
+ mBlockReportListener.onUnblock(
+ displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId);
+ } else if (view.getId() == R.id.report_not_spam_action) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_REPORT_AS_NOT_SPAM);
+ mBlockReportListener.onReportNotSpam(
+ displayNumber, number, countryIso, callType, info.sourceType);
+ } else if (view.getId() == R.id.call_compose_action) {
+ LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed");
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL);
+ CallComposerContact contact = new CallComposerContact();
+ contact.photoId = info.photoId;
+ contact.photoUri = info.photoUri == null ? null : info.photoUri.toString();
+ contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString();
+ contact.nameOrNumber = (String) nameOrNumber;
+ contact.isBusiness = isBusiness;
+ contact.number = number;
+ /* second line of contact view. */
+ contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber;
+ /* phone number type (e.g. mobile) in second line of contact view */
+ contact.numberLabel = numberType;
+ Activity activity = (Activity) mContext;
+ activity.startActivityForResult(
+ CallComposerActivity.newIntent(activity, contact),
+ DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE);
+ } else if (view.getId() == R.id.share_voicemail) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED);
+ mVoicemailPlaybackPresenter.shareVoicemail();
+ } else {
+ logCallLogAction(view.getId());
+ final IntentProvider intentProvider = (IntentProvider) view.getTag();
+ if (intentProvider != null) {
+ final Intent intent = intentProvider.getIntent(mContext);
+ // See IntentProvider.getCallDetailIntentProvider() for why this may be null.
+ if (intent != null) {
+ DialerUtils.startActivityWithErrorToast(mContext, intent);
+ }
+ }
+ }
+ }
+
+ private void logCallLogAction(int id) {
+ if (id == R.id.send_message_action) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE);
+ } else if (id == R.id.add_to_existing_contact_action) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_ADD_TO_CONTACT);
+ } else if (id == R.id.create_new_contact_action) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CREATE_NEW_CONTACT);
+ }
+ }
+
+ private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) {
+ if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog(
+ mContext, ((Activity) mContext).getFragmentManager(), listener)) {
+ listener.onComplete();
+ }
+ }
+
+ private void updateBlockReportActions(boolean isVoicemailNumber) {
+ // Set block/spam actions.
+ blockReportView.setVisibility(View.GONE);
+ blockView.setVisibility(View.GONE);
+ unblockView.setVisibility(View.GONE);
+ reportNotSpamView.setVisibility(View.GONE);
+ String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ if (isVoicemailNumber
+ || !FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number)
+ || !FilteredNumberCompat.canAttemptBlockOperations(mContext)) {
+ return;
+ }
+ boolean isBlocked = blockId != null;
+ if (isBlocked) {
+ unblockView.setVisibility(View.VISIBLE);
+ } else {
+ if (isSpamFeatureEnabled) {
+ if (isSpam) {
+ blockView.setVisibility(View.VISIBLE);
+ reportNotSpamView.setVisibility(View.VISIBLE);
+ } else {
+ blockReportView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ blockView.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ public interface OnClickListener {
+
+ void onBlockReportSpam(
+ String displayNumber,
+ String number,
+ String countryIso,
+ int callType,
+ int contactSourceType);
+
+ void onBlock(
+ String displayNumber,
+ String number,
+ String countryIso,
+ int callType,
+ int contactSourceType);
+
+ void onUnblock(
+ String displayNumber,
+ String number,
+ String countryIso,
+ int callType,
+ int contactSourceType,
+ boolean isSpam,
+ Integer blockId);
+
+ void onReportNotSpam(
+ String displayNumber,
+ String number,
+ String countryIso,
+ int callType,
+ int contactSourceType);
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java b/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java
new file mode 100644
index 0000000..9de260a
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java
@@ -0,0 +1,74 @@
+/*
+ * 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.app.calllog;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.app.R;
+import com.android.dialer.app.alert.AlertManager;
+
+/**
+ * Alert manager controls modal view to show message in call log. When modal view is shown, regular
+ * call log will be hidden.
+ */
+public class CallLogModalAlertManager implements AlertManager {
+
+ interface Listener {
+ void onShowModalAlert(boolean show);
+ }
+
+ private final Listener listener;
+ private final ViewGroup parent;
+ private final ViewGroup container;
+ private final LayoutInflater inflater;
+
+ public CallLogModalAlertManager(LayoutInflater inflater, ViewGroup parent, Listener listener) {
+ this.inflater = inflater;
+ this.parent = parent;
+ this.listener = listener;
+ container = (ViewGroup) parent.findViewById(R.id.modal_message_container);
+ }
+
+ @Override
+ public View inflate(int layoutId) {
+ return inflater.inflate(layoutId, parent, false);
+ }
+
+ @Override
+ public void add(View view) {
+ if (contains(view)) {
+ return;
+ }
+ container.addView(view);
+ listener.onShowModalAlert(true);
+ }
+
+ @Override
+ public void clear() {
+ container.removeAllViews();
+ listener.onShowModalAlert(false);
+ }
+
+ public boolean isEmpty() {
+ return container.getChildCount() == 0;
+ }
+
+ public boolean contains(View view) {
+ return container.indexOfChild(view) != -1;
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java
new file mode 100644
index 0000000..8f664d1
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2013 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.app.calllog;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog.Calls;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.app.R;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Helper class operating on call log notifications. */
+public class CallLogNotificationsHelper {
+
+ private static final String TAG = "CallLogNotifHelper";
+ private static CallLogNotificationsHelper sInstance;
+ private final Context mContext;
+ private final NewCallsQuery mNewCallsQuery;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final String mCurrentCountryIso;
+
+ CallLogNotificationsHelper(
+ Context context,
+ NewCallsQuery newCallsQuery,
+ ContactInfoHelper contactInfoHelper,
+ String countryIso) {
+ mContext = context;
+ mNewCallsQuery = newCallsQuery;
+ mContactInfoHelper = contactInfoHelper;
+ mCurrentCountryIso = countryIso;
+ }
+
+ /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
+ public static CallLogNotificationsHelper getInstance(Context context) {
+ if (sInstance == null) {
+ ContentResolver contentResolver = context.getContentResolver();
+ String countryIso = GeoUtil.getCurrentCountryIso(context);
+ sInstance =
+ new CallLogNotificationsHelper(
+ context,
+ createNewCallsQuery(context, contentResolver),
+ new ContactInfoHelper(context, countryIso),
+ countryIso);
+ }
+ return sInstance;
+ }
+
+ /** Removes the missed call notifications. */
+ public static void removeMissedCallNotifications(Context context) {
+ TelecomUtil.cancelMissedCallsNotification(context);
+ }
+
+ /** Update the voice mail notifications. */
+ public static void updateVoicemailNotifications(Context context) {
+ CallLogNotificationsService.updateVoicemailNotifications(context, null);
+ }
+
+ /** Create a new instance of {@link NewCallsQuery}. */
+ public static NewCallsQuery createNewCallsQuery(
+ Context context, ContentResolver contentResolver) {
+
+ return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
+ }
+
+ /**
+ * Get all voicemails with the "new" flag set to 1.
+ *
+ * @return A list of NewCall objects where each object represents a new voicemail.
+ */
+ @Nullable
+ public List<NewCall> getNewVoicemails() {
+ return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE);
+ }
+
+ /**
+ * Get all missed calls with the "new" flag set to 1.
+ *
+ * @return A list of NewCall objects where each object represents a new missed call.
+ */
+ @Nullable
+ public List<NewCall> getNewMissedCalls() {
+ return mNewCallsQuery.query(Calls.MISSED_TYPE);
+ }
+
+ /**
+ * Given a number and number information (presentation and country ISO), get the best name for
+ * display. If the name is empty but we have a special presentation, display that. Otherwise
+ * attempt to look it up in the database or the cache. If that fails, fall back to displaying the
+ * number.
+ */
+ public String getName(
+ @Nullable String number, int numberPresentation, @Nullable String countryIso) {
+ return getContactInfo(number, numberPresentation, countryIso).name;
+ }
+
+ /**
+ * Given a number and number information (presentation and country ISO), get {@link ContactInfo}.
+ * If the name is empty but we have a special presentation, display that. Otherwise attempt to
+ * look it up in the cache. If that fails, fall back to displaying the number.
+ */
+ public ContactInfo getContactInfo(
+ @Nullable String number, int numberPresentation, @Nullable String countryIso) {
+ if (countryIso == null) {
+ countryIso = mCurrentCountryIso;
+ }
+
+ number = (number == null) ? "" : number;
+ ContactInfo contactInfo = new ContactInfo();
+ contactInfo.number = number;
+ contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso);
+ // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo.
+ contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+
+ // 1. Special number representation.
+ contactInfo.name =
+ PhoneNumberDisplayUtil.getDisplayName(mContext, number, numberPresentation, false)
+ .toString();
+ if (!TextUtils.isEmpty(contactInfo.name)) {
+ return contactInfo;
+ }
+
+ // 2. Look it up in the cache.
+ ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso);
+
+ if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) {
+ return cachedContactInfo;
+ }
+
+ if (!TextUtils.isEmpty(contactInfo.formattedNumber)) {
+ // 3. If we cannot lookup the contact, use the formatted number instead.
+ contactInfo.name = contactInfo.formattedNumber;
+ } else if (!TextUtils.isEmpty(number)) {
+ // 4. If number can't be formatted, use number.
+ contactInfo.name = number;
+ } else {
+ // 5. Otherwise, it's unknown number.
+ contactInfo.name = mContext.getResources().getString(R.string.unknown);
+ }
+ return contactInfo;
+ }
+
+ /** Allows determining the new calls for which a notification should be generated. */
+ public interface NewCallsQuery {
+
+ /** Returns the new calls of a certain type for which a notification should be generated. */
+ @Nullable
+ List<NewCall> query(int type);
+ }
+
+ /** Information about a new voicemail. */
+ public static final class NewCall {
+
+ public final Uri callsUri;
+ public final Uri voicemailUri;
+ public final String number;
+ public final int numberPresentation;
+ public final String accountComponentName;
+ public final String accountId;
+ public final String transcription;
+ public final String countryIso;
+ public final long dateMs;
+
+ public NewCall(
+ Uri callsUri,
+ Uri voicemailUri,
+ String number,
+ int numberPresentation,
+ String accountComponentName,
+ String accountId,
+ String transcription,
+ String countryIso,
+ long dateMs) {
+ this.callsUri = callsUri;
+ this.voicemailUri = voicemailUri;
+ this.number = number;
+ this.numberPresentation = numberPresentation;
+ this.accountComponentName = accountComponentName;
+ this.accountId = accountId;
+ this.transcription = transcription;
+ this.countryIso = countryIso;
+ this.dateMs = dateMs;
+ }
+ }
+
+ /**
+ * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to notify
+ * about in the call log.
+ */
+ private static final class DefaultNewCallsQuery implements NewCallsQuery {
+
+ private static final String[] PROJECTION = {
+ Calls._ID,
+ Calls.NUMBER,
+ Calls.VOICEMAIL_URI,
+ Calls.NUMBER_PRESENTATION,
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME,
+ Calls.PHONE_ACCOUNT_ID,
+ Calls.TRANSCRIPTION,
+ Calls.COUNTRY_ISO,
+ Calls.DATE
+ };
+ private static final int ID_COLUMN_INDEX = 0;
+ private static final int NUMBER_COLUMN_INDEX = 1;
+ private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
+ private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
+ private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
+ private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
+ private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
+ private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
+ private static final int DATE_COLUMN_INDEX = 8;
+
+ private final ContentResolver mContentResolver;
+ private final Context mContext;
+
+ private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
+ mContext = context;
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ @Nullable
+ @TargetApi(VERSION_CODES.M)
+ public List<NewCall> query(int type) {
+ if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
+ Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
+ return null;
+ }
+ final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
+ final String[] selectionArgs = new String[] {Integer.toString(type)};
+ try (Cursor cursor =
+ mContentResolver.query(
+ Calls.CONTENT_URI_WITH_VOICEMAIL,
+ PROJECTION,
+ selection,
+ selectionArgs,
+ Calls.DEFAULT_SORT_ORDER)) {
+ if (cursor == null) {
+ return null;
+ }
+ List<NewCall> newCalls = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ newCalls.add(createNewCallsFromCursor(cursor));
+ }
+ return newCalls;
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
+ return null;
+ }
+ }
+
+ /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
+ private NewCall createNewCallsFromCursor(Cursor cursor) {
+ String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
+ Uri callsUri =
+ ContentUris.withAppendedId(
+ Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
+ Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
+ return new NewCall(
+ callsUri,
+ voicemailUri,
+ cursor.getString(NUMBER_COLUMN_INDEX),
+ cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
+ cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
+ cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
+ cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
+ cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
+ cursor.getLong(DATE_COLUMN_INDEX));
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
new file mode 100644
index 0000000..8205281
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import me.leolin.shortcutbadger.ShortcutBadger;
+
+/**
+ * Provides operations for managing call-related notifications.
+ *
+ * <p>It handles the following actions:
+ *
+ * <ul>
+ * <li>Updating voicemail notifications
+ * <li>Marking new voicemails as old
+ * <li>Updating missed call notifications
+ * <li>Marking new missed calls as old
+ * <li>Calling back from a missed call
+ * <li>Sending an SMS from a missed call
+ * </ul>
+ */
+public class CallLogNotificationsService extends IntentService {
+
+ /** Action to mark all the new voicemails as old. */
+ public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD =
+ "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
+ /**
+ * Action to update voicemail notifications.
+ *
+ * <p>May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}.
+ */
+ public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS =
+ "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS";
+ /**
+ * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new
+ * voicemail that triggered an update.
+ *
+ * <p>It must be a {@link Uri}.
+ */
+ public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
+ /**
+ * Action to update the missed call notifications.
+ *
+ * <p>Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and {@link
+ * #EXTRA_MISSED_CALL_COUNT}.
+ */
+ public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS =
+ "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS";
+ /** Action to mark all the new missed calls as old. */
+ public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD =
+ "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD";
+ /** Action to call back a missed call. */
+ public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION =
+ "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION";
+
+ public static final String ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION =
+ "com.android.dialer.calllog.SEND_SMS_FROM_MISSED_CALL_NOTIFICATION";
+ /**
+ * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS}, {@link
+ * #ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION} and {@link
+ * #ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION} to identify the number to display, call or
+ * text back.
+ *
+ * <p>It must be a {@link String}.
+ */
+ public static final String EXTRA_MISSED_CALL_NUMBER = "MISSED_CALL_NUMBER";
+ /**
+ * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS} to represent the
+ * number of missed calls.
+ *
+ * <p>It must be a {@link Integer}
+ */
+ public static final String EXTRA_MISSED_CALL_COUNT = "MISSED_CALL_COUNT";
+
+ public static final int UNKNOWN_MISSED_CALL_COUNT = -1;
+ private VoicemailQueryHandler mVoicemailQueryHandler;
+
+ public CallLogNotificationsService() {
+ super("CallLogNotificationsService");
+ }
+
+ /**
+ * Updates notifications for any new voicemails.
+ *
+ * @param context a valid context.
+ * @param voicemailUri The uri pointing to the voicemail to update the notification for. If {@code
+ * null}, then notifications for all new voicemails will be updated.
+ */
+ public static void updateVoicemailNotifications(Context context, Uri voicemailUri) {
+ if (!TelecomUtil.isDefaultDialer(context)) {
+ LogUtil.i(
+ "CallLogNotificationsService.updateVoicemailNotifications",
+ "not default dialer, ignoring voicemail notifications");
+ return;
+ }
+ if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
+ // If voicemailUri is null, then notifications for all voicemails will be updated.
+ if (voicemailUri != null) {
+ serviceIntent.putExtra(CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, voicemailUri);
+ }
+ context.startService(serviceIntent);
+ }
+ }
+
+ /**
+ * Updates notifications for any new missed calls.
+ *
+ * @param context A valid context.
+ * @param count The number of new missed calls.
+ * @param number The phone number of the newest missed call.
+ */
+ public static void updateMissedCallNotifications(Context context, int count, String number) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS);
+ serviceIntent.putExtra(EXTRA_MISSED_CALL_COUNT, count);
+ serviceIntent.putExtra(EXTRA_MISSED_CALL_NUMBER, number);
+ context.startService(serviceIntent);
+ }
+
+ public static void markNewVoicemailsAsOld(Context context) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+ context.startService(serviceIntent);
+ }
+
+ public static boolean updateBadgeCount(Context context, int count) {
+ boolean success = ShortcutBadger.applyCount(context, count);
+ LogUtil.i(
+ "CallLogNotificationsService.updateBadgeCount",
+ "update badge count: %d success: %b",
+ count,
+ success);
+ return success;
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent == null) {
+ LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle null intent");
+ return;
+ }
+
+ if (!PermissionsUtil.hasPermission(this, android.Manifest.permission.READ_CALL_LOG)) {
+ return;
+ }
+
+ String action = intent.getAction();
+ switch (action) {
+ case ACTION_MARK_NEW_VOICEMAILS_AS_OLD:
+ if (mVoicemailQueryHandler == null) {
+ mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
+ }
+ mVoicemailQueryHandler.markNewVoicemailsAsOld();
+ break;
+ case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS:
+ Uri voicemailUri = intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
+ DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+ break;
+ case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS:
+ int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT, UNKNOWN_MISSED_CALL_COUNT);
+ String number = intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER);
+ MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number);
+ updateBadgeCount(this, count);
+ break;
+ case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD:
+ CallLogNotificationsHelper.removeMissedCallNotifications(this);
+ break;
+ case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
+ MissedCallNotifier.getInstance(this)
+ .callBackFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ break;
+ case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION:
+ MissedCallNotifier.getInstance(this)
+ .sendSmsFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ break;
+ default:
+ LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent);
+ break;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java
new file mode 100644
index 0000000..a781b08
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.provider.VoicemailContract;
+import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler;
+import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.CallLogQueryHandler;
+
+/**
+ * Receiver for call log events.
+ *
+ * <p>It is currently used to handle {@link VoicemailContract#ACTION_NEW_VOICEMAIL} and {@link
+ * Intent#ACTION_BOOT_COMPLETED}.
+ */
+public class CallLogReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) {
+ checkVoicemailStatus(context);
+ CallLogNotificationsService.updateVoicemailNotifications(context, intent.getData());
+ } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ CallLogNotificationsService.updateVoicemailNotifications(context, null);
+ } else {
+ LogUtil.w("CallLogReceiver.onReceive", "could not handle: " + intent);
+ }
+ }
+
+ private static void checkVoicemailStatus(Context context) {
+ new CallLogQueryHandler(
+ context,
+ context.getContentResolver(),
+ new CallLogQueryHandler.Listener() {
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus(
+ context, statusCursor, Source.Notification);
+ }
+
+ @Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public boolean onCallsFetched(Cursor combinedCursor) {
+ return false;
+ }
+ })
+ .fetchVoicemailStatus();
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallTypeHelper.java b/java/com/android/dialer/app/calllog/CallTypeHelper.java
new file mode 100644
index 0000000..f3c27a1
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallTypeHelper.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.res.Resources;
+import com.android.dialer.app.R;
+import com.android.dialer.compat.AppCompatConstants;
+
+/** Helper class to perform operations related to call types. */
+public class CallTypeHelper {
+
+ /** Name used to identify incoming calls. */
+ private final CharSequence mIncomingName;
+ /** Name used to identify incoming calls which were transferred to another device. */
+ private final CharSequence mIncomingPulledName;
+ /** Name used to identify outgoing calls. */
+ private final CharSequence mOutgoingName;
+ /** Name used to identify outgoing calls which were transferred to another device. */
+ private final CharSequence mOutgoingPulledName;
+ /** Name used to identify missed calls. */
+ private final CharSequence mMissedName;
+ /** Name used to identify incoming video calls. */
+ private final CharSequence mIncomingVideoName;
+ /** Name used to identify incoming video calls which were transferred to another device. */
+ private final CharSequence mIncomingVideoPulledName;
+ /** Name used to identify outgoing video calls. */
+ private final CharSequence mOutgoingVideoName;
+ /** Name used to identify outgoing video calls which were transferred to another device. */
+ private final CharSequence mOutgoingVideoPulledName;
+ /** Name used to identify missed video calls. */
+ private final CharSequence mMissedVideoName;
+ /** Name used to identify voicemail calls. */
+ private final CharSequence mVoicemailName;
+ /** Name used to identify rejected calls. */
+ private final CharSequence mRejectedName;
+ /** Name used to identify blocked calls. */
+ private final CharSequence mBlockedName;
+ /** Name used to identify calls which were answered on another device. */
+ private final CharSequence mAnsweredElsewhereName;
+
+ public CallTypeHelper(Resources resources) {
+ // Cache these values so that we do not need to look them up each time.
+ mIncomingName = resources.getString(R.string.type_incoming);
+ mIncomingPulledName = resources.getString(R.string.type_incoming_pulled);
+ mOutgoingName = resources.getString(R.string.type_outgoing);
+ mOutgoingPulledName = resources.getString(R.string.type_outgoing_pulled);
+ mMissedName = resources.getString(R.string.type_missed);
+ mIncomingVideoName = resources.getString(R.string.type_incoming_video);
+ mIncomingVideoPulledName = resources.getString(R.string.type_incoming_video_pulled);
+ mOutgoingVideoName = resources.getString(R.string.type_outgoing_video);
+ mOutgoingVideoPulledName = resources.getString(R.string.type_outgoing_video_pulled);
+ mMissedVideoName = resources.getString(R.string.type_missed_video);
+ mVoicemailName = resources.getString(R.string.type_voicemail);
+ mRejectedName = resources.getString(R.string.type_rejected);
+ mBlockedName = resources.getString(R.string.type_blocked);
+ mAnsweredElsewhereName = resources.getString(R.string.type_answered_elsewhere);
+ }
+
+ public static boolean isMissedCallType(int callType) {
+ return (callType != AppCompatConstants.CALLS_INCOMING_TYPE
+ && callType != AppCompatConstants.CALLS_OUTGOING_TYPE
+ && callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE
+ && callType != AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE);
+ }
+
+ /** Returns the text used to represent the given call type. */
+ public CharSequence getCallTypeText(int callType, boolean isVideoCall, boolean isPulledCall) {
+ switch (callType) {
+ case AppCompatConstants.CALLS_INCOMING_TYPE:
+ if (isVideoCall) {
+ if (isPulledCall) {
+ return mIncomingVideoPulledName;
+ } else {
+ return mIncomingVideoName;
+ }
+ } else {
+ if (isPulledCall) {
+ return mIncomingPulledName;
+ } else {
+ return mIncomingName;
+ }
+ }
+
+ case AppCompatConstants.CALLS_OUTGOING_TYPE:
+ if (isVideoCall) {
+ if (isPulledCall) {
+ return mOutgoingVideoPulledName;
+ } else {
+ return mOutgoingVideoName;
+ }
+ } else {
+ if (isPulledCall) {
+ return mOutgoingPulledName;
+ } else {
+ return mOutgoingName;
+ }
+ }
+
+ case AppCompatConstants.CALLS_MISSED_TYPE:
+ if (isVideoCall) {
+ return mMissedVideoName;
+ } else {
+ return mMissedName;
+ }
+
+ case AppCompatConstants.CALLS_VOICEMAIL_TYPE:
+ return mVoicemailName;
+
+ case AppCompatConstants.CALLS_REJECTED_TYPE:
+ return mRejectedName;
+
+ case AppCompatConstants.CALLS_BLOCKED_TYPE:
+ return mBlockedName;
+
+ case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE:
+ return mAnsweredElsewhereName;
+
+ default:
+ return mMissedName;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/CallTypeIconsView.java b/java/com/android/dialer/app/calllog/CallTypeIconsView.java
new file mode 100644
index 0000000..cd5c546
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallTypeIconsView.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.dialer.app.R;
+import com.android.dialer.compat.AppCompatConstants;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * View that draws one or more symbols for different types of calls (missed calls, outgoing etc).
+ * The symbols are set up horizontally. As this view doesn't create subviews, it is better suited
+ * for ListView-recycling that a regular LinearLayout using ImageViews.
+ */
+public class CallTypeIconsView extends View {
+
+ private static Resources sResources;
+ private List<Integer> mCallTypes = new ArrayList<>(3);
+ private boolean mShowVideo = false;
+ private int mWidth;
+ private int mHeight;
+
+ public CallTypeIconsView(Context context) {
+ this(context, null);
+ }
+
+ public CallTypeIconsView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ if (sResources == null) {
+ sResources = new Resources(context);
+ }
+ }
+
+ public void clear() {
+ mCallTypes.clear();
+ mWidth = 0;
+ mHeight = 0;
+ invalidate();
+ }
+
+ public void add(int callType) {
+ mCallTypes.add(callType);
+
+ final Drawable drawable = getCallTypeDrawable(callType);
+ mWidth += drawable.getIntrinsicWidth() + sResources.iconMargin;
+ mHeight = Math.max(mHeight, drawable.getIntrinsicHeight());
+ invalidate();
+ }
+
+ /**
+ * Determines whether the video call icon will be shown.
+ *
+ * @param showVideo True where the video icon should be shown.
+ */
+ public void setShowVideo(boolean showVideo) {
+ mShowVideo = showVideo;
+ if (showVideo) {
+ mWidth += sResources.videoCall.getIntrinsicWidth();
+ mHeight = Math.max(mHeight, sResources.videoCall.getIntrinsicHeight());
+ invalidate();
+ }
+ }
+
+ /**
+ * Determines if the video icon should be shown.
+ *
+ * @return True if the video icon should be shown.
+ */
+ public boolean isVideoShown() {
+ return mShowVideo;
+ }
+
+ public int getCount() {
+ return mCallTypes.size();
+ }
+
+ public int getCallType(int index) {
+ return mCallTypes.get(index);
+ }
+
+ private Drawable getCallTypeDrawable(int callType) {
+ switch (callType) {
+ case AppCompatConstants.CALLS_INCOMING_TYPE:
+ case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE:
+ return sResources.incoming;
+ case AppCompatConstants.CALLS_OUTGOING_TYPE:
+ return sResources.outgoing;
+ case AppCompatConstants.CALLS_MISSED_TYPE:
+ return sResources.missed;
+ case AppCompatConstants.CALLS_VOICEMAIL_TYPE:
+ return sResources.voicemail;
+ case AppCompatConstants.CALLS_BLOCKED_TYPE:
+ return sResources.blocked;
+ default:
+ // It is possible for users to end up with calls with unknown call types in their
+ // call history, possibly due to 3rd party call log implementations (e.g. to
+ // distinguish between rejected and missed calls). Instead of crashing, just
+ // assume that all unknown call types are missed calls.
+ return sResources.missed;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(mWidth, mHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int left = 0;
+ for (Integer callType : mCallTypes) {
+ final Drawable drawable = getCallTypeDrawable(callType);
+ final int right = left + drawable.getIntrinsicWidth();
+ drawable.setBounds(left, 0, right, drawable.getIntrinsicHeight());
+ drawable.draw(canvas);
+ left = right + sResources.iconMargin;
+ }
+
+ // If showing the video call icon, draw it scaled appropriately.
+ if (mShowVideo) {
+ final Drawable drawable = sResources.videoCall;
+ final int right = left + sResources.videoCall.getIntrinsicWidth();
+ drawable.setBounds(left, 0, right, sResources.videoCall.getIntrinsicHeight());
+ drawable.draw(canvas);
+ }
+ }
+
+ private static class Resources {
+
+ // Drawable representing an incoming answered call.
+ public final Drawable incoming;
+
+ // Drawable respresenting an outgoing call.
+ public final Drawable outgoing;
+
+ // Drawable representing an incoming missed call.
+ public final Drawable missed;
+
+ // Drawable representing a voicemail.
+ public final Drawable voicemail;
+
+ // Drawable representing a blocked call.
+ public final Drawable blocked;
+
+ // Drawable repesenting a video call.
+ public final Drawable videoCall;
+
+ /** The margin to use for icons. */
+ public final int iconMargin;
+
+ /**
+ * Configures the call icon drawables. A single white call arrow which points down and left is
+ * used as a basis for all of the call arrow icons, applying rotation and colors as needed.
+ *
+ * @param context The current context.
+ */
+ public Resources(Context context) {
+ final android.content.res.Resources r = context.getResources();
+
+ incoming = r.getDrawable(R.drawable.ic_call_arrow);
+ incoming.setColorFilter(r.getColor(R.color.answered_call), PorterDuff.Mode.MULTIPLY);
+
+ // Create a rotated instance of the call arrow for outgoing calls.
+ outgoing = BitmapUtil.getRotatedDrawable(r, R.drawable.ic_call_arrow, 180f);
+ outgoing.setColorFilter(r.getColor(R.color.answered_call), PorterDuff.Mode.MULTIPLY);
+
+ // Need to make a copy of the arrow drawable, otherwise the same instance colored
+ // above will be recolored here.
+ missed = r.getDrawable(R.drawable.ic_call_arrow).mutate();
+ missed.setColorFilter(r.getColor(R.color.missed_call), PorterDuff.Mode.MULTIPLY);
+
+ voicemail = r.getDrawable(R.drawable.quantum_ic_voicemail_white_18);
+ voicemail.setColorFilter(
+ r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY);
+
+ blocked = getScaledBitmap(context, R.drawable.ic_block_24dp);
+ blocked.setColorFilter(r.getColor(R.color.blocked_call), PorterDuff.Mode.MULTIPLY);
+
+ videoCall = getScaledBitmap(context, R.drawable.quantum_ic_videocam_white_24);
+ videoCall.setColorFilter(
+ r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY);
+
+ iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin);
+ }
+
+ // Gets the icon, scaled to the height of the call type icons. This helps display all the
+ // icons to be the same height, while preserving their width aspect ratio.
+ private Drawable getScaledBitmap(Context context, int resourceId) {
+ Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resourceId);
+ int scaledHeight = context.getResources().getDimensionPixelSize(R.dimen.call_type_icon_size);
+ int scaledWidth =
+ (int) ((float) icon.getWidth() * ((float) scaledHeight / (float) icon.getHeight()));
+ Bitmap scaledIcon = Bitmap.createScaledBitmap(icon, scaledWidth, scaledHeight, false);
+ return new BitmapDrawable(context.getResources(), scaledIcon);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
new file mode 100644
index 0000000..0c9bd4b
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog.Calls;
+import com.android.dialer.app.R;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+
+/** Dialog that clears the call log after confirming with the user */
+public class ClearCallLogDialog extends DialogFragment {
+
+ /** Preferred way to show this dialog */
+ public static void show(FragmentManager fragmentManager) {
+ ClearCallLogDialog dialog = new ClearCallLogDialog();
+ dialog.show(fragmentManager, "deleteCallLog");
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final Context context = getActivity().getApplicationContext();
+ final OnClickListener okListener =
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final ProgressDialog progressDialog =
+ ProgressDialog.show(
+ getActivity(), getString(R.string.clearCallLogProgress_title), "", true, false);
+ progressDialog.setOwnerActivity(getActivity());
+ final AsyncTask<Void, Void, Void> task =
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ resolver.delete(Calls.CONTENT_URI, null, null);
+ CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(context).getCachedNumberLookupService();
+ if (cachedNumberLookupService != null) {
+ cachedNumberLookupService.clearAllCacheEntries(context);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ final Activity activity = progressDialog.getOwnerActivity();
+
+ if (activity == null || activity.isDestroyed() || activity.isFinishing()) {
+ return;
+ }
+
+ if (progressDialog != null && progressDialog.isShowing()) {
+ progressDialog.dismiss();
+ }
+ }
+ };
+ // TODO: Once we have the API, we should configure this ProgressDialog
+ // to only show up after a certain time (e.g. 150ms)
+ progressDialog.show();
+ task.execute();
+ }
+ };
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.clearCallLogConfirmation_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.clearCallLogConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, okListener)
+ .setCancelable(true)
+ .create();
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
new file mode 100644
index 0000000..651a0cc
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.app.list.ListsFragment;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/** Shows a voicemail notification in the status bar. */
+public class DefaultVoicemailNotifier {
+
+ public static final String TAG = "VoicemailNotifier";
+
+ /** The tag used to identify notifications from this class. */
+ private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
+ /** The identifier of the notification of new voicemails. */
+ private static final int NOTIFICATION_ID = 1;
+
+ /** The singleton instance of {@link DefaultVoicemailNotifier}. */
+ private static DefaultVoicemailNotifier sInstance;
+
+ private final Context mContext;
+
+ private DefaultVoicemailNotifier(Context context) {
+ mContext = context;
+ }
+
+ /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
+ public static DefaultVoicemailNotifier getInstance(Context context) {
+ if (sInstance == null) {
+ ContentResolver contentResolver = context.getContentResolver();
+ sInstance = new DefaultVoicemailNotifier(context);
+ }
+ return sInstance;
+ }
+
+ /**
+ * Updates the notification and notifies of the call with the given URI.
+ *
+ * <p>Clears the notification if there are no new voicemails, and notifies if the given URI
+ * corresponds to a new voicemail.
+ *
+ * <p>It is not safe to call this method from the main thread.
+ */
+ public void updateNotification(Uri newCallUri) {
+ // Lookup the list of new voicemails to include in the notification.
+ // TODO: Move this into a service, to avoid holding the receiver up.
+ final List<NewCall> newCalls =
+ CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails();
+
+ if (newCalls == null) {
+ // Query failed, just return.
+ return;
+ }
+
+ if (newCalls.isEmpty()) {
+ // No voicemails to notify about: clear the notification.
+ getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+ return;
+ }
+
+ Resources resources = mContext.getResources();
+
+ // This represents a list of names to include in the notification.
+ String callers = null;
+
+ // Maps each number into a name: if a number is in the map, it has already left a more
+ // recent voicemail.
+ final Map<String, String> names = new ArrayMap<>();
+
+ // Determine the call corresponding to the new voicemail we have to notify about.
+ NewCall callToNotify = null;
+
+ // Iterate over the new voicemails to determine all the information above.
+ Iterator<NewCall> itr = newCalls.iterator();
+ while (itr.hasNext()) {
+ NewCall newCall = itr.next();
+
+ // Skip notifying for numbers which are blocked.
+ if (FilteredNumbersUtil.shouldBlockVoicemail(
+ mContext, newCall.number, newCall.countryIso, newCall.dateMs)) {
+ itr.remove();
+
+ // Delete the voicemail.
+ mContext.getContentResolver().delete(newCall.voicemailUri, null, null);
+ continue;
+ }
+
+ // Check if we already know the name associated with this number.
+ String name = names.get(newCall.number);
+ if (name == null) {
+ name =
+ CallLogNotificationsHelper.getInstance(mContext)
+ .getName(newCall.number, newCall.numberPresentation, newCall.countryIso);
+ names.put(newCall.number, name);
+ // This is a new caller. Add it to the back of the list of callers.
+ if (TextUtils.isEmpty(callers)) {
+ callers = name;
+ } else {
+ callers =
+ resources.getString(R.string.notification_voicemail_callers_list, callers, name);
+ }
+ }
+ // Check if this is the new call we need to notify about.
+ if (newCallUri != null
+ && newCall.voicemailUri != null
+ && ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) {
+ callToNotify = newCall;
+ }
+ }
+
+ // All the potential new voicemails have been removed, e.g. if they were spam.
+ if (newCalls.isEmpty()) {
+ return;
+ }
+
+ // If there is only one voicemail, set its transcription as the "long text".
+ String transcription = null;
+ if (newCalls.size() == 1) {
+ transcription = newCalls.get(0).transcription;
+ }
+
+ if (newCallUri != null && callToNotify == null) {
+ Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
+ }
+
+ // Determine the title of the notification and the icon for it.
+ final String title =
+ resources.getQuantityString(
+ R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size());
+ // TODO: Use the photo of contact if all calls are from the same person.
+ final int icon = android.R.drawable.stat_notify_voicemail;
+
+ Pair<Uri, Integer> info = getNotificationInfo(callToNotify);
+
+ Notification.Builder notificationBuilder =
+ new Notification.Builder(mContext)
+ .setSmallIcon(icon)
+ .setContentTitle(title)
+ .setContentText(callers)
+ .setColor(resources.getColor(R.color.dialer_theme_color))
+ .setSound(info.first)
+ .setDefaults(info.second)
+ .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
+ .setAutoCancel(true);
+
+ if (!TextUtils.isEmpty(transcription)) {
+ notificationBuilder.setStyle(new Notification.BigTextStyle().bigText(transcription));
+ }
+
+ // Determine the intent to fire when the notification is clicked on.
+ final Intent contentIntent;
+ // Open the call log.
+ contentIntent = DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_VOICEMAIL);
+ contentIntent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true);
+ notificationBuilder.setContentIntent(
+ PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
+
+ // The text to show in the ticker, describing the new event.
+ if (callToNotify != null) {
+ CharSequence msg =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ resources,
+ R.string.notification_new_voicemail_ticker,
+ names.get(callToNotify.number));
+ notificationBuilder.setTicker(msg);
+ }
+ Log.i(TAG, "Creating voicemail notification");
+ getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ /**
+ * Determines which ringtone Uri and Notification defaults to use when updating the notification
+ * for the given call.
+ */
+ private Pair<Uri, Integer> getNotificationInfo(@Nullable NewCall callToNotify) {
+ Log.v(TAG, "getNotificationInfo");
+ if (callToNotify == null) {
+ Log.i(TAG, "callToNotify == null");
+ return new Pair<>(null, 0);
+ }
+ PhoneAccountHandle accountHandle;
+ if (callToNotify.accountComponentName == null || callToNotify.accountId == null) {
+ Log.v(TAG, "accountComponentName == null || callToNotify.accountId == null");
+ accountHandle = TelecomUtil.getDefaultOutgoingPhoneAccount(mContext, PhoneAccount.SCHEME_TEL);
+ if (accountHandle == null) {
+ Log.i(TAG, "No default phone account found, using default notification ringtone");
+ return new Pair<>(null, Notification.DEFAULT_ALL);
+ }
+
+ } else {
+ accountHandle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(callToNotify.accountComponentName),
+ callToNotify.accountId);
+ }
+ if (accountHandle.getComponentName() != null) {
+ Log.v(TAG, "PhoneAccountHandle.ComponentInfo:" + accountHandle.getComponentName());
+ } else {
+ Log.i(TAG, "PhoneAccountHandle.ComponentInfo: null");
+ }
+ return new Pair<>(
+ TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle),
+ getNotificationDefaults(accountHandle));
+ }
+
+ private int getNotificationDefaults(PhoneAccountHandle accountHandle) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return TelephonyManagerCompat.isVoicemailVibrationEnabled(
+ getTelephonyManager(), accountHandle)
+ ? Notification.DEFAULT_VIBRATE
+ : 0;
+ }
+ return Notification.DEFAULT_ALL;
+ }
+
+ /** Creates a pending intent that marks all new voicemails as old. */
+ private PendingIntent createMarkNewVoicemailsAsOldIntent() {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ private NotificationManager getNotificationManager() {
+ return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ private TelephonyManager getTelephonyManager() {
+ return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/GroupingListAdapter.java b/java/com/android/dialer/app/calllog/GroupingListAdapter.java
new file mode 100644
index 0000000..d115720
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/GroupingListAdapter.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2015 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.app.calllog;
+
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.support.v7.widget.RecyclerView;
+import android.util.SparseIntArray;
+
+/**
+ * Maintains a list that groups items into groups of consecutive elements which are disjoint, that
+ * is, an item can only belong to one group. This is leveraged for grouping calls in the call log
+ * received from or made to the same phone number.
+ *
+ * <p>There are two integers stored as metadata for every list item in the adapter.
+ */
+abstract class GroupingListAdapter extends RecyclerView.Adapter {
+
+ protected ContentObserver mChangeObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ onContentChanged();
+ }
+ };
+ protected DataSetObserver mDataSetObserver =
+ new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+ };
+ private Cursor mCursor;
+ /**
+ * SparseIntArray, which maps the cursor position of the first element of a group to the size of
+ * the group. The index of a key in this map corresponds to the list position of that group.
+ */
+ private SparseIntArray mGroupMetadata;
+
+ private int mItemCount;
+
+ public GroupingListAdapter() {
+ reset();
+ }
+
+ /**
+ * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for each of them.
+ */
+ protected abstract void addGroups(Cursor cursor);
+
+ protected abstract void onContentChanged();
+
+ public void changeCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return;
+ }
+
+ if (mCursor != null) {
+ mCursor.unregisterContentObserver(mChangeObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mCursor.close();
+ }
+
+ // Reset whenever the cursor is changed.
+ reset();
+ mCursor = cursor;
+
+ if (cursor != null) {
+ addGroups(mCursor);
+
+ // Calculate the item count by subtracting group child counts from the cursor count.
+ mItemCount = mGroupMetadata.size();
+
+ cursor.registerContentObserver(mChangeObserver);
+ cursor.registerDataSetObserver(mDataSetObserver);
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Records information about grouping in the list. Should be called by the overridden {@link
+ * #addGroups} method.
+ */
+ public void addGroup(int cursorPosition, int groupSize) {
+ int lastIndex = mGroupMetadata.size() - 1;
+ if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) {
+ mGroupMetadata.put(cursorPosition, groupSize);
+ } else {
+ // Optimization to avoid binary search if adding groups in ascending cursor position.
+ mGroupMetadata.append(cursorPosition, groupSize);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItemCount;
+ }
+
+ /**
+ * Given the position of a list item, returns the size of the group of items corresponding to that
+ * position.
+ */
+ public int getGroupSize(int listPosition) {
+ if (listPosition < 0 || listPosition >= mGroupMetadata.size()) {
+ return 0;
+ }
+
+ return mGroupMetadata.valueAt(listPosition);
+ }
+
+ /**
+ * Given the position of a list item, returns the the first item in the group of items
+ * corresponding to that position.
+ */
+ public Object getItem(int listPosition) {
+ if (mCursor == null || listPosition < 0 || listPosition >= mGroupMetadata.size()) {
+ return null;
+ }
+
+ int cursorPosition = mGroupMetadata.keyAt(listPosition);
+ if (mCursor.moveToPosition(cursorPosition)) {
+ return mCursor;
+ } else {
+ return null;
+ }
+ }
+
+ private void reset() {
+ mItemCount = 0;
+ mGroupMetadata = new SparseIntArray();
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java
new file mode 100644
index 0000000..879ac35
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/IntentProvider.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.telecom.PhoneAccountHandle;
+import com.android.contacts.common.model.Contact;
+import com.android.contacts.common.model.ContactLoader;
+import com.android.dialer.app.CallDetailActivity;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.IntentUtil;
+import java.util.ArrayList;
+
+/**
+ * Used to create an intent to attach to an action in the call log.
+ *
+ * <p>The intent is constructed lazily with the given information.
+ */
+public abstract class IntentProvider {
+
+ private static final String TAG = IntentProvider.class.getSimpleName();
+
+ public static IntentProvider getReturnCallIntentProvider(final String number) {
+ return getReturnCallIntentProvider(number, null);
+ }
+
+ public static IntentProvider getReturnCallIntentProvider(
+ final String number, final PhoneAccountHandle accountHandle) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ return new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
+ .setPhoneAccountHandle(accountHandle)
+ .build();
+ }
+ };
+ }
+
+ public static IntentProvider getReturnVideoCallIntentProvider(final String number) {
+ return getReturnVideoCallIntentProvider(number, null);
+ }
+
+ public static IntentProvider getReturnVideoCallIntentProvider(
+ final String number, final PhoneAccountHandle accountHandle) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ return new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
+ .setPhoneAccountHandle(accountHandle)
+ .setIsVideoCall(true)
+ .build();
+ }
+ };
+ }
+
+ public static IntentProvider getReturnVoicemailCallIntentProvider() {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ return new CallIntentBuilder(CallUtil.getVoicemailUri(), CallInitiationType.Type.CALL_LOG)
+ .build();
+ }
+ };
+ }
+
+ public static IntentProvider getSendSmsIntentProvider(final String number) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ return IntentUtil.getSendSmsIntent(number);
+ }
+ };
+ }
+
+ /**
+ * Retrieves the call details intent provider for an entry in the call log.
+ *
+ * @param id The call ID of the first call in the call group.
+ * @param extraIds The call ID of the other calls grouped together with the call.
+ * @param voicemailUri If call log entry is for a voicemail, the voicemail URI.
+ * @return The call details intent provider.
+ */
+ public static IntentProvider getCallDetailIntentProvider(
+ final long id, final long[] extraIds, final String voicemailUri) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ Intent intent = new Intent(context, CallDetailActivity.class);
+ // Check if the first item is a voicemail.
+ if (voicemailUri != null) {
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri));
+ }
+
+ if (extraIds != null && extraIds.length > 0) {
+ intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, extraIds);
+ } else {
+ // If there is a single item, use the direct URI for it.
+ intent.setData(ContentUris.withAppendedId(TelecomUtil.getCallLogUri(context), id));
+ }
+ return intent;
+ }
+ };
+ }
+
+ /** Retrieves an add contact intent for the given contact and phone call details. */
+ public static IntentProvider getAddContactIntentProvider(
+ final Uri lookupUri,
+ final CharSequence name,
+ final CharSequence number,
+ final int numberType,
+ final boolean isNewContact) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ Contact contactToSave = null;
+
+ if (lookupUri != null) {
+ contactToSave = ContactLoader.parseEncodedContactEntity(lookupUri);
+ }
+
+ if (contactToSave != null) {
+ // Populate the intent with contact information stored in the lookup URI.
+ // Note: This code mirrors code in Contacts/QuickContactsActivity.
+ final Intent intent;
+ if (isNewContact) {
+ intent = IntentUtil.getNewContactIntent();
+ } else {
+ intent = IntentUtil.getAddToExistingContactIntent();
+ }
+
+ ArrayList<ContentValues> values = contactToSave.getContentValues();
+ // Only pre-fill the name field if the provided display name is an nickname
+ // or better (e.g. structured name, nickname)
+ if (contactToSave.getDisplayNameSource()
+ >= ContactsContract.DisplayNameSources.NICKNAME) {
+ intent.putExtra(ContactsContract.Intents.Insert.NAME, contactToSave.getDisplayName());
+ } else if (contactToSave.getDisplayNameSource()
+ == ContactsContract.DisplayNameSources.ORGANIZATION) {
+ // This is probably an organization. Instead of copying the organization
+ // name into a name entry, copy it into the organization entry. This
+ // way we will still consider the contact an organization.
+ final ContentValues organization = new ContentValues();
+ organization.put(
+ ContactsContract.CommonDataKinds.Organization.COMPANY,
+ contactToSave.getDisplayName());
+ organization.put(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
+ values.add(organization);
+ }
+
+ // Last time used and times used are aggregated values from the usage stat
+ // table. They need to be removed from data values so the SQL table can insert
+ // properly
+ for (ContentValues value : values) {
+ value.remove(ContactsContract.Data.LAST_TIME_USED);
+ value.remove(ContactsContract.Data.TIMES_USED);
+ }
+
+ intent.putExtra(ContactsContract.Intents.Insert.DATA, values);
+
+ return intent;
+ } else {
+ // If no lookup uri is provided, rely on the available phone number and name.
+ if (isNewContact) {
+ return IntentUtil.getNewContactIntent(name, number, numberType);
+ } else {
+ return IntentUtil.getAddToExistingContactIntent(name, number, numberType);
+ }
+ }
+ }
+ };
+ }
+
+ public abstract Intent getIntent(Context context);
+}
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java b/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java
new file mode 100644
index 0000000..3a20203
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java
@@ -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.
+ */
+package com.android.dialer.app.calllog;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receives broadcasts that should trigger a refresh of the missed call notification. This includes
+ * both an explicit broadcast from Telecom and a reboot.
+ */
+public class MissedCallNotificationReceiver extends BroadcastReceiver {
+
+ //TODO: Use compat class for these methods.
+ public static final String ACTION_SHOW_MISSED_CALLS_NOTIFICATION =
+ "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION";
+
+ public static final String EXTRA_NOTIFICATION_COUNT = "android.telecom.extra.NOTIFICATION_COUNT";
+
+ public static final String EXTRA_NOTIFICATION_PHONE_NUMBER =
+ "android.telecom.extra.NOTIFICATION_PHONE_NUMBER";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!ACTION_SHOW_MISSED_CALLS_NOTIFICATION.equals(action)) {
+ return;
+ }
+
+ int count =
+ intent.getIntExtra(
+ EXTRA_NOTIFICATION_COUNT, CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT);
+ String number = intent.getStringExtra(EXTRA_NOTIFICATION_PHONE_NUMBER);
+ CallLogNotificationsService.updateMissedCallNotifications(context, count, number);
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
new file mode 100644
index 0000000..2fa3dae
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
@@ -0,0 +1,330 @@
+/*
+ * 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.app.calllog;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.UserManagerCompat;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.app.contactinfo.ContactPhotoLoader;
+import com.android.dialer.app.list.ListsFragment;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import java.util.List;
+
+/** Creates a notification for calls that the user missed (neither answered nor rejected). */
+public class MissedCallNotifier {
+
+ /** The tag used to identify notifications from this class. */
+ private static final String NOTIFICATION_TAG = "MissedCallNotifier";
+ /** The identifier of the notification of new missed calls. */
+ private static final int NOTIFICATION_ID = 1;
+
+ private static MissedCallNotifier sInstance;
+ private Context mContext;
+ private CallLogNotificationsHelper mCalllogNotificationsHelper;
+
+ @VisibleForTesting
+ MissedCallNotifier(Context context, CallLogNotificationsHelper callLogNotificationsHelper) {
+ mContext = context;
+ mCalllogNotificationsHelper = callLogNotificationsHelper;
+ }
+
+ /** Returns the singleton instance of the {@link MissedCallNotifier}. */
+ public static MissedCallNotifier getInstance(Context context) {
+ if (sInstance == null) {
+ CallLogNotificationsHelper callLogNotificationsHelper =
+ CallLogNotificationsHelper.getInstance(context);
+ sInstance = new MissedCallNotifier(context, callLogNotificationsHelper);
+ }
+ return sInstance;
+ }
+
+ /**
+ * Creates a missed call notification with a post call message if there are no existing missed
+ * calls.
+ */
+ public void createPostCallMessageNotification(String number, String message) {
+ int count = CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT;
+ if (ConfigProviderBindings.get(mContext).getBoolean("enable_call_compose", false)) {
+ updateMissedCallNotification(count, number, message);
+ } else {
+ updateMissedCallNotification(count, number, null);
+ }
+ }
+
+ /** Creates a missed call notification. */
+ public void updateMissedCallNotification(int count, String number) {
+ updateMissedCallNotification(count, number, null);
+ }
+
+ private void updateMissedCallNotification(
+ int count, String number, @Nullable String postCallMessage) {
+ final int titleResId;
+ CharSequence expandedText; // The text in the notification's line 1 and 2.
+
+ final List<NewCall> newCalls = mCalllogNotificationsHelper.getNewMissedCalls();
+
+ if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
+ if (newCalls == null) {
+ // If the intent did not contain a count, and we are unable to get a count from the
+ // call log, then no notification can be shown.
+ return;
+ }
+ count = newCalls.size();
+ }
+
+ if (count == 0) {
+ // No voicemails to notify about: clear the notification.
+ clearMissedCalls();
+ return;
+ }
+
+ // The call log has been updated, use that information preferentially.
+ boolean useCallLog = newCalls != null && newCalls.size() == count;
+ NewCall newestCall = useCallLog ? newCalls.get(0) : null;
+ long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
+ String missedNumber = useCallLog ? newestCall.number : number;
+
+ Notification.Builder builder = new Notification.Builder(mContext);
+ // Display the first line of the notification:
+ // 1 missed call: <caller name || handle>
+ // More than 1 missed call: <number of calls> + "missed calls"
+ if (count == 1) {
+ //TODO: look up caller ID that is not in contacts.
+ ContactInfo contactInfo =
+ mCalllogNotificationsHelper.getContactInfo(
+ missedNumber,
+ useCallLog ? newestCall.numberPresentation : Calls.PRESENTATION_ALLOWED,
+ useCallLog ? newestCall.countryIso : null);
+
+ titleResId =
+ contactInfo.userType == ContactsUtils.USER_TYPE_WORK
+ ? R.string.notification_missedWorkCallTitle
+ : R.string.notification_missedCallTitle;
+ if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
+ || TextUtils.equals(contactInfo.name, contactInfo.number)) {
+ expandedText =
+ PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance()
+ .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
+ } else {
+ expandedText = contactInfo.name;
+ }
+
+ if (!TextUtils.isEmpty(postCallMessage)) {
+ // Ex. "John Doe: Hey dude"
+ expandedText =
+ mContext.getString(
+ R.string.post_call_notification_message, expandedText, postCallMessage);
+ }
+ ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo);
+ Bitmap photoIcon = loader.loadPhotoIcon();
+ if (photoIcon != null) {
+ builder.setLargeIcon(photoIcon);
+ }
+ } else {
+ titleResId = R.string.notification_missedCallsTitle;
+ expandedText = mContext.getString(R.string.notification_missedCallsMsg, count);
+ }
+
+ // Create a public viewable version of the notification, suitable for display when sensitive
+ // notification content is hidden.
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder
+ .setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ // Show "Phone" for notification title.
+ .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
+ // Notification details shows that there are missed call(s), but does not reveal
+ // the missed caller information.
+ .setContentText(mContext.getText(titleResId))
+ .setContentIntent(createCallLogPendingIntent())
+ .setAutoCancel(true)
+ .setWhen(timeMs)
+ .setShowWhen(true)
+ .setDeleteIntent(createClearMissedCallsPendingIntent());
+
+ // Create the notification suitable for display when sensitive information is showing.
+ builder
+ .setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ .setContentTitle(mContext.getText(titleResId))
+ .setContentText(expandedText)
+ .setContentIntent(createCallLogPendingIntent())
+ .setAutoCancel(true)
+ .setWhen(timeMs)
+ .setShowWhen(true)
+ .setDefaults(Notification.DEFAULT_VIBRATE)
+ .setDeleteIntent(createClearMissedCallsPendingIntent())
+ // Include a public version of the notification to be shown when the missed call
+ // notification is shown on the user's lock screen and they have chosen to hide
+ // sensitive notification information.
+ .setPublicVersion(publicBuilder.build());
+
+ // Add additional actions when there is only 1 missed call and the user isn't locked
+ if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) {
+ if (!TextUtils.isEmpty(missedNumber)
+ && !TextUtils.equals(missedNumber, mContext.getString(R.string.handle_restricted))) {
+ builder.addAction(
+ R.drawable.ic_phone_24dp,
+ mContext.getString(R.string.notification_missedCall_call_back),
+ createCallBackPendingIntent(missedNumber));
+
+ if (!PhoneNumberHelper.isUriNumber(missedNumber)) {
+ builder.addAction(
+ R.drawable.ic_message_24dp,
+ mContext.getString(R.string.notification_missedCall_message),
+ createSendSmsFromNotificationPendingIntent(missedNumber));
+ }
+ }
+ }
+
+ Notification notification = builder.build();
+ configureLedOnNotification(notification);
+
+ LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
+ getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+ }
+
+ private void clearMissedCalls() {
+ AsyncTask.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ // Call log is only accessible when unlocked. If that's the case, clear the list of
+ // new missed calls from the call log.
+ if (UserManagerCompat.isUserUnlocked(mContext)) {
+ ContentValues values = new ContentValues();
+ values.put(Calls.NEW, 0);
+ values.put(Calls.IS_READ, 1);
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1 AND ");
+ where.append(Calls.TYPE);
+ where.append(" = ?");
+ try {
+ mContext
+ .getContentResolver()
+ .update(
+ Calls.CONTENT_URI,
+ values,
+ where.toString(),
+ new String[] {Integer.toString(Calls.MISSED_TYPE)});
+ } catch (IllegalArgumentException e) {
+ LogUtil.e(
+ "MissedCallNotifier.clearMissedCalls",
+ "contacts provider update command failed",
+ e);
+ }
+ }
+ getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+ }
+ });
+ }
+
+ /** Trigger an intent to make a call from a missed call number. */
+ public void callBackFromMissedCall(String number) {
+ closeSystemDialogs(mContext);
+ CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ DialerUtils.startActivityWithErrorToast(
+ mContext,
+ new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION)
+ .build()
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+
+ /** Trigger an intent to send an sms from a missed call number. */
+ public void sendSmsFromMissedCall(String number) {
+ closeSystemDialogs(mContext);
+ CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ DialerUtils.startActivityWithErrorToast(
+ mContext, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+
+ /**
+ * Creates a new pending intent that sends the user to the call log.
+ *
+ * @return The pending intent.
+ */
+ private PendingIntent createCallLogPendingIntent() {
+ Intent contentIntent =
+ DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_HISTORY);
+ return PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ /** Creates a pending intent that marks all new missed calls as old. */
+ private PendingIntent createClearMissedCallsPendingIntent() {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ private PendingIntent createCallBackPendingIntent(String number) {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
+ intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
+ // extra.
+ return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private PendingIntent createSendSmsFromNotificationPendingIntent(String number) {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
+ intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
+ // extra.
+ return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ /** Configures a notification to emit the blinky notification light. */
+ private void configureLedOnNotification(Notification notification) {
+ notification.flags |= Notification.FLAG_SHOW_LIGHTS;
+ notification.defaults |= Notification.DEFAULT_LIGHTS;
+ }
+
+ /** Closes open system dialogs and the notification shade. */
+ private void closeSystemDialogs(Context context) {
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ }
+
+ private NotificationManager getNotificationMgr() {
+ return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java b/java/com/android/dialer/app/calllog/PhoneAccountUtils.java
new file mode 100644
index 0000000..c6d94d3
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/PhoneAccountUtils.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2013 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.app.calllog;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.dialer.telecom.TelecomUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Methods to help extract {@code PhoneAccount} information from database and Telecomm sources. */
+public class PhoneAccountUtils {
+
+ /** Return a list of phone accounts that are subscription/SIM accounts. */
+ public static List<PhoneAccountHandle> getSubscriptionPhoneAccounts(Context context) {
+ List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<PhoneAccountHandle>();
+ final List<PhoneAccountHandle> accountHandles =
+ TelecomUtil.getCallCapablePhoneAccounts(context);
+ for (PhoneAccountHandle accountHandle : accountHandles) {
+ PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+ subscriptionAccountHandles.add(accountHandle);
+ }
+ }
+ return subscriptionAccountHandles;
+ }
+
+ /** Compose PhoneAccount object from component name and account id. */
+ @Nullable
+ public static PhoneAccountHandle getAccount(
+ @Nullable String componentString, @Nullable String accountId) {
+ if (TextUtils.isEmpty(componentString) || TextUtils.isEmpty(accountId)) {
+ return null;
+ }
+ final ComponentName componentName = ComponentName.unflattenFromString(componentString);
+ if (componentName == null) {
+ return null;
+ }
+ return new PhoneAccountHandle(componentName, accountId);
+ }
+
+ /** Extract account label from PhoneAccount object. */
+ @Nullable
+ public static String getAccountLabel(
+ Context context, @Nullable PhoneAccountHandle accountHandle) {
+ PhoneAccount account = getAccountOrNull(context, accountHandle);
+ if (account != null && account.getLabel() != null) {
+ return account.getLabel().toString();
+ }
+ return null;
+ }
+
+ /** Extract account color from PhoneAccount object. */
+ public static int getAccountColor(Context context, @Nullable PhoneAccountHandle accountHandle) {
+ final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
+
+ // For single-sim devices the PhoneAccount will be NO_HIGHLIGHT_COLOR by default, so it is
+ // safe to always use the account highlight color.
+ return account == null ? PhoneAccount.NO_HIGHLIGHT_COLOR : account.getHighlightColor();
+ }
+
+ /**
+ * Determine whether a phone account supports call subjects.
+ *
+ * @return {@code true} if call subjects are supported, {@code false} otherwise.
+ */
+ public static boolean getAccountSupportsCallSubject(
+ Context context, @Nullable PhoneAccountHandle accountHandle) {
+ final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
+
+ return account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
+ }
+
+ /**
+ * Retrieve the account metadata, but if the account does not exist or the device has only a
+ * single registered and enabled account, return null.
+ */
+ @Nullable
+ private static PhoneAccount getAccountOrNull(
+ Context context, @Nullable PhoneAccountHandle accountHandle) {
+ if (TelecomUtil.getCallCapablePhoneAccounts(context).size() <= 1) {
+ return null;
+ }
+ return TelecomUtil.getPhoneAccount(context, accountHandle);
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
new file mode 100644
index 0000000..b18270b
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccount;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.TextView;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.DialerUtils;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
+
+/** Helper class to fill in the views in {@link PhoneCallDetailsViews}. */
+public class PhoneCallDetailsHelper {
+
+ /** The maximum number of icons will be shown to represent the call types in a group. */
+ private static final int MAX_CALL_TYPE_ICONS = 3;
+
+ private final Context mContext;
+ private final Resources mResources;
+ private final CallLogCache mCallLogCache;
+ /** Calendar used to construct dates */
+ private final Calendar mCalendar;
+ /** The injected current time in milliseconds since the epoch. Used only by tests. */
+ private Long mCurrentTimeMillisForTest;
+
+ private CharSequence mPhoneTypeLabelForTest;
+ /** List of items to be concatenated together for accessibility descriptions */
+ private ArrayList<CharSequence> mDescriptionItems = new ArrayList<>();
+
+ /**
+ * Creates a new instance of the helper.
+ *
+ * <p>Generally you should have a single instance of this helper in any context.
+ *
+ * @param resources used to look up strings
+ */
+ public PhoneCallDetailsHelper(Context context, Resources resources, CallLogCache callLogCache) {
+ mContext = context;
+ mResources = resources;
+ mCallLogCache = callLogCache;
+ mCalendar = Calendar.getInstance();
+ }
+
+ /** Fills the call details views with content. */
+ public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details) {
+ // Display up to a given number of icons.
+ views.callTypeIcons.clear();
+ int count = details.callTypes.length;
+ boolean isVoicemail = false;
+ for (int index = 0; index < count && index < MAX_CALL_TYPE_ICONS; ++index) {
+ views.callTypeIcons.add(details.callTypes[index]);
+ if (index == 0) {
+ isVoicemail = details.callTypes[index] == Calls.VOICEMAIL_TYPE;
+ }
+ }
+
+ // Show the video icon if the call had video enabled.
+ views.callTypeIcons.setShowVideo(
+ (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO);
+ views.callTypeIcons.requestLayout();
+ views.callTypeIcons.setVisibility(View.VISIBLE);
+
+ // Show the total call count only if there are more than the maximum number of icons.
+ final Integer callCount;
+ if (count > MAX_CALL_TYPE_ICONS) {
+ callCount = count;
+ } else {
+ callCount = null;
+ }
+
+ // Set the call count, location, date and if voicemail, set the duration.
+ setDetailText(views, callCount, details);
+
+ // Set the account label if it exists.
+ String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle);
+ if (!TextUtils.isEmpty(details.viaNumber)) {
+ if (!TextUtils.isEmpty(accountLabel)) {
+ accountLabel =
+ mResources.getString(
+ R.string.call_log_via_number_phone_account, accountLabel, details.viaNumber);
+ } else {
+ accountLabel = mResources.getString(R.string.call_log_via_number, details.viaNumber);
+ }
+ }
+ if (!TextUtils.isEmpty(accountLabel)) {
+ views.callAccountLabel.setVisibility(View.VISIBLE);
+ views.callAccountLabel.setText(accountLabel);
+ int color = mCallLogCache.getAccountColor(details.accountHandle);
+ if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
+ int defaultColor = R.color.dialer_secondary_text_color;
+ views.callAccountLabel.setTextColor(mContext.getResources().getColor(defaultColor));
+ } else {
+ views.callAccountLabel.setTextColor(color);
+ }
+ } else {
+ views.callAccountLabel.setVisibility(View.GONE);
+ }
+
+ final CharSequence nameText;
+ final CharSequence displayNumber = details.displayNumber;
+ if (TextUtils.isEmpty(details.getPreferredName())) {
+ nameText = displayNumber;
+ // We have a real phone number as "nameView" so make it always LTR
+ views.nameView.setTextDirection(View.TEXT_DIRECTION_LTR);
+ } else {
+ nameText = details.getPreferredName();
+ }
+
+ views.nameView.setText(nameText);
+
+ if (isVoicemail) {
+ views.voicemailTranscriptionView.setText(
+ TextUtils.isEmpty(details.transcription) ? null : details.transcription);
+ }
+
+ // Bold if not read
+ Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD;
+ views.nameView.setTypeface(typeface);
+ views.voicemailTranscriptionView.setTypeface(typeface);
+ views.callLocationAndDate.setTypeface(typeface);
+ views.callLocationAndDate.setTextColor(
+ ContextCompat.getColor(
+ mContext,
+ details.isRead ? R.color.call_log_detail_color : R.color.call_log_unread_text_color));
+ }
+
+ /**
+ * Builds a string containing the call location and date. For voicemail logs only the call date is
+ * returned because location information is displayed in the call action button
+ *
+ * @param details The call details.
+ * @return The call location and date string.
+ */
+ public CharSequence getCallLocationAndDate(PhoneCallDetails details) {
+ mDescriptionItems.clear();
+
+ if (details.callTypes[0] != Calls.VOICEMAIL_TYPE) {
+ // Get type of call (ie mobile, home, etc) if known, or the caller's location.
+ CharSequence callTypeOrLocation = getCallTypeOrLocation(details);
+
+ // Only add the call type or location if its not empty. It will be empty for unknown
+ // callers.
+ if (!TextUtils.isEmpty(callTypeOrLocation)) {
+ mDescriptionItems.add(callTypeOrLocation);
+ }
+ }
+
+ // The date of this call
+ mDescriptionItems.add(getCallDate(details));
+
+ // Create a comma separated list from the call type or location, and call date.
+ return DialerUtils.join(mDescriptionItems);
+ }
+
+ /**
+ * For a call, if there is an associated contact for the caller, return the known call type (e.g.
+ * mobile, home, work). If there is no associated contact, attempt to use the caller's location if
+ * known.
+ *
+ * @param details Call details to use.
+ * @return Type of call (mobile/home) if known, or the location of the caller (if known).
+ */
+ public CharSequence getCallTypeOrLocation(PhoneCallDetails details) {
+ if (details.isSpam) {
+ return mResources.getString(R.string.spam_number_call_log_label);
+ } else if (details.isBlocked) {
+ return mResources.getString(R.string.blocked_number_call_log_label);
+ }
+
+ CharSequence numberFormattedLabel = null;
+ // Only show a label if the number is shown and it is not a SIP address.
+ if (!TextUtils.isEmpty(details.number)
+ && !PhoneNumberHelper.isUriNumber(details.number.toString())
+ && !mCallLogCache.isVoicemailNumber(details.accountHandle, details.number)) {
+
+ if (TextUtils.isEmpty(details.namePrimary) && !TextUtils.isEmpty(details.geocode)) {
+ numberFormattedLabel = details.geocode;
+ } else if (!(details.numberType == Phone.TYPE_CUSTOM
+ && TextUtils.isEmpty(details.numberLabel))) {
+ // Get type label only if it will not be "Custom" because of an empty number label.
+ numberFormattedLabel =
+ mPhoneTypeLabelForTest != null
+ ? mPhoneTypeLabelForTest
+ : Phone.getTypeLabel(mResources, details.numberType, details.numberLabel);
+ }
+ }
+
+ if (!TextUtils.isEmpty(details.namePrimary) && TextUtils.isEmpty(numberFormattedLabel)) {
+ numberFormattedLabel = details.displayNumber;
+ }
+ return numberFormattedLabel;
+ }
+
+ public void setPhoneTypeLabelForTest(CharSequence phoneTypeLabel) {
+ this.mPhoneTypeLabelForTest = phoneTypeLabel;
+ }
+
+ /**
+ * Get the call date/time of the call. For the call log this is relative to the current time. e.g.
+ * 3 minutes ago. For voicemail, see {@link #getGranularDateTime(PhoneCallDetails)}
+ *
+ * @param details Call details to use.
+ * @return String representing when the call occurred.
+ */
+ public CharSequence getCallDate(PhoneCallDetails details) {
+ if (details.callTypes[0] == Calls.VOICEMAIL_TYPE) {
+ return getGranularDateTime(details);
+ }
+
+ return DateUtils.getRelativeTimeSpanString(
+ details.date,
+ getCurrentTimeMillis(),
+ DateUtils.MINUTE_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_RELATIVE);
+ }
+
+ /**
+ * Get the granular version of the call date/time of the call. The result is always in the form
+ * 'DATE at TIME'. The date value changes based on when the call was created.
+ *
+ * <p>If created today, DATE is 'Today' If created this year, DATE is 'MMM dd' Otherwise, DATE is
+ * 'MMM dd, yyyy'
+ *
+ * <p>TIME is the localized time format, e.g. 'hh:mm a' or 'HH:mm'
+ *
+ * @param details Call details to use
+ * @return String representing when the call occurred
+ */
+ public CharSequence getGranularDateTime(PhoneCallDetails details) {
+ return mResources.getString(
+ R.string.voicemailCallLogDateTimeFormat,
+ getGranularDate(details.date),
+ DateUtils.formatDateTime(mContext, details.date, DateUtils.FORMAT_SHOW_TIME));
+ }
+
+ /**
+ * Get the granular version of the call date. See {@link #getGranularDateTime(PhoneCallDetails)}
+ */
+ private String getGranularDate(long date) {
+ if (DateUtils.isToday(date)) {
+ return mResources.getString(R.string.voicemailCallLogToday);
+ }
+ return DateUtils.formatDateTime(
+ mContext,
+ date,
+ DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_ABBREV_MONTH
+ | (shouldShowYear(date) ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR));
+ }
+
+ /**
+ * Determines whether the year should be shown for the given date
+ *
+ * @return {@code true} if date is within the current year, {@code false} otherwise
+ */
+ private boolean shouldShowYear(long date) {
+ mCalendar.setTimeInMillis(getCurrentTimeMillis());
+ int currentYear = mCalendar.get(Calendar.YEAR);
+ mCalendar.setTimeInMillis(date);
+ return currentYear != mCalendar.get(Calendar.YEAR);
+ }
+
+ /** Sets the text of the header view for the details page of a phone call. */
+ public void setCallDetailsHeader(TextView nameView, PhoneCallDetails details) {
+ final CharSequence nameText;
+ if (!TextUtils.isEmpty(details.namePrimary)) {
+ nameText = details.namePrimary;
+ } else if (!TextUtils.isEmpty(details.displayNumber)) {
+ nameText = details.displayNumber;
+ } else {
+ nameText = mResources.getString(R.string.unknown);
+ }
+
+ nameView.setText(nameText);
+ }
+
+ public void setCurrentTimeForTest(long currentTimeMillis) {
+ mCurrentTimeMillisForTest = currentTimeMillis;
+ }
+
+ /**
+ * Returns the current time in milliseconds since the epoch.
+ *
+ * <p>It can be injected in tests using {@link #setCurrentTimeForTest(long)}.
+ */
+ private long getCurrentTimeMillis() {
+ if (mCurrentTimeMillisForTest == null) {
+ return System.currentTimeMillis();
+ } else {
+ return mCurrentTimeMillisForTest;
+ }
+ }
+
+ /** Sets the call count, date, and if it is a voicemail, sets the duration. */
+ private void setDetailText(
+ PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details) {
+ // Combine the count (if present) and the date.
+ CharSequence dateText = details.callLocationAndDate;
+ final CharSequence text;
+ if (callCount != null) {
+ text = mResources.getString(R.string.call_log_item_count_and_date, callCount, dateText);
+ } else {
+ text = dateText;
+ }
+
+ if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) {
+ views.callLocationAndDate.setText(
+ mResources.getString(
+ R.string.voicemailCallLogDateTimeFormatWithDuration,
+ text,
+ getVoicemailDuration(details)));
+ } else {
+ views.callLocationAndDate.setText(text);
+ }
+ }
+
+ private String getVoicemailDuration(PhoneCallDetails details) {
+ long minutes = TimeUnit.SECONDS.toMinutes(details.duration);
+ long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes);
+ if (minutes > 99) {
+ minutes = 99;
+ }
+ return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds);
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
new file mode 100644
index 0000000..4769968
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.TextView;
+import com.android.dialer.app.R;
+
+/** Encapsulates the views that are used to display the details of a phone call in the call log. */
+public final class PhoneCallDetailsViews {
+
+ public final TextView nameView;
+ public final View callTypeView;
+ public final CallTypeIconsView callTypeIcons;
+ public final TextView callLocationAndDate;
+ public final TextView voicemailTranscriptionView;
+ public final TextView callAccountLabel;
+
+ private PhoneCallDetailsViews(
+ TextView nameView,
+ View callTypeView,
+ CallTypeIconsView callTypeIcons,
+ TextView callLocationAndDate,
+ TextView voicemailTranscriptionView,
+ TextView callAccountLabel) {
+ this.nameView = nameView;
+ this.callTypeView = callTypeView;
+ this.callTypeIcons = callTypeIcons;
+ this.callLocationAndDate = callLocationAndDate;
+ this.voicemailTranscriptionView = voicemailTranscriptionView;
+ this.callAccountLabel = callAccountLabel;
+ }
+
+ /**
+ * Create a new instance by extracting the elements from the given view.
+ *
+ * <p>The view should contain three text views with identifiers {@code R.id.name}, {@code
+ * R.id.date}, and {@code R.id.number}, and a linear layout with identifier {@code
+ * R.id.call_types}.
+ */
+ public static PhoneCallDetailsViews fromView(View view) {
+ return new PhoneCallDetailsViews(
+ (TextView) view.findViewById(R.id.name),
+ view.findViewById(R.id.call_type),
+ (CallTypeIconsView) view.findViewById(R.id.call_type_icons),
+ (TextView) view.findViewById(R.id.call_location_and_date),
+ (TextView) view.findViewById(R.id.voicemail_transcription),
+ (TextView) view.findViewById(R.id.call_account_label));
+ }
+
+ public static PhoneCallDetailsViews createForTest(Context context) {
+ return new PhoneCallDetailsViews(
+ new TextView(context),
+ new View(context),
+ new CallTypeIconsView(context),
+ new TextView(context),
+ new TextView(context),
+ new TextView(context));
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java b/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java
new file mode 100644
index 0000000..410d4cc
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.Context;
+import android.provider.CallLog.Calls;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.app.R;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+/** Helper for formatting and managing the display of phone numbers. */
+public class PhoneNumberDisplayUtil {
+
+ /** Returns the string to display for the given phone number if there is no matching contact. */
+ /* package */
+ static CharSequence getDisplayName(
+ Context context, CharSequence number, int presentation, boolean isVoicemail) {
+ if (presentation == Calls.PRESENTATION_UNKNOWN) {
+ return context.getResources().getString(R.string.unknown);
+ }
+ if (presentation == Calls.PRESENTATION_RESTRICTED) {
+ return PhoneNumberHelper.getDisplayNameForRestrictedNumber(context);
+ }
+ if (presentation == Calls.PRESENTATION_PAYPHONE) {
+ return context.getResources().getString(R.string.payphone);
+ }
+ if (isVoicemail) {
+ return context.getResources().getString(R.string.voicemail);
+ }
+ if (PhoneNumberHelper.isLegacyUnknownNumbers(number)) {
+ return context.getResources().getString(R.string.unknown);
+ }
+ return "";
+ }
+
+ /**
+ * Returns the string to display for the given phone number.
+ *
+ * @param number the number to display
+ * @param formattedNumber the formatted number if available, may be null
+ */
+ public static CharSequence getDisplayNumber(
+ Context context,
+ CharSequence number,
+ int presentation,
+ CharSequence formattedNumber,
+ CharSequence postDialDigits,
+ boolean isVoicemail) {
+ final CharSequence displayName = getDisplayName(context, number, presentation, isVoicemail);
+ if (!TextUtils.isEmpty(displayName)) {
+ return getTtsSpannableLtrNumber(displayName);
+ }
+
+ if (!TextUtils.isEmpty(formattedNumber)) {
+ return getTtsSpannableLtrNumber(formattedNumber);
+ } else if (!TextUtils.isEmpty(number)) {
+ return getTtsSpannableLtrNumber(number.toString() + postDialDigits);
+ } else {
+ return context.getResources().getString(R.string.unknown);
+ }
+ }
+
+ /** Returns number annotated as phone number in LTR direction. */
+ public static CharSequence getTtsSpannableLtrNumber(CharSequence number) {
+ return PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance().unicodeWrap(number.toString(), TextDirectionHeuristics.LTR));
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java
new file mode 100644
index 0000000..e539cee
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.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.app.calllog;
+
+import android.app.Activity;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.app.R;
+import com.android.dialer.app.list.ListsFragment;
+import com.android.dialer.app.voicemail.VoicemailAudioManager;
+import com.android.dialer.app.voicemail.VoicemailErrorManager;
+import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.common.LogUtil;
+
+public class VisualVoicemailCallLogFragment extends CallLogFragment {
+
+ private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
+ private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+
+ private VoicemailErrorManager mVoicemailAlertManager;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+ mCallTypeFilter = CallLog.Calls.VOICEMAIL_TYPE;
+ mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), state);
+ getActivity()
+ .getContentResolver()
+ .registerContentObserver(
+ VoicemailContract.Status.CONTENT_URI, true, mVoicemailStatusObserver);
+ }
+
+ @Override
+ protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() {
+ return mVoicemailPlaybackPresenter;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mVoicemailAlertManager =
+ new VoicemailErrorManager(getContext(), getAdapter().getAlertManager(), mModalAlertManager);
+ getActivity()
+ .getContentResolver()
+ .registerContentObserver(
+ VoicemailContract.Status.CONTENT_URI,
+ true,
+ mVoicemailAlertManager.getContentObserver());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ View view = inflater.inflate(R.layout.call_log_fragment, container, false);
+ setupView(view);
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mVoicemailPlaybackPresenter.onResume();
+ mVoicemailAlertManager.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ mVoicemailPlaybackPresenter.onPause();
+ mVoicemailAlertManager.onPause();
+ super.onPause();
+ }
+
+ @Override
+ public void onDestroy() {
+ getActivity()
+ .getContentResolver()
+ .unregisterContentObserver(mVoicemailAlertManager.getContentObserver());
+ mVoicemailPlaybackPresenter.onDestroy();
+ getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void fetchCalls() {
+ super.fetchCalls();
+ ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+ }
+
+ @Override
+ public void onPageResume(@Nullable Activity activity) {
+ LogUtil.d("VisualVoicemailCallLogFragment.onPageResume", null);
+ super.onPageResume(activity);
+ if (activity != null) {
+ activity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM);
+ }
+ }
+
+ @Override
+ public void onPagePause(@Nullable Activity activity) {
+ LogUtil.d("VisualVoicemailCallLogFragment.onPagePause", null);
+ super.onPagePause(activity);
+ if (activity != null) {
+ activity.setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
new file mode 100644
index 0000000..d6d8354
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 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.app.calllog;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.CallLog.Calls;
+import android.util.Log;
+
+/** Handles asynchronous queries to the call log for voicemail. */
+public class VoicemailQueryHandler extends AsyncQueryHandler {
+
+ private static final String TAG = "VoicemailQueryHandler";
+
+ /** The token for the query to mark all new voicemails as old. */
+ private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 50;
+
+ private Context mContext;
+
+ public VoicemailQueryHandler(Context context, ContentResolver contentResolver) {
+ super(contentResolver);
+ mContext = context;
+ }
+
+ /** Updates all new voicemails to mark them as old. */
+ public void markNewVoicemailsAsOld() {
+ // Mark all "new" voicemails as not new anymore.
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1 AND ");
+ where.append(Calls.TYPE);
+ where.append(" = ?");
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.NEW, "0");
+
+ startUpdate(
+ UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN,
+ null,
+ Calls.CONTENT_URI_WITH_VOICEMAIL,
+ values,
+ where.toString(),
+ new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)});
+ }
+
+ @Override
+ protected void onUpdateComplete(int token, Object cookie, int result) {
+ if (token == UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN) {
+ if (mContext != null) {
+ Intent serviceIntent = new Intent(mContext, CallLogNotificationsService.class);
+ serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
+ mContext.startService(serviceIntent);
+ } else {
+ Log.w(TAG, "Unknown update completed: ignoring: " + token);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java
new file mode 100644
index 0000000..7645a33
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2015 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.app.calllog.calllogcache;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.app.calllog.CallLogAdapter;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.CallUtil;
+
+/**
+ * This is the base class for the CallLogCaches.
+ *
+ * <p>Keeps a cache of recently made queries to the Telecom/Telephony processes. The aim of this
+ * cache is to reduce the number of cross-process requests to TelecomManager, which can negatively
+ * affect performance.
+ *
+ * <p>This is designed with the specific use case of the {@link CallLogAdapter} in mind.
+ */
+public abstract class CallLogCache {
+ // TODO: Dialer should be fixed so as not to check isVoicemail() so often but at the time of
+ // this writing, that was a much larger undertaking than creating this cache.
+
+ protected final Context mContext;
+
+ private boolean mHasCheckedForVideoAvailability;
+ private int mVideoAvailability;
+
+ public CallLogCache(Context context) {
+ mContext = context;
+ }
+
+ /** Return the most compatible version of the TelecomCallLogCache. */
+ public static CallLogCache getCallLogCache(Context context) {
+ if (CompatUtils.isClassAvailable("android.telecom.PhoneAccountHandle")) {
+ return new CallLogCacheLollipopMr1(context);
+ }
+ return new CallLogCacheLollipop(context);
+ }
+
+ public void reset() {
+ mHasCheckedForVideoAvailability = false;
+ mVideoAvailability = 0;
+ }
+
+ /**
+ * Returns true if the given number is the number of the configured voicemail. To be able to
+ * mock-out this, it is not a static method.
+ */
+ public abstract boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number);
+
+ /**
+ * Returns {@code true} when the current sim supports video calls, regardless of the value in a
+ * contact's {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE}
+ * column.
+ */
+ public boolean isVideoEnabled() {
+ if (!mHasCheckedForVideoAvailability) {
+ mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext);
+ mHasCheckedForVideoAvailability = true;
+ }
+ return (mVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) != 0;
+ }
+
+ /**
+ * Returns {@code true} when the current sim supports checking video calling capabilities via the
+ * {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} column.
+ */
+ public boolean canRelyOnVideoPresence() {
+ if (!mHasCheckedForVideoAvailability) {
+ mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext);
+ mHasCheckedForVideoAvailability = true;
+ }
+ return (mVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
+ }
+
+ /** Extract account label from PhoneAccount object. */
+ public abstract String getAccountLabel(PhoneAccountHandle accountHandle);
+
+ /** Extract account color from PhoneAccount object. */
+ public abstract int getAccountColor(PhoneAccountHandle accountHandle);
+
+ /**
+ * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note)
+ * for outgoing calls.
+ *
+ * @param accountHandle The PhoneAccount handle.
+ * @return {@code true} if calling with a note is supported, {@code false} otherwise.
+ */
+ public abstract boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle);
+}
diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java
new file mode 100644
index 0000000..78aaa41
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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.app.calllog.calllogcache;
+
+import android.content.Context;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+/**
+ * This is a compatibility class for the CallLogCache for versions of dialer before Lollipop Mr1
+ * (the introduction of phone accounts).
+ *
+ * <p>This class should not be initialized directly and instead be acquired from {@link
+ * CallLogCache#getCallLogCache}.
+ */
+class CallLogCacheLollipop extends CallLogCache {
+
+ private String mVoicemailNumber;
+
+ /* package */ CallLogCacheLollipop(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) {
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+
+ String numberString = number.toString();
+
+ if (!TextUtils.isEmpty(mVoicemailNumber)) {
+ return PhoneNumberUtils.compare(numberString, mVoicemailNumber);
+ }
+
+ if (PhoneNumberUtils.isVoiceMailNumber(numberString)) {
+ mVoicemailNumber = numberString;
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public String getAccountLabel(PhoneAccountHandle accountHandle) {
+ return null;
+ }
+
+ @Override
+ public int getAccountColor(PhoneAccountHandle accountHandle) {
+ return PhoneAccount.NO_HIGHLIGHT_COLOR;
+ }
+
+ @Override
+ public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) {
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
new file mode 100644
index 0000000..c342b7e
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2013 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.app.calllog.calllogcache;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.Pair;
+import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * This is the CallLogCache for versions of dialer Lollipop Mr1 and above with support for multi-SIM
+ * devices.
+ *
+ * <p>This class should not be initialized directly and instead be acquired from {@link
+ * CallLogCache#getCallLogCache}.
+ */
+class CallLogCacheLollipopMr1 extends CallLogCache {
+
+ /*
+ * Maps from a phone-account/number pair to a boolean because multiple numbers could return true
+ * for the voicemail number if those numbers are not pre-normalized. Access must be synchronzied
+ * as it's used in the background thread in CallLogAdapter. {@see CallLogAdapter#loadData}
+ */
+ @VisibleForTesting
+ final Map<Pair<PhoneAccountHandle, CharSequence>, Boolean> mVoicemailQueryCache =
+ new ConcurrentHashMap<>();
+
+ private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new HashMap<>();
+ private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new HashMap<>();
+ private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new HashMap<>();
+
+ /* package */ CallLogCacheLollipopMr1(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void reset() {
+ mVoicemailQueryCache.clear();
+ mPhoneAccountLabelCache.clear();
+ mPhoneAccountColorCache.clear();
+ mPhoneAccountCallWithNoteCache.clear();
+
+ super.reset();
+ }
+
+ @Override
+ public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) {
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+
+ Pair<PhoneAccountHandle, CharSequence> key = new Pair<>(accountHandle, number);
+ Boolean value = mVoicemailQueryCache.get(key);
+ if (value != null) {
+ return value;
+ }
+ boolean isVoicemail =
+ PhoneNumberHelper.isVoicemailNumber(mContext, accountHandle, number.toString());
+ mVoicemailQueryCache.put(key, isVoicemail);
+ return isVoicemail;
+ }
+
+ @Override
+ public String getAccountLabel(PhoneAccountHandle accountHandle) {
+ if (mPhoneAccountLabelCache.containsKey(accountHandle)) {
+ return mPhoneAccountLabelCache.get(accountHandle);
+ } else {
+ String label = PhoneAccountUtils.getAccountLabel(mContext, accountHandle);
+ mPhoneAccountLabelCache.put(accountHandle, label);
+ return label;
+ }
+ }
+
+ @Override
+ public int getAccountColor(PhoneAccountHandle accountHandle) {
+ if (mPhoneAccountColorCache.containsKey(accountHandle)) {
+ return mPhoneAccountColorCache.get(accountHandle);
+ } else {
+ Integer color = PhoneAccountUtils.getAccountColor(mContext, accountHandle);
+ mPhoneAccountColorCache.put(accountHandle, color);
+ return color;
+ }
+ }
+
+ @Override
+ public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) {
+ if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) {
+ return mPhoneAccountCallWithNoteCache.get(accountHandle);
+ } else {
+ Boolean supportsCallWithNote =
+ PhoneAccountUtils.getAccountSupportsCallSubject(mContext, accountHandle);
+ mPhoneAccountCallWithNoteCache.put(accountHandle, supportsCallWithNote);
+ return supportsCallWithNote;
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
new file mode 100644
index 0000000..4135cb7
--- /dev/null
+++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2015 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.app.contactinfo;
+
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.util.ExpirableCache;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.PriorityBlockingQueue;
+
+/**
+ * This is a cache of contact details for the phone numbers in the c all log. The key is the phone
+ * number with the country in which teh call was placed or received. The content of the cache is
+ * expired (but not purged) whenever the application comes to the foreground.
+ *
+ * <p>This cache queues request for information and queries for information on a background thread,
+ * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction
+ * as needed.
+ *
+ * <p>TODO: Explore whether there is a pattern to remove external dependencies for starting and
+ * stopping the query thread.
+ */
+public class ContactInfoCache {
+
+ private static final int REDRAW = 1;
+ private static final int START_THREAD = 2;
+ private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000;
+
+ private final ExpirableCache<NumberWithCountryIso, ContactInfo> mCache;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final OnContactInfoChangedListener mOnContactInfoChangedListener;
+ private final BlockingQueue<ContactInfoRequest> mUpdateRequests;
+ private final Handler mHandler;
+ private QueryThread mContactInfoQueryThread;
+ private volatile boolean mRequestProcessingDisabled = false;
+
+ private static class InnerHandler extends Handler {
+
+ private final WeakReference<ContactInfoCache> contactInfoCacheWeakReference;
+
+ public InnerHandler(WeakReference<ContactInfoCache> contactInfoCacheWeakReference) {
+ this.contactInfoCacheWeakReference = contactInfoCacheWeakReference;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ ContactInfoCache reference = contactInfoCacheWeakReference.get();
+ if (reference == null) {
+ return;
+ }
+ switch (msg.what) {
+ case REDRAW:
+ reference.mOnContactInfoChangedListener.onContactInfoChanged();
+ break;
+ case START_THREAD:
+ reference.startRequestProcessing();
+ }
+ }
+ }
+
+ public ContactInfoCache(
+ @NonNull ExpirableCache<NumberWithCountryIso, ContactInfo> internalCache,
+ @NonNull ContactInfoHelper contactInfoHelper,
+ @NonNull OnContactInfoChangedListener listener) {
+ mCache = internalCache;
+ mContactInfoHelper = contactInfoHelper;
+ mOnContactInfoChangedListener = listener;
+ mUpdateRequests = new PriorityBlockingQueue<>();
+ mHandler = new InnerHandler(new WeakReference<>(this));
+ }
+
+ public ContactInfo getValue(
+ String number,
+ String countryIso,
+ ContactInfo callLogContactInfo,
+ boolean remoteLookupIfNotFoundLocally) {
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ ExpirableCache.CachedValue<ContactInfo> cachedInfo = mCache.getCachedValue(numberCountryIso);
+ ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
+ if (cachedInfo == null) {
+ mCache.put(numberCountryIso, ContactInfo.EMPTY);
+ // Use the cached contact info from the call log.
+ info = callLogContactInfo;
+ // The db request should happen on a non-UI thread.
+ // Request the contact details immediately since they are currently missing.
+ int requestType =
+ remoteLookupIfNotFoundLocally
+ ? ContactInfoRequest.TYPE_LOCAL_AND_REMOTE
+ : ContactInfoRequest.TYPE_LOCAL;
+ enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ true, requestType);
+ // We will format the phone number when we make the background request.
+ } else {
+ if (cachedInfo.isExpired()) {
+ // The contact info is no longer up to date, we should request it. However, we
+ // do not need to request them immediately.
+ enqueueRequest(
+ number,
+ countryIso,
+ callLogContactInfo, /* immediate */
+ false,
+ ContactInfoRequest.TYPE_LOCAL);
+ } else if (!callLogInfoMatches(callLogContactInfo, info)) {
+ // The call log information does not match the one we have, look it up again.
+ // We could simply update the call log directly, but that needs to be done in a
+ // background thread, so it is easier to simply request a new lookup, which will, as
+ // a side-effect, update the call log.
+ enqueueRequest(
+ number,
+ countryIso,
+ callLogContactInfo, /* immediate */
+ false,
+ ContactInfoRequest.TYPE_LOCAL);
+ }
+
+ if (info == ContactInfo.EMPTY) {
+ // Use the cached contact info from the call log.
+ info = callLogContactInfo;
+ }
+ }
+ return info;
+ }
+
+ /**
+ * Queries the appropriate content provider for the contact associated with the number.
+ *
+ * <p>Upon completion it also updates the cache in the call log, if it is different from {@code
+ * callLogInfo}.
+ *
+ * <p>The number might be either a SIP address or a phone number.
+ *
+ * <p>It returns true if it updated the content of the cache and we should therefore tell the view
+ * to update its content.
+ */
+ private boolean queryContactInfo(ContactInfoRequest request) {
+ ContactInfo info;
+ if (request.isLocalRequest()) {
+ info = mContactInfoHelper.lookupNumber(request.number, request.countryIso);
+ if (request.type == ContactInfoRequest.TYPE_LOCAL_AND_REMOTE) {
+ if (!mContactInfoHelper.hasName(info)) {
+ enqueueRequest(
+ request.number,
+ request.countryIso,
+ request.callLogInfo,
+ true,
+ ContactInfoRequest.TYPE_REMOTE);
+ return false;
+ }
+ }
+ } else {
+ info = mContactInfoHelper.lookupNumberInRemoteDirectory(request.number, request.countryIso);
+ }
+
+ if (info == null) {
+ // The lookup failed, just return without requesting to update the view.
+ return false;
+ }
+
+ // Check the existing entry in the cache: only if it has changed we should update the
+ // view.
+ NumberWithCountryIso numberCountryIso =
+ new NumberWithCountryIso(request.number, request.countryIso);
+ ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso);
+
+ final boolean isRemoteSource = info.sourceType != 0;
+
+ // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
+ // to avoid updating the data set for every new row that is scrolled into view.
+
+ // Exception: Photo uris for contacts from remote sources are not cached in the call log
+ // cache, so we have to force a redraw for these contacts regardless.
+ boolean updated =
+ (existingInfo != ContactInfo.EMPTY || isRemoteSource) && !info.equals(existingInfo);
+
+ // Store the data in the cache so that the UI thread can use to display it. Store it
+ // even if it has not changed so that it is marked as not expired.
+ mCache.put(numberCountryIso, info);
+
+ // Update the call log even if the cache it is up-to-date: it is possible that the cache
+ // contains the value from a different call log entry.
+ mContactInfoHelper.updateCallLogContactInfo(
+ request.number, request.countryIso, info, request.callLogInfo);
+ if (!request.isLocalRequest()) {
+ mContactInfoHelper.updateCachedNumberLookupService(info);
+ }
+ return updated;
+ }
+
+ /**
+ * After a delay, start the thread to begin processing requests. We perform lookups on a
+ * background thread, but this must be called to indicate the thread should be running.
+ */
+ public void start() {
+ // Schedule a thread-creation message if the thread hasn't been created yet, as an
+ // optimization to queue fewer messages.
+ if (mContactInfoQueryThread == null) {
+ // TODO: Check whether this delay before starting to process is necessary.
+ mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS);
+ }
+ }
+
+ /**
+ * Stops the thread and clears the queue of messages to process. This cleans up the thread for
+ * lookups so that it is not perpetually running.
+ */
+ public void stop() {
+ stopRequestProcessing();
+ }
+
+ /**
+ * Starts a background thread to process contact-lookup requests, unless one has already been
+ * started.
+ */
+ private synchronized void startRequestProcessing() {
+ // For unit-testing.
+ if (mRequestProcessingDisabled) {
+ return;
+ }
+
+ // If a thread is already started, don't start another.
+ if (mContactInfoQueryThread != null) {
+ return;
+ }
+
+ mContactInfoQueryThread = new QueryThread();
+ mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY);
+ mContactInfoQueryThread.start();
+ }
+
+ public void invalidate() {
+ mCache.expireAll();
+ stopRequestProcessing();
+ }
+
+ /**
+ * Stops the background thread that processes updates and cancels any pending requests to start
+ * it.
+ */
+ private synchronized void stopRequestProcessing() {
+ // Remove any pending requests to start the processing thread.
+ mHandler.removeMessages(START_THREAD);
+ if (mContactInfoQueryThread != null) {
+ // Stop the thread; we are finished with it.
+ mContactInfoQueryThread.stopProcessing();
+ mContactInfoQueryThread.interrupt();
+ mContactInfoQueryThread = null;
+ }
+ }
+
+ /**
+ * Enqueues a request to look up the contact details for the given phone number.
+ *
+ * <p>It also provides the current contact info stored in the call log for this number.
+ *
+ * <p>If the {@code immediate} parameter is true, it will start immediately the thread that looks
+ * up the contact information (if it has not been already started). Otherwise, it will be started
+ * with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MS}.
+ */
+ private void enqueueRequest(
+ String number,
+ String countryIso,
+ ContactInfo callLogInfo,
+ boolean immediate,
+ @ContactInfoRequest.TYPE int type) {
+ ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo, type);
+ if (!mUpdateRequests.contains(request)) {
+ mUpdateRequests.offer(request);
+ }
+
+ if (immediate) {
+ startRequestProcessing();
+ }
+ }
+
+ /** Checks whether the contact info from the call log matches the one from the contacts db. */
+ private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
+ // The call log only contains a subset of the fields in the contacts db. Only check those.
+ return TextUtils.equals(callLogInfo.name, info.name)
+ && callLogInfo.type == info.type
+ && TextUtils.equals(callLogInfo.label, info.label);
+ }
+
+ /** Sets whether processing of requests for contact details should be enabled. */
+ public void disableRequestProcessing() {
+ mRequestProcessingDisabled = true;
+ }
+
+ @VisibleForTesting
+ public void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ mCache.put(numberCountryIso, contactInfo);
+ }
+
+ public interface OnContactInfoChangedListener {
+
+ void onContactInfoChanged();
+ }
+
+ /*
+ * Handles requests for contact name and number type.
+ */
+ private class QueryThread extends Thread {
+
+ private volatile boolean mDone = false;
+
+ public QueryThread() {
+ super("ContactInfoCache.QueryThread");
+ }
+
+ public void stopProcessing() {
+ mDone = true;
+ }
+
+ @Override
+ public void run() {
+ boolean shouldRedraw = false;
+ while (true) {
+ // Check if thread is finished, and if so return immediately.
+ if (mDone) {
+ return;
+ }
+
+ try {
+ ContactInfoRequest request = mUpdateRequests.take();
+ shouldRedraw |= queryContactInfo(request);
+ if (shouldRedraw
+ && (mUpdateRequests.isEmpty()
+ || request.isLocalRequest() && !mUpdateRequests.peek().isLocalRequest())) {
+ shouldRedraw = false;
+ mHandler.sendEmptyMessage(REDRAW);
+ }
+ } catch (InterruptedException e) {
+ // Ignore and attempt to continue processing requests
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java
new file mode 100644
index 0000000..5c2eb1d
--- /dev/null
+++ b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2015 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.app.contactinfo;
+
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** A request for contact details for the given number, used by the ContactInfoCache. */
+public final class ContactInfoRequest implements Comparable<ContactInfoRequest> {
+
+ private static final AtomicLong NEXT_SEQUENCE_NUMBER = new AtomicLong(0);
+
+ private final long sequenceNumber;
+
+ /** The number to look-up. */
+ public final String number;
+ /** The country in which a call to or from this number was placed or received. */
+ public final String countryIso;
+ /** The cached contact information stored in the call log. */
+ public final ContactInfo callLogInfo;
+
+ /** Is the request a remote lookup. Remote requests are treated as lower priority. */
+ @TYPE public final int type;
+
+ /** Specifies the type of the request is. */
+ @IntDef(
+ value = {
+ TYPE_LOCAL,
+ TYPE_LOCAL_AND_REMOTE,
+ TYPE_REMOTE,
+ }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TYPE {}
+
+ public static final int TYPE_LOCAL = 0;
+ /** If cannot find the contact locally, do remote lookup later. */
+ public static final int TYPE_LOCAL_AND_REMOTE = 1;
+
+ public static final int TYPE_REMOTE = 2;
+
+ public ContactInfoRequest(
+ String number, String countryIso, ContactInfo callLogInfo, @TYPE int type) {
+ this.sequenceNumber = NEXT_SEQUENCE_NUMBER.getAndIncrement();
+ this.number = number;
+ this.countryIso = countryIso;
+ this.callLogInfo = callLogInfo;
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof ContactInfoRequest)) {
+ return false;
+ }
+
+ ContactInfoRequest other = (ContactInfoRequest) obj;
+
+ if (!TextUtils.equals(number, other.number)) {
+ return false;
+ }
+ if (!TextUtils.equals(countryIso, other.countryIso)) {
+ return false;
+ }
+ if (!Objects.equals(callLogInfo, other.callLogInfo)) {
+ return false;
+ }
+
+ if (type != other.type) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean isLocalRequest() {
+ return type == TYPE_LOCAL || type == TYPE_LOCAL_AND_REMOTE;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(sequenceNumber, number, countryIso, callLogInfo, type);
+ }
+
+ @Override
+ public int compareTo(ContactInfoRequest other) {
+ // Local query always comes first.
+ if (isLocalRequest() && !other.isLocalRequest()) {
+ return -1;
+ }
+ if (!isLocalRequest() && other.isLocalRequest()) {
+ return 1;
+ }
+ // First come first served.
+ return sequenceNumber < other.sequenceNumber ? -1 : 1;
+ }
+}
diff --git a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
new file mode 100644
index 0000000..a8c7185
--- /dev/null
+++ b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
@@ -0,0 +1,129 @@
+/*
+ * 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.app.contactinfo;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.app.R;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+/**
+ * Class to create the appropriate contact icon from a ContactInfo. This class is for synchronous,
+ * blocking calls to generate bitmaps, while ContactCommons.ContactPhotoManager is to cache, manage
+ * and update a ImageView asynchronously.
+ */
+public class ContactPhotoLoader {
+
+ private final Context mContext;
+ private final ContactInfo mContactInfo;
+
+ public ContactPhotoLoader(Context context, ContactInfo contactInfo) {
+ mContext = Objects.requireNonNull(context);
+ mContactInfo = Objects.requireNonNull(contactInfo);
+ }
+
+ private static Bitmap drawableToBitmap(Drawable drawable, int width, int height) {
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
+
+ /** Create a contact photo icon bitmap appropriate for the ContactInfo. */
+ public Bitmap loadPhotoIcon() {
+ Assert.isWorkerThread();
+ int photoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
+ return drawableToBitmap(getIcon(), photoSize, photoSize);
+ }
+
+ @VisibleForTesting
+ Drawable getIcon() {
+ Drawable drawable = createPhotoIconDrawable();
+ if (drawable == null) {
+ drawable = createLetterTileDrawable();
+ }
+ return drawable;
+ }
+
+ /**
+ * @return a {@link Drawable} of circular photo icon if the photo can be loaded, {@code null}
+ * otherwise.
+ */
+ @Nullable
+ private Drawable createPhotoIconDrawable() {
+ if (mContactInfo.photoUri == null) {
+ return null;
+ }
+ try {
+ InputStream input = mContext.getContentResolver().openInputStream(mContactInfo.photoUri);
+ if (input == null) {
+ LogUtil.w(
+ "ContactPhotoLoader.createPhotoIconDrawable",
+ "createPhotoIconDrawable: InputStream is null");
+ return null;
+ }
+ Bitmap bitmap = BitmapFactory.decodeStream(input);
+ input.close();
+
+ if (bitmap == null) {
+ LogUtil.w(
+ "ContactPhotoLoader.createPhotoIconDrawable",
+ "createPhotoIconDrawable: Bitmap is null");
+ return null;
+ }
+ final RoundedBitmapDrawable drawable =
+ RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap);
+ drawable.setAntiAlias(true);
+ drawable.setCornerRadius(bitmap.getHeight() / 2);
+ return drawable;
+ } catch (IOException e) {
+ LogUtil.e("ContactPhotoLoader.createPhotoIconDrawable", e.toString());
+ return null;
+ }
+ }
+
+ /** @return a {@link LetterTileDrawable} based on the ContactInfo. */
+ private Drawable createLetterTileDrawable() {
+ ContactInfoHelper helper =
+ new ContactInfoHelper(mContext, GeoUtil.getCurrentCountryIso(mContext));
+ LetterTileDrawable drawable = new LetterTileDrawable(mContext.getResources());
+ drawable.setCanonicalDialerLetterTileDetails(
+ mContactInfo.name,
+ mContactInfo.lookupKey,
+ LetterTileDrawable.SHAPE_CIRCLE,
+ helper.isBusiness(mContactInfo.sourceType)
+ ? LetterTileDrawable.TYPE_BUSINESS
+ : LetterTileDrawable.TYPE_DEFAULT);
+ return drawable;
+ }
+}
diff --git a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
new file mode 100644
index 0000000..aed51b5
--- /dev/null
+++ b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
@@ -0,0 +1,67 @@
+/*
+ * 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.app.contactinfo;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.app.AppCompatActivity;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.ExpirableCache;
+
+/**
+ * Fragment without any UI whose purpose is to retain an instance of {@link ExpirableCache} across
+ * configuration change through the use of {@link #setRetainInstance(boolean)}. This is done as
+ * opposed to implementing {@link android.os.Parcelable} as it is a less widespread change.
+ */
+public class ExpirableCacheHeadlessFragment extends Fragment {
+
+ private static final String FRAGMENT_TAG = "ExpirableCacheHeadlessFragment";
+ private static final int CONTACT_INFO_CACHE_SIZE = 100;
+
+ private ExpirableCache<NumberWithCountryIso, ContactInfo> retainedCache;
+
+ @NonNull
+ public static ExpirableCacheHeadlessFragment attach(@NonNull AppCompatActivity parentActivity) {
+ return attach(parentActivity.getSupportFragmentManager());
+ }
+
+ @NonNull
+ private static ExpirableCacheHeadlessFragment attach(FragmentManager fragmentManager) {
+ ExpirableCacheHeadlessFragment fragment =
+ (ExpirableCacheHeadlessFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
+ if (fragment == null) {
+ fragment = new ExpirableCacheHeadlessFragment();
+ // Allowing state loss since in rare cases this is called after activity's state is saved and
+ // it's fine if the cache is lost.
+ fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG).commitNowAllowingStateLoss();
+ }
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ retainedCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
+ setRetainInstance(true);
+ }
+
+ public ExpirableCache<NumberWithCountryIso, ContactInfo> getRetainedCache() {
+ return retainedCache;
+ }
+}
diff --git a/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java
new file mode 100644
index 0000000..a005c44
--- /dev/null
+++ b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 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.app.contactinfo;
+
+import android.text.TextUtils;
+
+/**
+ * Stores a phone number of a call with the country code where it originally occurred. This object
+ * is used as a key in the {@code ContactInfoCache}.
+ *
+ * <p>The country does not necessarily specify the country of the phone number itself, but rather it
+ * is the country in which the user was in when the call was placed or received.
+ */
+public final class NumberWithCountryIso {
+
+ public final String number;
+ public final String countryIso;
+
+ public NumberWithCountryIso(String number, String countryIso) {
+ this.number = number;
+ this.countryIso = countryIso;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) {
+ return false;
+ }
+ if (!(o instanceof NumberWithCountryIso)) {
+ return false;
+ }
+ NumberWithCountryIso other = (NumberWithCountryIso) o;
+ return TextUtils.equals(number, other.number) && TextUtils.equals(countryIso, other.countryIso);
+ }
+
+ @Override
+ public int hashCode() {
+ int numberHashCode = number == null ? 0 : number.hashCode();
+ int countryHashCode = countryIso == null ? 0 : countryIso.hashCode();
+
+ return numberHashCode ^ countryHashCode;
+ }
+}
diff --git a/java/com/android/dialer/app/dialpad/DialpadFragment.java b/java/com/android/dialer/app/dialpad/DialpadFragment.java
new file mode 100644
index 0000000..18bb250
--- /dev/null
+++ b/java/com/android/dialer/app/dialpad/DialpadFragment.java
@@ -0,0 +1,1689 @@
+/*
+ * Copyright (C) 2011 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.app.dialpad;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Trace;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.Contacts.PhonesColumns;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberFormattingTextWatcher;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.dialog.CallSubjectDialog;
+import com.android.contacts.common.util.StopWatch;
+import com.android.contacts.common.widget.FloatingActionButtonController;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.R;
+import com.android.dialer.app.SpecialCharSequenceMgr;
+import com.android.dialer.app.calllog.CallLogAsync;
+import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.dialpadview.DialpadKeyButton;
+import com.android.dialer.dialpadview.DialpadView;
+import com.android.dialer.proguard.UsedByReflection;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.HashSet;
+import java.util.List;
+
+/** Fragment that displays a twelve-key phone dialpad. */
+public class DialpadFragment extends Fragment
+ implements View.OnClickListener,
+ View.OnLongClickListener,
+ View.OnKeyListener,
+ AdapterView.OnItemClickListener,
+ TextWatcher,
+ PopupMenu.OnMenuItemClickListener,
+ DialpadKeyButton.OnPressedListener {
+
+ private static final String TAG = "DialpadFragment";
+ private static final boolean DEBUG = DialtactsActivity.DEBUG;
+ private static final String EMPTY_NUMBER = "";
+ private static final char PAUSE = ',';
+ private static final char WAIT = ';';
+ /** The length of DTMF tones in milliseconds */
+ private static final int TONE_LENGTH_MS = 150;
+
+ private static final int TONE_LENGTH_INFINITE = -1;
+ /** The DTMF tone volume relative to other sounds in the stream */
+ private static final int TONE_RELATIVE_VOLUME = 80;
+ /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */
+ private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF;
+ /** Identifier for the "Add Call" intent extra. */
+ private static final String ADD_CALL_MODE_KEY = "add_call_mode";
+ /**
+ * Identifier for intent extra for sending an empty Flash message for CDMA networks. This message
+ * is used by the network to simulate a press/depress of the "hookswitch" of a landline phone. Aka
+ * "empty flash".
+ *
+ * <p>TODO: Using an intent extra to tell the phone to send this flash is a temporary measure. To
+ * be replaced with an Telephony/TelecomManager call in the future. TODO: Keep in sync with the
+ * string defined in OutgoingCallBroadcaster.java in Phone app until this is replaced with the
+ * Telephony/Telecom API.
+ */
+ private static final String EXTRA_SEND_EMPTY_FLASH = "com.android.phone.extra.SEND_EMPTY_FLASH";
+
+ private static final String PREF_DIGITS_FILLED_BY_INTENT = "pref_digits_filled_by_intent";
+ private final Object mToneGeneratorLock = new Object();
+ /** Set of dialpad keys that are currently being pressed */
+ private final HashSet<View> mPressedDialpadKeys = new HashSet<View>(12);
+ // Last number dialed, retrieved asynchronously from the call DB
+ // in onCreate. This number is displayed when the user hits the
+ // send key and cleared in onPause.
+ private final CallLogAsync mCallLog = new CallLogAsync();
+ private OnDialpadQueryChangedListener mDialpadQueryListener;
+ private DialpadView mDialpadView;
+ private EditText mDigits;
+ private int mDialpadSlideInDuration;
+ /** Remembers if we need to clear digits field when the screen is completely gone. */
+ private boolean mClearDigitsOnStop;
+
+ private View mOverflowMenuButton;
+ private PopupMenu mOverflowPopupMenu;
+ private View mDelete;
+ private ToneGenerator mToneGenerator;
+ private View mSpacer;
+ private FloatingActionButtonController mFloatingActionButtonController;
+ private ListView mDialpadChooser;
+ private DialpadChooserAdapter mDialpadChooserAdapter;
+ /** Regular expression prohibiting manual phone call. Can be empty, which means "no rule". */
+ private String mProhibitedPhoneNumberRegexp;
+
+ private PseudoEmergencyAnimator mPseudoEmergencyAnimator;
+ private String mLastNumberDialed = EMPTY_NUMBER;
+
+ // determines if we want to playback local DTMF tones.
+ private boolean mDTMFToneEnabled;
+ private String mCurrentCountryIso;
+ private CallStateReceiver mCallStateReceiver;
+ private boolean mWasEmptyBeforeTextChange;
+ /**
+ * This field is set to true while processing an incoming DIAL intent, in order to make sure that
+ * SpecialCharSequenceMgr actions can be triggered by user input but *not* by a tel: URI passed by
+ * some other app. It will be set to false when all digits are cleared.
+ */
+ private boolean mDigitsFilledByIntent;
+
+ private boolean mStartedFromNewIntent = false;
+ private boolean mFirstLaunch = false;
+ private boolean mAnimate = false;
+
+ /**
+ * Determines whether an add call operation is requested.
+ *
+ * @param intent The intent.
+ * @return {@literal true} if add call operation was requested. {@literal false} otherwise.
+ */
+ public static boolean isAddCallMode(Intent intent) {
+ if (intent == null) {
+ return false;
+ }
+ final String action = intent.getAction();
+ if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
+ // see if we are "adding a call" from the InCallScreen; false by default.
+ return intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Format the provided string of digits into one that represents a properly formatted phone
+ * number.
+ *
+ * @param dialString String of characters to format
+ * @param normalizedNumber the E164 format number whose country code is used if the given
+ * phoneNumber doesn't have the country code.
+ * @param countryIso The country code representing the format to use if the provided normalized
+ * number is null or invalid.
+ * @return the provided string of digits as a formatted phone number, retaining any post-dial
+ * portion of the string.
+ */
+ @VisibleForTesting
+ static String getFormattedDigits(String dialString, String normalizedNumber, String countryIso) {
+ String number = PhoneNumberUtils.extractNetworkPortion(dialString);
+ // Also retrieve the post dial portion of the provided data, so that the entire dial
+ // string can be reconstituted later.
+ final String postDial = PhoneNumberUtils.extractPostDialPortion(dialString);
+
+ if (TextUtils.isEmpty(number)) {
+ return postDial;
+ }
+
+ number = PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+
+ if (TextUtils.isEmpty(postDial)) {
+ return number;
+ }
+
+ return number.concat(postDial);
+ }
+
+ /**
+ * Returns true of the newDigit parameter can be added at the current selection point, otherwise
+ * returns false. Only prevents input of WAIT and PAUSE digits at an unsupported position. Fails
+ * early if start == -1 or start is larger than end.
+ */
+ @VisibleForTesting
+ /* package */ static boolean canAddDigit(CharSequence digits, int start, int end, char newDigit) {
+ if (newDigit != WAIT && newDigit != PAUSE) {
+ throw new IllegalArgumentException(
+ "Should not be called for anything other than PAUSE & WAIT");
+ }
+
+ // False if no selection, or selection is reversed (end < start)
+ if (start == -1 || end < start) {
+ return false;
+ }
+
+ // unsupported selection-out-of-bounds state
+ if (start > digits.length() || end > digits.length()) {
+ return false;
+ }
+
+ // Special digit cannot be the first digit
+ if (start == 0) {
+ return false;
+ }
+
+ if (newDigit == WAIT) {
+ // preceding char is ';' (WAIT)
+ if (digits.charAt(start - 1) == WAIT) {
+ return false;
+ }
+
+ // next char is ';' (WAIT)
+ if ((digits.length() > end) && (digits.charAt(end) == WAIT)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private TelephonyManager getTelephonyManager() {
+ return (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+ }
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ mWasEmptyBeforeTextChange = TextUtils.isEmpty(s);
+ }
+
+ @Override
+ public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
+ if (mWasEmptyBeforeTextChange != TextUtils.isEmpty(input)) {
+ final Activity activity = getActivity();
+ if (activity != null) {
+ activity.invalidateOptionsMenu();
+ updateMenuOverflowButton(mWasEmptyBeforeTextChange);
+ }
+ }
+
+ // DTMF Tones do not need to be played here any longer -
+ // the DTMF dialer handles that functionality now.
+ }
+
+ @Override
+ public void afterTextChanged(Editable input) {
+ // When DTMF dialpad buttons are being pressed, we delay SpecialCharSequenceMgr sequence,
+ // since some of SpecialCharSequenceMgr's behavior is too abrupt for the "touch-down"
+ // behavior.
+ if (!mDigitsFilledByIntent
+ && SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), mDigits)) {
+ // A special sequence was entered, clear the digits
+ mDigits.getText().clear();
+ }
+
+ if (isDigitsEmpty()) {
+ mDigitsFilledByIntent = false;
+ mDigits.setCursorVisible(false);
+ }
+
+ if (mDialpadQueryListener != null) {
+ mDialpadQueryListener.onDialpadQueryChanged(mDigits.getText().toString());
+ }
+
+ updateDeleteButtonEnabledState();
+ }
+
+ @Override
+ public void onCreate(Bundle state) {
+ Trace.beginSection(TAG + " onCreate");
+ super.onCreate(state);
+
+ mFirstLaunch = state == null;
+
+ mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
+
+ mProhibitedPhoneNumberRegexp =
+ getResources().getString(R.string.config_prohibited_phone_number_regexp);
+
+ if (state != null) {
+ mDigitsFilledByIntent = state.getBoolean(PREF_DIGITS_FILLED_BY_INTENT);
+ }
+
+ mDialpadSlideInDuration = getResources().getInteger(R.integer.dialpad_slide_in_duration);
+
+ if (mCallStateReceiver == null) {
+ IntentFilter callStateIntentFilter =
+ new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
+ mCallStateReceiver = new CallStateReceiver();
+ getActivity().registerReceiver(mCallStateReceiver, callStateIntentFilter);
+ }
+ Trace.endSection();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ Trace.beginSection(TAG + " onCreateView");
+ Trace.beginSection(TAG + " inflate view");
+ final View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false);
+ Trace.endSection();
+ Trace.beginSection(TAG + " buildLayer");
+ fragmentView.buildLayer();
+ Trace.endSection();
+
+ Trace.beginSection(TAG + " setup views");
+
+ mDialpadView = (DialpadView) fragmentView.findViewById(R.id.dialpad_view);
+ mDialpadView.setCanDigitsBeEdited(true);
+ mDigits = mDialpadView.getDigits();
+ mDigits.setKeyListener(UnicodeDialerKeyListener.INSTANCE);
+ mDigits.setOnClickListener(this);
+ mDigits.setOnKeyListener(this);
+ mDigits.setOnLongClickListener(this);
+ mDigits.addTextChangedListener(this);
+ mDigits.setElegantTextHeight(false);
+
+ PhoneNumberFormattingTextWatcher watcher =
+ new PhoneNumberFormattingTextWatcher(GeoUtil.getCurrentCountryIso(getActivity()));
+ mDigits.addTextChangedListener(watcher);
+
+ // Check for the presence of the keypad
+ View oneButton = fragmentView.findViewById(R.id.one);
+ if (oneButton != null) {
+ configureKeypadListeners(fragmentView);
+ }
+
+ mDelete = mDialpadView.getDeleteButton();
+
+ if (mDelete != null) {
+ mDelete.setOnClickListener(this);
+ mDelete.setOnLongClickListener(this);
+ }
+
+ mSpacer = fragmentView.findViewById(R.id.spacer);
+ mSpacer.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (isDigitsEmpty()) {
+ if (getActivity() != null) {
+ return ((HostInterface) getActivity()).onDialpadSpacerTouchWithEmptyQuery();
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+
+ mDigits.setCursorVisible(false);
+
+ // Set up the "dialpad chooser" UI; see showDialpadChooser().
+ mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser);
+ mDialpadChooser.setOnItemClickListener(this);
+
+ final View floatingActionButtonContainer =
+ fragmentView.findViewById(R.id.dialpad_floating_action_button_container);
+ final ImageButton floatingActionButton =
+ (ImageButton) fragmentView.findViewById(R.id.dialpad_floating_action_button);
+ floatingActionButton.setOnClickListener(this);
+ mFloatingActionButtonController =
+ new FloatingActionButtonController(
+ getActivity(), floatingActionButtonContainer, floatingActionButton);
+ Trace.endSection();
+ Trace.endSection();
+ return fragmentView;
+ }
+
+ private boolean isLayoutReady() {
+ return mDigits != null;
+ }
+
+ @VisibleForTesting
+ public EditText getDigitsWidget() {
+ return mDigits;
+ }
+
+ /** @return true when {@link #mDigits} is actually filled by the Intent. */
+ private boolean fillDigitsIfNecessary(Intent intent) {
+ // Only fills digits from an intent if it is a new intent.
+ // Otherwise falls back to the previously used number.
+ if (!mFirstLaunch && !mStartedFromNewIntent) {
+ return false;
+ }
+
+ final String action = intent.getAction();
+ if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
+ Uri uri = intent.getData();
+ if (uri != null) {
+ if (PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) {
+ // Put the requested number into the input area
+ String data = uri.getSchemeSpecificPart();
+ // Remember it is filled via Intent.
+ mDigitsFilledByIntent = true;
+ final String converted =
+ PhoneNumberUtils.convertKeypadLettersToDigits(
+ PhoneNumberUtils.replaceUnicodeDigits(data));
+ setFormattedDigits(converted, null);
+ return true;
+ } else {
+ if (!PermissionsUtil.hasContactsPermissions(getActivity())) {
+ return false;
+ }
+ String type = intent.getType();
+ if (People.CONTENT_ITEM_TYPE.equals(type) || Phones.CONTENT_ITEM_TYPE.equals(type)) {
+ // Query the phone number
+ Cursor c =
+ getActivity()
+ .getContentResolver()
+ .query(
+ intent.getData(),
+ new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY},
+ null,
+ null,
+ null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ // Remember it is filled via Intent.
+ mDigitsFilledByIntent = true;
+ // Put the number into the input area
+ setFormattedDigits(c.getString(0), c.getString(1));
+ return true;
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks the given Intent and changes dialpad's UI state. For example, if the Intent requires the
+ * screen to enter "Add Call" mode, this method will show correct UI for the mode.
+ */
+ private void configureScreenFromIntent(Activity parent) {
+ // If we were not invoked with a DIAL intent,
+ if (!(parent instanceof DialtactsActivity)) {
+ setStartedFromNewIntent(false);
+ return;
+ }
+ // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
+ // digits in the dialer field.
+ Intent intent = parent.getIntent();
+
+ if (!isLayoutReady()) {
+ // This happens typically when parent's Activity#onNewIntent() is called while
+ // Fragment#onCreateView() isn't called yet, and thus we cannot configure Views at
+ // this point. onViewCreate() should call this method after preparing layouts, so
+ // just ignore this call now.
+ LogUtil.i(
+ "DialpadFragment.configureScreenFromIntent",
+ "Screen configuration is requested before onCreateView() is called. Ignored");
+ return;
+ }
+
+ boolean needToShowDialpadChooser = false;
+
+ // Be sure *not* to show the dialpad chooser if this is an
+ // explicit "Add call" action, though.
+ final boolean isAddCallMode = isAddCallMode(intent);
+ if (!isAddCallMode) {
+
+ // Don't show the chooser when called via onNewIntent() and phone number is present.
+ // i.e. User clicks a telephone link from gmail for example.
+ // In this case, we want to show the dialpad with the phone number.
+ final boolean digitsFilled = fillDigitsIfNecessary(intent);
+ if (!(mStartedFromNewIntent && digitsFilled)) {
+
+ final String action = intent.getAction();
+ if (Intent.ACTION_DIAL.equals(action)
+ || Intent.ACTION_VIEW.equals(action)
+ || Intent.ACTION_MAIN.equals(action)) {
+ // If there's already an active call, bring up an intermediate UI to
+ // make the user confirm what they really want to do.
+ if (isPhoneInUse()) {
+ needToShowDialpadChooser = true;
+ }
+ }
+ }
+ }
+ showDialpadChooser(needToShowDialpadChooser);
+ setStartedFromNewIntent(false);
+ }
+
+ public void setStartedFromNewIntent(boolean value) {
+ mStartedFromNewIntent = value;
+ }
+
+ public void clearCallRateInformation() {
+ setCallRateInformation(null, null);
+ }
+
+ public void setCallRateInformation(String countryName, String displayRate) {
+ mDialpadView.setCallRateInformation(countryName, displayRate);
+ }
+
+ /** Sets formatted digits to digits field. */
+ private void setFormattedDigits(String data, String normalizedNumber) {
+ final String formatted = getFormattedDigits(data, normalizedNumber, mCurrentCountryIso);
+ if (!TextUtils.isEmpty(formatted)) {
+ Editable digits = mDigits.getText();
+ digits.replace(0, digits.length(), formatted);
+ // for some reason this isn't getting called in the digits.replace call above..
+ // but in any case, this will make sure the background drawable looks right
+ afterTextChanged(digits);
+ }
+ }
+
+ private void configureKeypadListeners(View fragmentView) {
+ final int[] buttonIds =
+ new int[] {
+ R.id.one,
+ R.id.two,
+ R.id.three,
+ R.id.four,
+ R.id.five,
+ R.id.six,
+ R.id.seven,
+ R.id.eight,
+ R.id.nine,
+ R.id.star,
+ R.id.zero,
+ R.id.pound
+ };
+
+ DialpadKeyButton dialpadKey;
+
+ for (int i = 0; i < buttonIds.length; i++) {
+ dialpadKey = (DialpadKeyButton) fragmentView.findViewById(buttonIds[i]);
+ dialpadKey.setOnPressedListener(this);
+ }
+
+ // Long-pressing one button will initiate Voicemail.
+ final DialpadKeyButton one = (DialpadKeyButton) fragmentView.findViewById(R.id.one);
+ one.setOnLongClickListener(this);
+
+ // Long-pressing zero button will enter '+' instead.
+ final DialpadKeyButton zero = (DialpadKeyButton) fragmentView.findViewById(R.id.zero);
+ zero.setOnLongClickListener(this);
+ }
+
+ @Override
+ public void onStart() {
+ Trace.beginSection(TAG + " onStart");
+ super.onStart();
+ // if the mToneGenerator creation fails, just continue without it. It is
+ // a local audio signal, and is not as important as the dtmf tone itself.
+ final long start = System.currentTimeMillis();
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ try {
+ mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME);
+ } catch (RuntimeException e) {
+ LogUtil.e(
+ "DialpadFragment.onStart",
+ "Exception caught while creating local tone generator: " + e);
+ mToneGenerator = null;
+ }
+ }
+ }
+ final long total = System.currentTimeMillis() - start;
+ if (total > 50) {
+ LogUtil.i("DialpadFragment.onStart", "Time for ToneGenerator creation: " + total);
+ }
+ Trace.endSection();
+ }
+
+ @Override
+ public void onResume() {
+ Trace.beginSection(TAG + " onResume");
+ super.onResume();
+
+ final DialtactsActivity activity = (DialtactsActivity) getActivity();
+ mDialpadQueryListener = activity;
+
+ final StopWatch stopWatch = StopWatch.start("Dialpad.onResume");
+
+ // Query the last dialed number. Do it first because hitting
+ // the DB is 'slow'. This call is asynchronous.
+ queryLastOutgoingCall();
+
+ stopWatch.lap("qloc");
+
+ final ContentResolver contentResolver = activity.getContentResolver();
+
+ // retrieve the DTMF tone play back setting.
+ mDTMFToneEnabled =
+ Settings.System.getInt(contentResolver, Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
+
+ stopWatch.lap("dtwd");
+
+ stopWatch.lap("hptc");
+
+ mPressedDialpadKeys.clear();
+
+ configureScreenFromIntent(getActivity());
+
+ stopWatch.lap("fdin");
+
+ if (!isPhoneInUse()) {
+ // A sanity-check: the "dialpad chooser" UI should not be visible if the phone is idle.
+ showDialpadChooser(false);
+ }
+
+ stopWatch.lap("hnt");
+
+ updateDeleteButtonEnabledState();
+
+ stopWatch.lap("bes");
+
+ stopWatch.stopAndLog(TAG, 50);
+
+ // Populate the overflow menu in onResume instead of onCreate, so that if the SMS activity
+ // is disabled while Dialer is paused, the "Send a text message" option can be correctly
+ // removed when resumed.
+ mOverflowMenuButton = mDialpadView.getOverflowMenuButton();
+ mOverflowPopupMenu = buildOptionsMenu(mOverflowMenuButton);
+ mOverflowMenuButton.setOnTouchListener(mOverflowPopupMenu.getDragToOpenListener());
+ mOverflowMenuButton.setOnClickListener(this);
+ mOverflowMenuButton.setVisibility(isDigitsEmpty() ? View.INVISIBLE : View.VISIBLE);
+
+ if (mFirstLaunch) {
+ // The onHiddenChanged callback does not get called the first time the fragment is
+ // attached, so call it ourselves here.
+ onHiddenChanged(false);
+ }
+
+ mFirstLaunch = false;
+ Trace.endSection();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ // Make sure we don't leave this activity with a tone still playing.
+ stopTone();
+ mPressedDialpadKeys.clear();
+
+ // TODO: I wonder if we should not check if the AsyncTask that
+ // lookup the last dialed number has completed.
+ mLastNumberDialed = EMPTY_NUMBER; // Since we are going to query again, free stale number.
+
+ SpecialCharSequenceMgr.cleanup();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator != null) {
+ mToneGenerator.release();
+ mToneGenerator = null;
+ }
+ }
+
+ if (mClearDigitsOnStop) {
+ mClearDigitsOnStop = false;
+ clearDialpad();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(PREF_DIGITS_FILLED_BY_INTENT, mDigitsFilledByIntent);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mPseudoEmergencyAnimator != null) {
+ mPseudoEmergencyAnimator.destroy();
+ mPseudoEmergencyAnimator = null;
+ }
+ getActivity().unregisterReceiver(mCallStateReceiver);
+ }
+
+ private void keyPressed(int keyCode) {
+ if (getView() == null || getView().getTranslationY() != 0) {
+ return;
+ }
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_1:
+ playTone(ToneGenerator.TONE_DTMF_1, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_2:
+ playTone(ToneGenerator.TONE_DTMF_2, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_3:
+ playTone(ToneGenerator.TONE_DTMF_3, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_4:
+ playTone(ToneGenerator.TONE_DTMF_4, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_5:
+ playTone(ToneGenerator.TONE_DTMF_5, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_6:
+ playTone(ToneGenerator.TONE_DTMF_6, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_7:
+ playTone(ToneGenerator.TONE_DTMF_7, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_8:
+ playTone(ToneGenerator.TONE_DTMF_8, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_9:
+ playTone(ToneGenerator.TONE_DTMF_9, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_0:
+ playTone(ToneGenerator.TONE_DTMF_0, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_POUND:
+ playTone(ToneGenerator.TONE_DTMF_P, TONE_LENGTH_INFINITE);
+ break;
+ case KeyEvent.KEYCODE_STAR:
+ playTone(ToneGenerator.TONE_DTMF_S, TONE_LENGTH_INFINITE);
+ break;
+ default:
+ break;
+ }
+
+ getView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+ KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
+ mDigits.onKeyDown(keyCode, event);
+
+ // If the cursor is at the end of the text we hide it.
+ final int length = mDigits.length();
+ if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) {
+ mDigits.setCursorVisible(false);
+ }
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent event) {
+ if (view.getId() == R.id.digits) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ handleDialButtonPressed();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * When a key is pressed, we start playing DTMF tone, do vibration, and enter the digit
+ * immediately. When a key is released, we stop the tone. Note that the "key press" event will be
+ * delivered by the system with certain amount of delay, it won't be synced with user's actual
+ * "touch-down" behavior.
+ */
+ @Override
+ public void onPressed(View view, boolean pressed) {
+ if (DEBUG) {
+ LogUtil.d("DialpadFragment.onPressed", "view: " + view + ", pressed: " + pressed);
+ }
+ if (pressed) {
+ int resId = view.getId();
+ if (resId == R.id.one) {
+ keyPressed(KeyEvent.KEYCODE_1);
+ } else if (resId == R.id.two) {
+ keyPressed(KeyEvent.KEYCODE_2);
+ } else if (resId == R.id.three) {
+ keyPressed(KeyEvent.KEYCODE_3);
+ } else if (resId == R.id.four) {
+ keyPressed(KeyEvent.KEYCODE_4);
+ } else if (resId == R.id.five) {
+ keyPressed(KeyEvent.KEYCODE_5);
+ } else if (resId == R.id.six) {
+ keyPressed(KeyEvent.KEYCODE_6);
+ } else if (resId == R.id.seven) {
+ keyPressed(KeyEvent.KEYCODE_7);
+ } else if (resId == R.id.eight) {
+ keyPressed(KeyEvent.KEYCODE_8);
+ } else if (resId == R.id.nine) {
+ keyPressed(KeyEvent.KEYCODE_9);
+ } else if (resId == R.id.zero) {
+ keyPressed(KeyEvent.KEYCODE_0);
+ } else if (resId == R.id.pound) {
+ keyPressed(KeyEvent.KEYCODE_POUND);
+ } else if (resId == R.id.star) {
+ keyPressed(KeyEvent.KEYCODE_STAR);
+ } else {
+ LogUtil.e(
+ "DialpadFragment.onPressed", "Unexpected onTouch(ACTION_DOWN) event from: " + view);
+ }
+ mPressedDialpadKeys.add(view);
+ } else {
+ mPressedDialpadKeys.remove(view);
+ if (mPressedDialpadKeys.isEmpty()) {
+ stopTone();
+ }
+ }
+ }
+
+ /**
+ * Called by the containing Activity to tell this Fragment to build an overflow options menu for
+ * display by the container when appropriate.
+ *
+ * @param invoker the View that invoked the options menu, to act as an anchor location.
+ */
+ private PopupMenu buildOptionsMenu(View invoker) {
+ final PopupMenu popupMenu =
+ new PopupMenu(getActivity(), invoker) {
+ @Override
+ public void show() {
+ final Menu menu = getMenu();
+
+ boolean enable = !isDigitsEmpty();
+ for (int i = 0; i < menu.size(); i++) {
+ MenuItem item = menu.getItem(i);
+ item.setEnabled(enable);
+ if (item.getItemId() == R.id.menu_call_with_note) {
+ item.setVisible(CallUtil.isCallWithSubjectSupported(getContext()));
+ }
+ }
+ super.show();
+ }
+ };
+ popupMenu.inflate(R.menu.dialpad_options);
+ popupMenu.setOnMenuItemClickListener(this);
+ return popupMenu;
+ }
+
+ @Override
+ public void onClick(View view) {
+ int resId = view.getId();
+ if (resId == R.id.dialpad_floating_action_button) {
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+ handleDialButtonPressed();
+ } else if (resId == R.id.deleteButton) {
+ keyPressed(KeyEvent.KEYCODE_DEL);
+ } else if (resId == R.id.digits) {
+ if (!isDigitsEmpty()) {
+ mDigits.setCursorVisible(true);
+ }
+ } else if (resId == R.id.dialpad_overflow) {
+ mOverflowPopupMenu.show();
+ } else {
+ LogUtil.w("DialpadFragment.onClick", "Unexpected event from: " + view);
+ return;
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ final Editable digits = mDigits.getText();
+ final int id = view.getId();
+ if (id == R.id.deleteButton) {
+ digits.clear();
+ return true;
+ } else if (id == R.id.one) {
+ if (isDigitsEmpty() || TextUtils.equals(mDigits.getText(), "1")) {
+ // We'll try to initiate voicemail and thus we want to remove irrelevant string.
+ removePreviousDigitIfPossible('1');
+
+ List<PhoneAccountHandle> subscriptionAccountHandles =
+ PhoneAccountUtils.getSubscriptionPhoneAccounts(getActivity());
+ boolean hasUserSelectedDefault =
+ subscriptionAccountHandles.contains(
+ TelecomUtil.getDefaultOutgoingPhoneAccount(
+ getActivity(), PhoneAccount.SCHEME_VOICEMAIL));
+ boolean needsAccountDisambiguation =
+ subscriptionAccountHandles.size() > 1 && !hasUserSelectedDefault;
+
+ if (needsAccountDisambiguation || isVoicemailAvailable()) {
+ // On a multi-SIM phone, if the user has not selected a default
+ // subscription, initiate a call to voicemail so they can select an account
+ // from the "Call with" dialog.
+ callVoicemail();
+ } else if (getActivity() != null) {
+ // Voicemail is unavailable maybe because Airplane mode is turned on.
+ // Check the current status and show the most appropriate error message.
+ final boolean isAirplaneModeOn =
+ Settings.System.getInt(
+ getActivity().getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0)
+ != 0;
+ if (isAirplaneModeOn) {
+ DialogFragment dialogFragment =
+ ErrorDialogFragment.newInstance(R.string.dialog_voicemail_airplane_mode_message);
+ dialogFragment.show(getFragmentManager(), "voicemail_request_during_airplane_mode");
+ } else {
+ DialogFragment dialogFragment =
+ ErrorDialogFragment.newInstance(R.string.dialog_voicemail_not_ready_message);
+ dialogFragment.show(getFragmentManager(), "voicemail_not_ready");
+ }
+ }
+ return true;
+ }
+ return false;
+ } else if (id == R.id.zero) {
+ if (mPressedDialpadKeys.contains(view)) {
+ // If the zero key is currently pressed, then the long press occurred by touch
+ // (and not via other means like certain accessibility input methods).
+ // Remove the '0' that was input when the key was first pressed.
+ removePreviousDigitIfPossible('0');
+ }
+ keyPressed(KeyEvent.KEYCODE_PLUS);
+ stopTone();
+ mPressedDialpadKeys.remove(view);
+ return true;
+ } else if (id == R.id.digits) {
+ mDigits.setCursorVisible(true);
+ return false;
+ }
+ return false;
+ }
+
+ /**
+ * Remove the digit just before the current position of the cursor, iff the following conditions
+ * are true: 1) The cursor is not positioned at index 0. 2) The digit before the current cursor
+ * position matches the current digit.
+ *
+ * @param digit to remove from the digits view.
+ */
+ private void removePreviousDigitIfPossible(char digit) {
+ final int currentPosition = mDigits.getSelectionStart();
+ if (currentPosition > 0 && digit == mDigits.getText().charAt(currentPosition - 1)) {
+ mDigits.setSelection(currentPosition);
+ mDigits.getText().delete(currentPosition - 1, currentPosition);
+ }
+ }
+
+ public void callVoicemail() {
+ DialerUtils.startActivityWithErrorToast(
+ getActivity(),
+ new CallIntentBuilder(CallUtil.getVoicemailUri(), CallInitiationType.Type.DIALPAD).build());
+ hideAndClearDialpad(false);
+ }
+
+ private void hideAndClearDialpad(boolean animate) {
+ ((DialtactsActivity) getActivity()).hideDialpadFragment(animate, true);
+ }
+
+ /**
+ * In most cases, when the dial button is pressed, there is a number in digits area. Pack it in
+ * the intent, start the outgoing call broadcast as a separate task and finish this activity.
+ *
+ * <p>When there is no digit and the phone is CDMA and off hook, we're sending a blank flash for
+ * CDMA. CDMA networks use Flash messages when special processing needs to be done, mainly for
+ * 3-way or call waiting scenarios. Presumably, here we're in a special 3-way scenario where the
+ * network needs a blank flash before being able to add the new participant. (This is not the case
+ * with all 3-way calls, just certain CDMA infrastructures.)
+ *
+ * <p>Otherwise, there is no digit, display the last dialed number. Don't finish since the user
+ * may want to edit it. The user needs to press the dial button again, to dial it (general case
+ * described above).
+ */
+ private void handleDialButtonPressed() {
+ if (isDigitsEmpty()) { // No number entered.
+ handleDialButtonClickWithEmptyDigits();
+ } else {
+ final String number = mDigits.getText().toString();
+
+ // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated
+ // test equipment.
+ // TODO: clean it up.
+ if (number != null
+ && !TextUtils.isEmpty(mProhibitedPhoneNumberRegexp)
+ && number.matches(mProhibitedPhoneNumberRegexp)) {
+ LogUtil.i(
+ "DialpadFragment.handleDialButtonPressed",
+ "The phone number is prohibited explicitly by a rule.");
+ if (getActivity() != null) {
+ DialogFragment dialogFragment =
+ ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message);
+ dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog");
+ }
+
+ // Clear the digits just in case.
+ clearDialpad();
+ } else {
+ final Intent intent =
+ new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD).build();
+ DialerUtils.startActivityWithErrorToast(getActivity(), intent);
+ hideAndClearDialpad(false);
+ }
+ }
+ }
+
+ public void clearDialpad() {
+ if (mDigits != null) {
+ mDigits.getText().clear();
+ }
+ }
+
+ private void handleDialButtonClickWithEmptyDigits() {
+ if (phoneIsCdma() && isPhoneInUse()) {
+ // TODO: Move this logic into services/Telephony
+ //
+ // This is really CDMA specific. On GSM is it possible
+ // to be off hook and wanted to add a 3rd party using
+ // the redial feature.
+ startActivity(newFlashIntent());
+ } else {
+ if (!TextUtils.isEmpty(mLastNumberDialed)) {
+ // Recall the last number dialed.
+ mDigits.setText(mLastNumberDialed);
+
+ // ...and move the cursor to the end of the digits string,
+ // so you'll be able to delete digits using the Delete
+ // button (just as if you had typed the number manually.)
+ //
+ // Note we use mDigits.getText().length() here, not
+ // mLastNumberDialed.length(), since the EditText widget now
+ // contains a *formatted* version of mLastNumberDialed (due to
+ // mTextWatcher) and its length may have changed.
+ mDigits.setSelection(mDigits.getText().length());
+ } else {
+ // There's no "last number dialed" or the
+ // background query is still running. There's
+ // nothing useful for the Dial button to do in
+ // this case. Note: with a soft dial button, this
+ // can never happens since the dial button is
+ // disabled under these conditons.
+ playTone(ToneGenerator.TONE_PROP_NACK);
+ }
+ }
+ }
+
+ /** Plays the specified tone for TONE_LENGTH_MS milliseconds. */
+ private void playTone(int tone) {
+ playTone(tone, TONE_LENGTH_MS);
+ }
+
+ /**
+ * Play the specified tone for the specified milliseconds
+ *
+ * <p>The tone is played locally, using the audio stream for phone calls. Tones are played only if
+ * the "Audible touch tones" user preference is checked, and are NOT played if the device is in
+ * silent mode.
+ *
+ * <p>The tone length can be -1, meaning "keep playing the tone." If the caller does so, it should
+ * call stopTone() afterward.
+ *
+ * @param tone a tone code from {@link ToneGenerator}
+ * @param durationMs tone length.
+ */
+ private void playTone(int tone, int durationMs) {
+ // if local tone playback is disabled, just return.
+ if (!mDTMFToneEnabled) {
+ return;
+ }
+
+ // Also do nothing if the phone is in silent mode.
+ // We need to re-check the ringer mode for *every* playTone()
+ // call, rather than keeping a local flag that's updated in
+ // onResume(), since it's possible to toggle silent mode without
+ // leaving the current activity (via the ENDCALL-longpress menu.)
+ AudioManager audioManager =
+ (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE);
+ int ringerMode = audioManager.getRingerMode();
+ if ((ringerMode == AudioManager.RINGER_MODE_SILENT)
+ || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
+ return;
+ }
+
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ LogUtil.w("DialpadFragment.playTone", "mToneGenerator == null, tone: " + tone);
+ return;
+ }
+
+ // Start the new tone (will stop any playing tone)
+ mToneGenerator.startTone(tone, durationMs);
+ }
+ }
+
+ /** Stop the tone if it is played. */
+ private void stopTone() {
+ // if local tone playback is disabled, just return.
+ if (!mDTMFToneEnabled) {
+ return;
+ }
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ LogUtil.w("DialpadFragment.stopTone", "mToneGenerator == null");
+ return;
+ }
+ mToneGenerator.stopTone();
+ }
+ }
+
+ /**
+ * Brings up the "dialpad chooser" UI in place of the usual Dialer elements (the textfield/button
+ * and the dialpad underneath).
+ *
+ * <p>We show this UI if the user brings up the Dialer while a call is already in progress, since
+ * there's a good chance we got here accidentally (and the user really wanted the in-call dialpad
+ * instead). So in this situation we display an intermediate UI that lets the user explicitly
+ * choose between the in-call dialpad ("Use touch tone keypad") and the regular Dialer ("Add
+ * call"). (Or, the option "Return to call in progress" just goes back to the in-call UI with no
+ * dialpad at all.)
+ *
+ * @param enabled If true, show the "dialpad chooser" instead of the regular Dialer UI
+ */
+ private void showDialpadChooser(boolean enabled) {
+ if (getActivity() == null) {
+ return;
+ }
+ // Check if onCreateView() is already called by checking one of View objects.
+ if (!isLayoutReady()) {
+ return;
+ }
+
+ if (enabled) {
+ LogUtil.i("DialpadFragment.showDialpadChooser", "Showing dialpad chooser!");
+ if (mDialpadView != null) {
+ mDialpadView.setVisibility(View.GONE);
+ }
+
+ mFloatingActionButtonController.setVisible(false);
+ mDialpadChooser.setVisibility(View.VISIBLE);
+
+ // Instantiate the DialpadChooserAdapter and hook it up to the
+ // ListView. We do this only once.
+ if (mDialpadChooserAdapter == null) {
+ mDialpadChooserAdapter = new DialpadChooserAdapter(getActivity());
+ }
+ mDialpadChooser.setAdapter(mDialpadChooserAdapter);
+ } else {
+ LogUtil.i("DialpadFragment.showDialpadChooser", "Displaying normal Dialer UI.");
+ if (mDialpadView != null) {
+ mDialpadView.setVisibility(View.VISIBLE);
+ } else {
+ mDigits.setVisibility(View.VISIBLE);
+ }
+
+ // mFloatingActionButtonController must also be 'scaled in', in order to be visible after
+ // 'scaleOut()' hidden method.
+ if (!mFloatingActionButtonController.isVisible()) {
+ // Just call 'scaleIn()' method if the mFloatingActionButtonController was not already
+ // previously visible.
+ mFloatingActionButtonController.scaleIn(0);
+ mFloatingActionButtonController.setVisible(true);
+ }
+ mDialpadChooser.setVisibility(View.GONE);
+ }
+ }
+
+ /** @return true if we're currently showing the "dialpad chooser" UI. */
+ private boolean isDialpadChooserVisible() {
+ return mDialpadChooser.getVisibility() == View.VISIBLE;
+ }
+
+ /** Handle clicks from the dialpad chooser. */
+ @Override
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ DialpadChooserAdapter.ChoiceItem item =
+ (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position);
+ int itemId = item.id;
+ if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD) {
+ // Fire off an intent to go back to the in-call UI
+ // with the dialpad visible.
+ returnToInCallScreen(true);
+ } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL) {
+ // Fire off an intent to go back to the in-call UI
+ // (with the dialpad hidden).
+ returnToInCallScreen(false);
+ } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL) {
+ // Ok, guess the user really did want to be here (in the
+ // regular Dialer) after all. Bring back the normal Dialer UI.
+ showDialpadChooser(false);
+ } else {
+ LogUtil.w("DialpadFragment.onItemClick", "Unexpected itemId: " + itemId);
+ }
+ }
+
+ /**
+ * Returns to the in-call UI (where there's presumably a call in progress) in response to the user
+ * selecting "use touch tone keypad" or "return to call" from the dialpad chooser.
+ */
+ private void returnToInCallScreen(boolean showDialpad) {
+ TelecomUtil.showInCallScreen(getActivity(), showDialpad);
+
+ // Finally, finish() ourselves so that we don't stay on the
+ // activity stack.
+ // Note that we do this whether or not the showCallScreenWithDialpad()
+ // call above had any effect or not! (That call is a no-op if the
+ // phone is idle, which can happen if the current call ends while
+ // the dialpad chooser is up. In this case we can't show the
+ // InCallScreen, and there's no point staying here in the Dialer,
+ // so we just take the user back where he came from...)
+ getActivity().finish();
+ }
+
+ /**
+ * @return true if the phone is "in use", meaning that at least one line is active (ie. off hook
+ * or ringing or dialing, or on hold).
+ */
+ private boolean isPhoneInUse() {
+ final Context context = getActivity();
+ if (context != null) {
+ return TelecomUtil.isInCall(context);
+ }
+ return false;
+ }
+
+ /** @return true if the phone is a CDMA phone type */
+ private boolean phoneIsCdma() {
+ return getTelephonyManager().getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ int resId = item.getItemId();
+ if (resId == R.id.menu_2s_pause) {
+ updateDialString(PAUSE);
+ return true;
+ } else if (resId == R.id.menu_add_wait) {
+ updateDialString(WAIT);
+ return true;
+ } else if (resId == R.id.menu_call_with_note) {
+ CallSubjectDialog.start(getActivity(), mDigits.getText().toString());
+ hideAndClearDialpad(false);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Updates the dial string (mDigits) after inserting a Pause character (,) or Wait character (;).
+ */
+ private void updateDialString(char newDigit) {
+ if (newDigit != WAIT && newDigit != PAUSE) {
+ throw new IllegalArgumentException("Not expected for anything other than PAUSE & WAIT");
+ }
+
+ int selectionStart;
+ int selectionEnd;
+
+ // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText());
+ int anchor = mDigits.getSelectionStart();
+ int point = mDigits.getSelectionEnd();
+
+ selectionStart = Math.min(anchor, point);
+ selectionEnd = Math.max(anchor, point);
+
+ if (selectionStart == -1) {
+ selectionStart = selectionEnd = mDigits.length();
+ }
+
+ Editable digits = mDigits.getText();
+
+ if (canAddDigit(digits, selectionStart, selectionEnd, newDigit)) {
+ digits.replace(selectionStart, selectionEnd, Character.toString(newDigit));
+
+ if (selectionStart != selectionEnd) {
+ // Unselect: back to a regular cursor, just pass the character inserted.
+ mDigits.setSelection(selectionStart + 1);
+ }
+ }
+ }
+
+ /** Update the enabledness of the "Dial" and "Backspace" buttons if applicable. */
+ private void updateDeleteButtonEnabledState() {
+ if (getActivity() == null) {
+ return;
+ }
+ final boolean digitsNotEmpty = !isDigitsEmpty();
+ mDelete.setEnabled(digitsNotEmpty);
+ }
+
+ /**
+ * Handle transitions for the menu button depending on the state of the digits edit text.
+ * Transition out when going from digits to no digits and transition in when the first digit is
+ * pressed.
+ *
+ * @param transitionIn True if transitioning in, False if transitioning out
+ */
+ private void updateMenuOverflowButton(boolean transitionIn) {
+ mOverflowMenuButton = mDialpadView.getOverflowMenuButton();
+ if (transitionIn) {
+ AnimUtils.fadeIn(mOverflowMenuButton, AnimUtils.DEFAULT_DURATION);
+ } else {
+ AnimUtils.fadeOut(mOverflowMenuButton, AnimUtils.DEFAULT_DURATION);
+ }
+ }
+
+ /**
+ * Check if voicemail is enabled/accessible.
+ *
+ * @return true if voicemail is enabled and accessible. Note that this can be false "temporarily"
+ * after the app boot.
+ */
+ private boolean isVoicemailAvailable() {
+ try {
+ PhoneAccountHandle defaultUserSelectedAccount =
+ TelecomUtil.getDefaultOutgoingPhoneAccount(getActivity(), PhoneAccount.SCHEME_VOICEMAIL);
+ if (defaultUserSelectedAccount == null) {
+ // In a single-SIM phone, there is no default outgoing phone account selected by
+ // the user, so just call TelephonyManager#getVoicemailNumber directly.
+ return !TextUtils.isEmpty(getTelephonyManager().getVoiceMailNumber());
+ } else {
+ return !TextUtils.isEmpty(
+ TelecomUtil.getVoicemailNumber(getActivity(), defaultUserSelectedAccount));
+ }
+ } catch (SecurityException se) {
+ // Possibly no READ_PHONE_STATE privilege.
+ LogUtil.w(
+ "DialpadFragment.isVoicemailAvailable",
+ "SecurityException is thrown. Maybe privilege isn't sufficient.");
+ }
+ return false;
+ }
+
+ /** @return true if the widget with the phone number digits is empty. */
+ private boolean isDigitsEmpty() {
+ return mDigits.length() == 0;
+ }
+
+ /**
+ * Starts the asyn query to get the last dialed/outgoing number. When the background query
+ * finishes, mLastNumberDialed is set to the last dialed number or an empty string if none exists
+ * yet.
+ */
+ private void queryLastOutgoingCall() {
+ mLastNumberDialed = EMPTY_NUMBER;
+ if (ContextCompat.checkSelfPermission(getActivity(), permission.READ_CALL_LOG)
+ != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ CallLogAsync.GetLastOutgoingCallArgs lastCallArgs =
+ new CallLogAsync.GetLastOutgoingCallArgs(
+ getActivity(),
+ new CallLogAsync.OnLastOutgoingCallComplete() {
+ @Override
+ public void lastOutgoingCall(String number) {
+ // TODO: Filter out emergency numbers if
+ // the carrier does not want redial for
+ // these.
+ // If the fragment has already been detached since the last time
+ // we called queryLastOutgoingCall in onResume there is no point
+ // doing anything here.
+ if (getActivity() == null) {
+ return;
+ }
+ mLastNumberDialed = number;
+ updateDeleteButtonEnabledState();
+ }
+ });
+ mCallLog.getLastOutgoingCall(lastCallArgs);
+ }
+
+ private Intent newFlashIntent() {
+ Intent intent = new CallIntentBuilder(EMPTY_NUMBER, CallInitiationType.Type.DIALPAD).build();
+ intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true);
+ return intent;
+ }
+
+ @Override
+ public void onHiddenChanged(boolean hidden) {
+ super.onHiddenChanged(hidden);
+ final DialtactsActivity activity = (DialtactsActivity) getActivity();
+ final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
+ if (activity == null) {
+ return;
+ }
+ if (!hidden && !isDialpadChooserVisible()) {
+ if (mAnimate) {
+ dialpadView.animateShow();
+ }
+ mFloatingActionButtonController.setVisible(false);
+ mFloatingActionButtonController.scaleIn(mAnimate ? mDialpadSlideInDuration : 0);
+ activity.onDialpadShown();
+ mDigits.requestFocus();
+ }
+ if (hidden) {
+ if (mAnimate) {
+ mFloatingActionButtonController.scaleOut();
+ } else {
+ mFloatingActionButtonController.setVisible(false);
+ }
+ }
+ }
+
+ public boolean getAnimate() {
+ return mAnimate;
+ }
+
+ public void setAnimate(boolean value) {
+ mAnimate = value;
+ }
+
+ public void setYFraction(float yFraction) {
+ ((DialpadSlidingRelativeLayout) getView()).setYFraction(yFraction);
+ }
+
+ public int getDialpadHeight() {
+ if (mDialpadView == null) {
+ return 0;
+ }
+ return mDialpadView.getHeight();
+ }
+
+ public void process_quote_emergency_unquote(String query) {
+ if (PseudoEmergencyAnimator.PSEUDO_EMERGENCY_NUMBER.equals(query)) {
+ if (mPseudoEmergencyAnimator == null) {
+ mPseudoEmergencyAnimator =
+ new PseudoEmergencyAnimator(
+ new PseudoEmergencyAnimator.ViewProvider() {
+ @Override
+ public View getView() {
+ return DialpadFragment.this.getView();
+ }
+ });
+ }
+ mPseudoEmergencyAnimator.start();
+ } else {
+ if (mPseudoEmergencyAnimator != null) {
+ mPseudoEmergencyAnimator.end();
+ }
+ }
+ }
+
+ public interface OnDialpadQueryChangedListener {
+
+ void onDialpadQueryChanged(String query);
+ }
+
+ public interface HostInterface {
+
+ /**
+ * Notifies the parent activity that the space above the dialpad has been tapped with no query
+ * in the dialpad present. In most situations this will cause the dialpad to be dismissed,
+ * unless there happens to be content showing.
+ */
+ boolean onDialpadSpacerTouchWithEmptyQuery();
+ }
+
+ /**
+ * LinearLayout with getter and setter methods for the translationY property using floats, for
+ * animation purposes.
+ */
+ public static class DialpadSlidingRelativeLayout extends RelativeLayout {
+
+ public DialpadSlidingRelativeLayout(Context context) {
+ super(context);
+ }
+
+ public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @UsedByReflection(value = "dialpad_fragment.xml")
+ public float getYFraction() {
+ final int height = getHeight();
+ if (height == 0) {
+ return 0;
+ }
+ return getTranslationY() / height;
+ }
+
+ @UsedByReflection(value = "dialpad_fragment.xml")
+ public void setYFraction(float yFraction) {
+ setTranslationY(yFraction * getHeight());
+ }
+ }
+
+ public static class ErrorDialogFragment extends DialogFragment {
+
+ private static final String ARG_TITLE_RES_ID = "argTitleResId";
+ private static final String ARG_MESSAGE_RES_ID = "argMessageResId";
+ private int mTitleResId;
+ private int mMessageResId;
+
+ public static ErrorDialogFragment newInstance(int messageResId) {
+ return newInstance(0, messageResId);
+ }
+
+ public static ErrorDialogFragment newInstance(int titleResId, int messageResId) {
+ final ErrorDialogFragment fragment = new ErrorDialogFragment();
+ final Bundle args = new Bundle();
+ args.putInt(ARG_TITLE_RES_ID, titleResId);
+ args.putInt(ARG_MESSAGE_RES_ID, messageResId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID);
+ mMessageResId = getArguments().getInt(ARG_MESSAGE_RES_ID);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ if (mTitleResId != 0) {
+ builder.setTitle(mTitleResId);
+ }
+ if (mMessageResId != 0) {
+ builder.setMessage(mMessageResId);
+ }
+ builder.setPositiveButton(
+ android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ });
+ return builder.create();
+ }
+ }
+
+ /**
+ * Simple list adapter, binding to an icon + text label for each item in the "dialpad chooser"
+ * list.
+ */
+ private static class DialpadChooserAdapter extends BaseAdapter {
+
+ // IDs for the possible "choices":
+ static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101;
+ static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102;
+ static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103;
+ private static final int NUM_ITEMS = 3;
+ private LayoutInflater mInflater;
+ private ChoiceItem[] mChoiceItems = new ChoiceItem[NUM_ITEMS];
+
+ public DialpadChooserAdapter(Context context) {
+ // Cache the LayoutInflate to avoid asking for a new one each time.
+ mInflater = LayoutInflater.from(context);
+
+ // Initialize the possible choices.
+ // TODO: could this be specified entirely in XML?
+
+ // - "Use touch tone keypad"
+ mChoiceItems[0] =
+ new ChoiceItem(
+ context.getString(R.string.dialer_useDtmfDialpad),
+ BitmapFactory.decodeResource(
+ context.getResources(), R.drawable.ic_dialer_fork_tt_keypad),
+ DIALPAD_CHOICE_USE_DTMF_DIALPAD);
+
+ // - "Return to call in progress"
+ mChoiceItems[1] =
+ new ChoiceItem(
+ context.getString(R.string.dialer_returnToInCallScreen),
+ BitmapFactory.decodeResource(
+ context.getResources(), R.drawable.ic_dialer_fork_current_call),
+ DIALPAD_CHOICE_RETURN_TO_CALL);
+
+ // - "Add call"
+ mChoiceItems[2] =
+ new ChoiceItem(
+ context.getString(R.string.dialer_addAnotherCall),
+ BitmapFactory.decodeResource(
+ context.getResources(), R.drawable.ic_dialer_fork_add_call),
+ DIALPAD_CHOICE_ADD_NEW_CALL);
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_ITEMS;
+ }
+
+ /** Return the ChoiceItem for a given position. */
+ @Override
+ public Object getItem(int position) {
+ return mChoiceItems[position];
+ }
+
+ /** Return a unique ID for each possible choice. */
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ /** Make a view for each row. */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // When convertView is non-null, we can reuse it (there's no need
+ // to reinflate it.)
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null);
+ }
+
+ TextView text = (TextView) convertView.findViewById(R.id.text);
+ text.setText(mChoiceItems[position].text);
+
+ ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
+ icon.setImageBitmap(mChoiceItems[position].icon);
+
+ return convertView;
+ }
+
+ // Simple struct for a single "choice" item.
+ static class ChoiceItem {
+
+ String text;
+ Bitmap icon;
+ int id;
+
+ public ChoiceItem(String s, Bitmap b, int i) {
+ text = s;
+ icon = b;
+ id = i;
+ }
+ }
+ }
+
+ private class CallStateReceiver extends BroadcastReceiver {
+
+ /**
+ * Receive call state changes so that we can take down the "dialpad chooser" if the phone
+ * becomes idle while the chooser UI is visible.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
+ if ((TextUtils.equals(state, TelephonyManager.EXTRA_STATE_IDLE)
+ || TextUtils.equals(state, TelephonyManager.EXTRA_STATE_OFFHOOK))
+ && isDialpadChooserVisible()) {
+ // Note there's a race condition in the UI here: the
+ // dialpad chooser could conceivably disappear (on its
+ // own) at the exact moment the user was trying to select
+ // one of the choices, which would be confusing. (But at
+ // least that's better than leaving the dialpad chooser
+ // onscreen, but useless...)
+ showDialpadChooser(false);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java b/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java
new file mode 100644
index 0000000..2ffacb6
--- /dev/null
+++ b/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 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.app.dialpad;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.LightingColorFilter;
+import android.os.Handler;
+import android.os.Vibrator;
+import android.view.View;
+import com.android.dialer.app.R;
+
+/** Animates the dial button on "emergency" phone numbers. */
+public class PseudoEmergencyAnimator {
+
+ public static final String PSEUDO_EMERGENCY_NUMBER = "01189998819991197253";
+ private static final int VIBRATE_LENGTH_MILLIS = 200;
+ private static final int ITERATION_LENGTH_MILLIS = 1000;
+ private static final int ANIMATION_ITERATION_COUNT = 6;
+ private ViewProvider mViewProvider;
+ private ValueAnimator mPseudoEmergencyColorAnimator;
+
+ PseudoEmergencyAnimator(ViewProvider viewProvider) {
+ mViewProvider = viewProvider;
+ }
+
+ public void destroy() {
+ end();
+ mViewProvider = null;
+ }
+
+ public void start() {
+ if (mPseudoEmergencyColorAnimator == null) {
+ Integer colorFrom = Color.BLUE;
+ Integer colorTo = Color.RED;
+ mPseudoEmergencyColorAnimator =
+ ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+
+ mPseudoEmergencyColorAnimator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ try {
+ int color = (int) animator.getAnimatedValue();
+ ColorFilter colorFilter = new LightingColorFilter(Color.BLACK, color);
+
+ View floatingActionButtonContainer =
+ getView().findViewById(R.id.dialpad_floating_action_button_container);
+ if (floatingActionButtonContainer != null) {
+ floatingActionButtonContainer.getBackground().setColorFilter(colorFilter);
+ }
+ } catch (Exception e) {
+ animator.cancel();
+ }
+ }
+ });
+
+ mPseudoEmergencyColorAnimator.addListener(
+ new AnimatorListener() {
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ try {
+ vibrate(VIBRATE_LENGTH_MILLIS);
+ } catch (Exception e) {
+ animation.cancel();
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ try {
+ View floatingActionButtonContainer =
+ getView().findViewById(R.id.dialpad_floating_action_button_container);
+ if (floatingActionButtonContainer != null) {
+ floatingActionButtonContainer.getBackground().clearColorFilter();
+ }
+
+ new Handler()
+ .postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ vibrate(VIBRATE_LENGTH_MILLIS);
+ } catch (Exception e) {
+ // ignored
+ }
+ }
+ },
+ ITERATION_LENGTH_MILLIS);
+ } catch (Exception e) {
+ animation.cancel();
+ }
+ }
+ });
+
+ mPseudoEmergencyColorAnimator.setDuration(VIBRATE_LENGTH_MILLIS);
+ mPseudoEmergencyColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
+ mPseudoEmergencyColorAnimator.setRepeatCount(ANIMATION_ITERATION_COUNT);
+ }
+ if (!mPseudoEmergencyColorAnimator.isStarted()) {
+ mPseudoEmergencyColorAnimator.start();
+ }
+ }
+
+ public void end() {
+ if (mPseudoEmergencyColorAnimator != null && mPseudoEmergencyColorAnimator.isStarted()) {
+ mPseudoEmergencyColorAnimator.end();
+ }
+ }
+
+ private View getView() {
+ return mViewProvider == null ? null : mViewProvider.getView();
+ }
+
+ private Context getContext() {
+ View view = getView();
+ return view != null ? view.getContext() : null;
+ }
+
+ private void vibrate(long milliseconds) {
+ Context context = getContext();
+ if (context != null) {
+ Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ if (vibrator != null) {
+ vibrator.vibrate(milliseconds);
+ }
+ }
+ }
+
+ public interface ViewProvider {
+
+ View getView();
+ }
+}
diff --git a/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java b/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java
new file mode 100644
index 0000000..f3a93f9
--- /dev/null
+++ b/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2013 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.app.dialpad;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.util.Log;
+import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery;
+import com.android.dialer.database.Database;
+import com.android.dialer.database.DialerDatabaseHelper;
+import com.android.dialer.database.DialerDatabaseHelper.ContactNumber;
+import com.android.dialer.smartdial.SmartDialNameMatcher;
+import com.android.dialer.smartdial.SmartDialPrefix;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+
+/** Implements a Loader<Cursor> class to asynchronously load SmartDial search results. */
+public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> {
+
+ private static final String TAG = "SmartDialCursorLoader";
+ private static final boolean DEBUG = false;
+
+ private final Context mContext;
+
+ private Cursor mCursor;
+
+ private String mQuery;
+ private SmartDialNameMatcher mNameMatcher;
+
+ private ForceLoadContentObserver mObserver;
+
+ private boolean mShowEmptyListForNullQuery = true;
+
+ public SmartDialCursorLoader(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ /**
+ * Configures the query string to be used to find SmartDial matches.
+ *
+ * @param query The query string user typed.
+ */
+ public void configureQuery(String query) {
+ if (DEBUG) {
+ Log.v(TAG, "Configure new query to be " + query);
+ }
+ mQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialPrefix.getMap());
+
+ /** Constructs a name matcher object for matching names. */
+ mNameMatcher = new SmartDialNameMatcher(mQuery, SmartDialPrefix.getMap());
+ mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery);
+ }
+
+ /**
+ * Queries the SmartDial database and loads results in background.
+ *
+ * @return Cursor of contacts that matches the SmartDial query.
+ */
+ @Override
+ public Cursor loadInBackground() {
+ if (DEBUG) {
+ Log.v(TAG, "Load in background " + mQuery);
+ }
+
+ if (!PermissionsUtil.hasContactsPermissions(mContext)) {
+ return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY);
+ }
+
+ /** Loads results from the database helper. */
+ final DialerDatabaseHelper dialerDatabaseHelper =
+ Database.get(mContext).getDatabaseHelper(mContext);
+ final ArrayList<ContactNumber> allMatches =
+ dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher);
+
+ if (DEBUG) {
+ Log.v(TAG, "Loaded matches " + String.valueOf(allMatches.size()));
+ }
+
+ /** Constructs a cursor for the returned array of results. */
+ final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY);
+ Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length];
+ for (ContactNumber contact : allMatches) {
+ row[PhoneQuery.PHONE_ID] = contact.dataId;
+ row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber;
+ row[PhoneQuery.CONTACT_ID] = contact.id;
+ row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey;
+ row[PhoneQuery.PHOTO_ID] = contact.photoId;
+ row[PhoneQuery.DISPLAY_NAME] = contact.displayName;
+ row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence;
+ cursor.addRow(row);
+ }
+ return cursor;
+ }
+
+ @Override
+ public void deliverResult(Cursor cursor) {
+ if (isReset()) {
+ /** The Loader has been reset; ignore the result and invalidate the data. */
+ releaseResources(cursor);
+ return;
+ }
+
+ /** Hold a reference to the old data so it doesn't get garbage collected. */
+ Cursor oldCursor = mCursor;
+ mCursor = cursor;
+
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ mContext
+ .getContentResolver()
+ .registerContentObserver(DialerDatabaseHelper.SMART_DIAL_UPDATED_URI, true, mObserver);
+ }
+
+ if (isStarted()) {
+ /** If the Loader is in a started state, deliver the results to the client. */
+ super.deliverResult(cursor);
+ }
+
+ /** Invalidate the old data as we don't need it any more. */
+ if (oldCursor != null && oldCursor != cursor) {
+ releaseResources(oldCursor);
+ }
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mCursor != null) {
+ /** Deliver any previously loaded data immediately. */
+ deliverResult(mCursor);
+ }
+ if (mCursor == null) {
+ /** Force loads every time as our results change with queries. */
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ /** The Loader is in a stopped state, so we should attempt to cancel the current load. */
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ /** Ensure the loader has been stopped. */
+ onStopLoading();
+
+ if (mObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+
+ /** Release all previously saved query results. */
+ if (mCursor != null) {
+ releaseResources(mCursor);
+ mCursor = null;
+ }
+ }
+
+ @Override
+ public void onCanceled(Cursor cursor) {
+ super.onCanceled(cursor);
+
+ if (mObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+
+ /** The load has been canceled, so we should release the resources associated with 'data'. */
+ releaseResources(cursor);
+ }
+
+ private void releaseResources(Cursor cursor) {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ public void setShowEmptyListForNullQuery(boolean show) {
+ mShowEmptyListForNullQuery = show;
+ if (mNameMatcher != null) {
+ mNameMatcher.setShouldMatchEmptyQuery(!show);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java b/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java
new file mode 100644
index 0000000..051daf4
--- /dev/null
+++ b/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.app.dialpad;
+
+import android.telephony.PhoneNumberUtils;
+import android.text.Spanned;
+import android.text.method.DialerKeyListener;
+
+/**
+ * {@link DialerKeyListener} with Unicode support. Converts any Unicode(e.g. Arabic) characters that
+ * represent digits into digits before filtering the results so that we can support pasted digits
+ * from Unicode languages.
+ */
+public class UnicodeDialerKeyListener extends DialerKeyListener {
+
+ public static final UnicodeDialerKeyListener INSTANCE = new UnicodeDialerKeyListener();
+
+ @Override
+ public CharSequence filter(
+ CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
+ final String converted =
+ PhoneNumberUtils.convertKeypadLettersToDigits(
+ PhoneNumberUtils.replaceUnicodeDigits(source.toString()));
+ // PhoneNumberUtils.replaceUnicodeDigits performs a character for character replacement,
+ // so we can assume that start and end positions should remain unchanged.
+ CharSequence result = super.filter(converted, start, end, dest, dstart, dend);
+ if (result == null) {
+ if (source.equals(converted)) {
+ // There was no conversion or filtering performed. Just return null according to
+ // the behavior of DialerKeyListener.
+ return null;
+ } else {
+ // filter returns null if the charsequence is to be returned unchanged/unfiltered.
+ // But in this case we do want to return a modified character string (even if
+ // none of the characters in the modified string are filtered). So if
+ // result == null we return the unfiltered but converted numeric string instead.
+ return converted.subSequence(start, end);
+ }
+ }
+ return result;
+ }
+}
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java
new file mode 100644
index 0000000..b938133
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 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.app.filterednumber;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.telephony.PhoneNumberUtils;
+import android.view.View;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.BlockNumberDialogFragment;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+
+public class BlockedNumbersAdapter extends NumbersAdapter {
+
+ private BlockedNumbersAdapter(
+ Context context,
+ FragmentManager fragmentManager,
+ ContactInfoHelper contactInfoHelper,
+ ContactPhotoManager contactPhotoManager) {
+ super(context, fragmentManager, contactInfoHelper, contactPhotoManager);
+ }
+
+ public static BlockedNumbersAdapter newBlockedNumbersAdapter(
+ Context context, FragmentManager fragmentManager) {
+ return new BlockedNumbersAdapter(
+ context,
+ fragmentManager,
+ new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)),
+ ContactPhotoManager.getInstance(context));
+ }
+
+ @Override
+ public void bindView(View view, final Context context, Cursor cursor) {
+ super.bindView(view, context, cursor);
+ final Integer id = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
+ final String countryIso =
+ cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.COUNTRY_ISO));
+ final String number = cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER));
+ final String normalizedNumber =
+ cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NORMALIZED_NUMBER));
+
+ final View deleteButton = view.findViewById(R.id.delete_button);
+ deleteButton.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ BlockNumberDialogFragment.show(
+ id,
+ number,
+ countryIso,
+ PhoneNumberUtils.formatNumber(number, countryIso),
+ R.id.blocked_numbers_activity_container,
+ getFragmentManager(),
+ new BlockNumberDialogFragment.Callback() {
+ @Override
+ public void onFilterNumberSuccess() {}
+
+ @Override
+ public void onUnfilterNumberSuccess() {
+ Logger.get(context)
+ .logInteraction(InteractionEvent.Type.UNBLOCK_NUMBER_MANAGEMENT_SCREEN);
+ }
+
+ @Override
+ public void onChangeFilteredNumberUndo() {}
+ });
+ }
+ });
+
+ updateView(view, number, countryIso);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ // Always return false, so that the header with blocking-related options always shows.
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java
new file mode 100644
index 0000000..f53a458
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2015 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.app.filterednumber;
+
+import android.app.ListFragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.BlockedNumbersMigrator;
+import com.android.dialer.blocking.BlockedNumbersMigrator.Listener;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.blocking.FilteredNumbersUtil.CheckForSendToVoicemailContactListener;
+import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker;
+
+public class BlockedNumbersFragment extends ListFragment
+ implements LoaderManager.LoaderCallbacks<Cursor>,
+ View.OnClickListener,
+ VisualVoicemailEnabledChecker.Callback {
+
+ private static final char ADD_BLOCKED_NUMBER_ICON_LETTER = '+';
+ protected View migratePromoView;
+ private BlockedNumbersMigrator blockedNumbersMigratorForTest;
+ private TextView blockedNumbersText;
+ private TextView footerText;
+ private BlockedNumbersAdapter mAdapter;
+ private VisualVoicemailEnabledChecker mVoicemailEnabledChecker;
+ private View mImportSettings;
+ private View mBlockedNumbersDisabledForEmergency;
+ private View mBlockedNumberListDivider;
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ LayoutInflater inflater =
+ (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ getListView().addHeaderView(inflater.inflate(R.layout.blocked_number_header, null));
+ getListView().addFooterView(inflater.inflate(R.layout.blocked_number_footer, null));
+ //replace the icon for add number with LetterTileDrawable(), so it will have identical style
+ ImageView addNumberIcon = (ImageView) getActivity().findViewById(R.id.add_number_icon);
+ LetterTileDrawable drawable = new LetterTileDrawable(getResources());
+ drawable.setLetter(ADD_BLOCKED_NUMBER_ICON_LETTER);
+ drawable.setColor(
+ ActivityCompat.getColor(getActivity(), R.color.add_blocked_number_icon_color));
+ drawable.setIsCircular(true);
+ addNumberIcon.setImageDrawable(drawable);
+
+ if (mAdapter == null) {
+ mAdapter =
+ BlockedNumbersAdapter.newBlockedNumbersAdapter(
+ getContext(), getActivity().getFragmentManager());
+ }
+ setListAdapter(mAdapter);
+
+ blockedNumbersText = (TextView) getListView().findViewById(R.id.blocked_number_text_view);
+ migratePromoView = getListView().findViewById(R.id.migrate_promo);
+ getListView().findViewById(R.id.migrate_promo_allow_button).setOnClickListener(this);
+ mImportSettings = getListView().findViewById(R.id.import_settings);
+ mBlockedNumbersDisabledForEmergency =
+ getListView().findViewById(R.id.blocked_numbers_disabled_for_emergency);
+ mBlockedNumberListDivider = getActivity().findViewById(R.id.blocked_number_list_divider);
+ getListView().findViewById(R.id.import_button).setOnClickListener(this);
+ getListView().findViewById(R.id.view_numbers_button).setOnClickListener(this);
+ getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(this);
+
+ footerText = (TextView) getActivity().findViewById(R.id.blocked_number_footer_textview);
+ mVoicemailEnabledChecker = new VisualVoicemailEnabledChecker(getContext(), this);
+ mVoicemailEnabledChecker.asyncUpdate();
+ updateActiveVoicemailProvider();
+ }
+
+ @Override
+ public void onDestroy() {
+ setListAdapter(null);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ ColorDrawable backgroundDrawable =
+ new ColorDrawable(ActivityCompat.getColor(getActivity(), R.color.dialer_theme_color));
+ actionBar.setBackgroundDrawable(backgroundDrawable);
+ actionBar.setDisplayShowCustomEnabled(false);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setDisplayShowHomeEnabled(true);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setTitle(R.string.manage_blocked_numbers_label);
+
+ // If the device can use the framework blocking solution, users should not be able to add
+ // new blocked numbers from the Blocked Management UI. They will be shown a promo card
+ // asking them to migrate to new blocking instead.
+ if (FilteredNumberCompat.canUseNewFiltering()) {
+ migratePromoView.setVisibility(View.VISIBLE);
+ blockedNumbersText.setVisibility(View.GONE);
+ getListView().findViewById(R.id.add_number_linear_layout).setVisibility(View.GONE);
+ getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(null);
+ mBlockedNumberListDivider.setVisibility(View.GONE);
+ mImportSettings.setVisibility(View.GONE);
+ getListView().findViewById(R.id.import_button).setOnClickListener(null);
+ getListView().findViewById(R.id.view_numbers_button).setOnClickListener(null);
+ mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE);
+ footerText.setVisibility(View.GONE);
+ } else {
+ FilteredNumbersUtil.checkForSendToVoicemailContact(
+ getActivity(),
+ new CheckForSendToVoicemailContactListener() {
+ @Override
+ public void onComplete(boolean hasSendToVoicemailContact) {
+ final int visibility = hasSendToVoicemailContact ? View.VISIBLE : View.GONE;
+ mImportSettings.setVisibility(visibility);
+ }
+ });
+ }
+
+ // All views except migrate and the block list are hidden when new filtering is available
+ if (!FilteredNumberCompat.canUseNewFiltering()
+ && FilteredNumbersUtil.hasRecentEmergencyCall(getContext())) {
+ mBlockedNumbersDisabledForEmergency.setVisibility(View.VISIBLE);
+ } else {
+ mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE);
+ }
+
+ mVoicemailEnabledChecker.asyncUpdate();
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.blocked_number_fragment, container, false);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ final String[] projection = {
+ FilteredNumberContract.FilteredNumberColumns._ID,
+ FilteredNumberContract.FilteredNumberColumns.COUNTRY_ISO,
+ FilteredNumberContract.FilteredNumberColumns.NUMBER,
+ FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER
+ };
+ final String selection =
+ FilteredNumberContract.FilteredNumberColumns.TYPE
+ + "="
+ + FilteredNumberContract.FilteredNumberTypes.BLOCKED_NUMBER;
+ return new CursorLoader(
+ getContext(),
+ FilteredNumberContract.FilteredNumber.CONTENT_URI,
+ projection,
+ selection,
+ null,
+ null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ mAdapter.swapCursor(data);
+ if (FilteredNumberCompat.canUseNewFiltering() || data.getCount() == 0) {
+ mBlockedNumberListDivider.setVisibility(View.INVISIBLE);
+ } else {
+ mBlockedNumberListDivider.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ mAdapter.swapCursor(null);
+ }
+
+ @Override
+ public void onClick(final View view) {
+ final BlockedNumbersSettingsActivity activity = (BlockedNumbersSettingsActivity) getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ int resId = view.getId();
+ if (resId == R.id.add_number_linear_layout) {
+ activity.showSearchUi();
+ } else if (resId == R.id.view_numbers_button) {
+ activity.showNumbersToImportPreviewUi();
+ } else if (resId == R.id.import_button) {
+ FilteredNumbersUtil.importSendToVoicemailContacts(
+ activity,
+ new ImportSendToVoicemailContactsListener() {
+ @Override
+ public void onImportComplete() {
+ mImportSettings.setVisibility(View.GONE);
+ }
+ });
+ } else if (resId == R.id.migrate_promo_allow_button) {
+ view.setEnabled(false);
+ (blockedNumbersMigratorForTest != null
+ ? blockedNumbersMigratorForTest
+ : new BlockedNumbersMigrator(getContext()))
+ .migrate(
+ new Listener() {
+ @Override
+ public void onComplete() {
+ getContext()
+ .startActivity(
+ FilteredNumberCompat.createManageBlockedNumbersIntent(getContext()));
+ // Remove this activity from the backstack
+ activity.finish();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onVisualVoicemailEnabledStatusChanged(boolean newStatus) {
+ updateActiveVoicemailProvider();
+ }
+
+ private void updateActiveVoicemailProvider() {
+ if (getActivity() == null || getActivity().isFinishing()) {
+ return;
+ }
+ if (mVoicemailEnabledChecker.isVisualVoicemailEnabled()) {
+ footerText.setText(R.string.block_number_footer_message_vvm);
+ } else {
+ footerText.setText(R.string.block_number_footer_message_no_vvm);
+ }
+ }
+
+ void setBlockedNumbersMigratorForTest(BlockedNumbersMigrator blockedNumbersMigrator) {
+ blockedNumbersMigratorForTest = blockedNumbersMigrator;
+ }
+}
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
new file mode 100644
index 0000000..eef9207
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015 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.app.filterednumber;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import com.android.dialer.app.R;
+import com.android.dialer.app.list.BlockedListSearchFragment;
+import com.android.dialer.app.list.SearchFragment;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+
+public class BlockedNumbersSettingsActivity extends AppCompatActivity
+ implements SearchFragment.HostInterface {
+
+ private static final String TAG_BLOCKED_MANAGEMENT_FRAGMENT = "blocked_management";
+ private static final String TAG_BLOCKED_SEARCH_FRAGMENT = "blocked_search";
+ private static final String TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT = "view_numbers_to_import";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.blocked_numbers_activity);
+
+ // If savedInstanceState != null, the Activity will automatically restore the last fragment.
+ if (savedInstanceState == null) {
+ showManagementUi();
+ }
+ }
+
+ /** Shows fragment with the list of currently blocked numbers and settings related to blocking. */
+ public void showManagementUi() {
+ BlockedNumbersFragment fragment =
+ (BlockedNumbersFragment)
+ getFragmentManager().findFragmentByTag(TAG_BLOCKED_MANAGEMENT_FRAGMENT);
+ if (fragment == null) {
+ fragment = new BlockedNumbersFragment();
+ }
+
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_MANAGEMENT_FRAGMENT)
+ .commit();
+
+ Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_MANAGEMENT, this);
+ }
+
+ /** Shows fragment with search UI for browsing/finding numbers to block. */
+ public void showSearchUi() {
+ BlockedListSearchFragment fragment =
+ (BlockedListSearchFragment)
+ getFragmentManager().findFragmentByTag(TAG_BLOCKED_SEARCH_FRAGMENT);
+ if (fragment == null) {
+ fragment = new BlockedListSearchFragment();
+ fragment.setHasOptionsMenu(false);
+ fragment.setShowEmptyListForNullQuery(true);
+ fragment.setDirectorySearchEnabled(false);
+ }
+
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_SEARCH_FRAGMENT)
+ .addToBackStack(null)
+ .commit();
+
+ Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_ADD_NUMBER, this);
+ }
+
+ /**
+ * Shows fragment with UI to preview the numbers of contacts currently marked as send-to-voicemail
+ * in Contacts. These numbers can be imported into Dialer's blocked number list.
+ */
+ public void showNumbersToImportPreviewUi() {
+ ViewNumbersToImportFragment fragment =
+ (ViewNumbersToImportFragment)
+ getFragmentManager().findFragmentByTag(TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT);
+ if (fragment == null) {
+ fragment = new ViewNumbersToImportFragment();
+ }
+
+ getFragmentManager()
+ .beginTransaction()
+ .replace(
+ R.id.blocked_numbers_activity_container, fragment, TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT)
+ .addToBackStack(null)
+ .commit();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ // TODO: Achieve back navigation without overriding onBackPressed.
+ if (getFragmentManager().getBackStackEntryCount() > 0) {
+ getFragmentManager().popBackStack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean isActionBarShowing() {
+ return false;
+ }
+
+ @Override
+ public boolean isDialpadShown() {
+ return false;
+ }
+
+ @Override
+ public int getDialpadHeight() {
+ return 0;
+ }
+
+ @Override
+ public int getActionBarHideOffset() {
+ return 0;
+ }
+
+ @Override
+ public int getActionBarHeight() {
+ return 0;
+ }
+}
diff --git a/java/com/android/dialer/app/filterednumber/NumbersAdapter.java b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java
new file mode 100644
index 0000000..f71517a
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2015 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.app.filterednumber;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.QuickContactBadge;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.app.R;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+public class NumbersAdapter extends SimpleCursorAdapter {
+
+ private Context mContext;
+ private FragmentManager mFragmentManager;
+ private ContactInfoHelper mContactInfoHelper;
+ private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+ private ContactPhotoManager mContactPhotoManager;
+
+ public NumbersAdapter(
+ Context context,
+ FragmentManager fragmentManager,
+ ContactInfoHelper contactInfoHelper,
+ ContactPhotoManager contactPhotoManager) {
+ super(context, R.layout.blocked_number_item, null, new String[] {}, new int[] {}, 0);
+ mContext = context;
+ mFragmentManager = fragmentManager;
+ mContactInfoHelper = contactInfoHelper;
+ mContactPhotoManager = contactPhotoManager;
+ }
+
+ public void updateView(View view, String number, String countryIso) {
+ final TextView callerName = (TextView) view.findViewById(R.id.caller_name);
+ final TextView callerNumber = (TextView) view.findViewById(R.id.caller_number);
+ final QuickContactBadge quickContactBadge =
+ (QuickContactBadge) view.findViewById(R.id.quick_contact_photo);
+ quickContactBadge.setOverlay(null);
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ quickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+ }
+
+ ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+ if (info == null) {
+ info = new ContactInfo();
+ info.number = number;
+ }
+ final CharSequence locationOrType = getNumberTypeOrLocation(info);
+ final String displayNumber = getDisplayNumber(info);
+ final String displayNumberStr =
+ mBidiFormatter.unicodeWrap(displayNumber, TextDirectionHeuristics.LTR);
+
+ String nameForDefaultImage;
+ if (!TextUtils.isEmpty(info.name)) {
+ nameForDefaultImage = info.name;
+ callerName.setText(info.name);
+ callerNumber.setText(locationOrType + " " + displayNumberStr);
+ } else {
+ nameForDefaultImage = displayNumber;
+ callerName.setText(displayNumberStr);
+ if (!TextUtils.isEmpty(locationOrType)) {
+ callerNumber.setText(locationOrType);
+ callerNumber.setVisibility(View.VISIBLE);
+ } else {
+ callerNumber.setVisibility(View.GONE);
+ }
+ }
+ loadContactPhoto(info, nameForDefaultImage, quickContactBadge);
+ }
+
+ private void loadContactPhoto(ContactInfo info, String displayName, QuickContactBadge badge) {
+ final String lookupKey =
+ info.lookupUri == null ? null : UriUtils.getLookupKeyFromUri(info.lookupUri);
+ final int contactType =
+ mContactInfoHelper.isBusiness(info.sourceType)
+ ? ContactPhotoManager.TYPE_BUSINESS
+ : ContactPhotoManager.TYPE_DEFAULT;
+ final DefaultImageRequest request =
+ new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */);
+ badge.assignContactUri(info.lookupUri);
+ badge.setContentDescription(
+ mContext.getResources().getString(R.string.description_contact_details, displayName));
+ mContactPhotoManager.loadDirectoryPhoto(
+ badge, info.photoUri, false /* darkTheme */, true /* isCircular */, request);
+ }
+
+ private String getDisplayNumber(ContactInfo info) {
+ if (!TextUtils.isEmpty(info.formattedNumber)) {
+ return info.formattedNumber;
+ } else if (!TextUtils.isEmpty(info.number)) {
+ return info.number;
+ } else {
+ return "";
+ }
+ }
+
+ private CharSequence getNumberTypeOrLocation(ContactInfo info) {
+ if (!TextUtils.isEmpty(info.name)) {
+ return ContactsContract.CommonDataKinds.Phone.getTypeLabel(
+ mContext.getResources(), info.type, info.label);
+ } else {
+ return PhoneNumberHelper.getGeoDescription(mContext, info.number);
+ }
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+
+ protected FragmentManager getFragmentManager() {
+ return mFragmentManager;
+ }
+}
diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java
new file mode 100644
index 0000000..5228a1d
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 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.app.filterednumber;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+
+public class ViewNumbersToImportAdapter extends NumbersAdapter {
+
+ private ViewNumbersToImportAdapter(
+ Context context,
+ FragmentManager fragmentManager,
+ ContactInfoHelper contactInfoHelper,
+ ContactPhotoManager contactPhotoManager) {
+ super(context, fragmentManager, contactInfoHelper, contactPhotoManager);
+ }
+
+ public static ViewNumbersToImportAdapter newViewNumbersToImportAdapter(
+ Context context, FragmentManager fragmentManager) {
+ return new ViewNumbersToImportAdapter(
+ context,
+ fragmentManager,
+ new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)),
+ ContactPhotoManager.getInstance(context));
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ super.bindView(view, context, cursor);
+
+ final String number = cursor.getString(FilteredNumbersUtil.PhoneQuery.NUMBER_COLUMN_INDEX);
+
+ view.findViewById(R.id.delete_button).setVisibility(View.GONE);
+ updateView(view, number, null /* countryIso */);
+ }
+}
diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java
new file mode 100644
index 0000000..d45f61e
--- /dev/null
+++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2015 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.app.filterednumber;
+
+import android.app.ListFragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener;
+
+public class ViewNumbersToImportFragment extends ListFragment
+ implements LoaderManager.LoaderCallbacks<Cursor>, View.OnClickListener {
+
+ private ViewNumbersToImportAdapter mAdapter;
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ if (mAdapter == null) {
+ mAdapter =
+ ViewNumbersToImportAdapter.newViewNumbersToImportAdapter(
+ getContext(), getActivity().getFragmentManager());
+ }
+ setListAdapter(mAdapter);
+ }
+
+ @Override
+ public void onDestroy() {
+ setListAdapter(null);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ actionBar.setTitle(R.string.import_send_to_voicemail_numbers_label);
+ actionBar.setDisplayShowCustomEnabled(false);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setDisplayShowHomeEnabled(true);
+ actionBar.setDisplayShowTitleEnabled(true);
+
+ getActivity().findViewById(R.id.cancel_button).setOnClickListener(this);
+ getActivity().findViewById(R.id.import_button).setOnClickListener(this);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.view_numbers_to_import_fragment, container, false);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ final CursorLoader cursorLoader =
+ new CursorLoader(
+ getContext(),
+ Phone.CONTENT_URI,
+ FilteredNumbersUtil.PhoneQuery.PROJECTION,
+ FilteredNumbersUtil.PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+ null,
+ null);
+ return cursorLoader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ mAdapter.swapCursor(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ mAdapter.swapCursor(null);
+ }
+
+ @Override
+ public void onClick(final View view) {
+ if (view.getId() == R.id.import_button) {
+ FilteredNumbersUtil.importSendToVoicemailContacts(
+ getContext(),
+ new ImportSendToVoicemailContactsListener() {
+ @Override
+ public void onImportComplete() {
+ if (getActivity() != null) {
+ getActivity().onBackPressed();
+ }
+ }
+ });
+ } else if (view.getId() == R.id.cancel_button) {
+ getActivity().onBackPressed();
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
new file mode 100644
index 0000000..2125a15
--- /dev/null
+++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
@@ -0,0 +1,47 @@
+/*
+ * 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.app.legacybindings;
+
+import android.app.Activity;
+import android.view.ViewGroup;
+import com.android.dialer.app.calllog.CallLogAdapter;
+import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.app.contactinfo.ContactInfoCache;
+import com.android.dialer.app.list.RegularSearchFragment;
+import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+
+/**
+ * These are old bindings between Dialer and the container application. All new bindings should be
+ * added to the bindings module and not here.
+ */
+public interface DialerLegacyBindings {
+
+ /**
+ * activityType must be one of following constants: CallLogAdapter.ACTIVITY_TYPE_CALL_LOG, or
+ * CallLogAdapter.ACTIVITY_TYPE_DIALTACTS.
+ */
+ CallLogAdapter newCallLogAdapter(
+ Activity activity,
+ ViewGroup alertContainer,
+ CallLogAdapter.CallFetcher callFetcher,
+ CallLogCache callLogCache,
+ ContactInfoCache contactInfoCache,
+ VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ int activityType);
+
+ RegularSearchFragment newRegularSearchFragment();
+}
diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java
new file mode 100644
index 0000000..70d379c
--- /dev/null
+++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.app.legacybindings;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the dialer module
+ * to get references to the DialerLegacyBindings.
+ */
+public interface DialerLegacyBindingsFactory {
+
+ DialerLegacyBindings newDialerLegacyBindings();
+}
diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
new file mode 100644
index 0000000..f01df78
--- /dev/null
+++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
@@ -0,0 +1,53 @@
+/*
+ * 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.app.legacybindings;
+
+import android.app.Activity;
+import android.view.ViewGroup;
+import com.android.dialer.app.calllog.CallLogAdapter;
+import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.app.contactinfo.ContactInfoCache;
+import com.android.dialer.app.list.RegularSearchFragment;
+import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+
+/** Default implementation for dialer legacy bindings. */
+public class DialerLegacyBindingsStub implements DialerLegacyBindings {
+
+ @Override
+ public CallLogAdapter newCallLogAdapter(
+ Activity activity,
+ ViewGroup alertContainer,
+ CallLogAdapter.CallFetcher callFetcher,
+ CallLogCache callLogCache,
+ ContactInfoCache contactInfoCache,
+ VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ int activityType) {
+ return new CallLogAdapter(
+ activity,
+ alertContainer,
+ callFetcher,
+ callLogCache,
+ contactInfoCache,
+ voicemailPlaybackPresenter,
+ activityType);
+ }
+
+ @Override
+ public RegularSearchFragment newRegularSearchFragment() {
+ return new RegularSearchFragment();
+ }
+}
diff --git a/java/com/android/dialer/app/list/AllContactsFragment.java b/java/com/android/dialer/app/list/AllContactsFragment.java
new file mode 100644
index 0000000..093e8f3
--- /dev/null
+++ b/java/com/android/dialer/app/list/AllContactsFragment.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import static android.Manifest.permission.READ_CONTACTS;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.QuickContact;
+import android.support.annotation.Nullable;
+import android.support.v13.app.FragmentCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.list.ContactEntryListFragment;
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.list.DefaultContactListAdapter;
+import com.android.contacts.common.util.FabUtil;
+import com.android.dialer.app.R;
+import com.android.dialer.app.list.ListsFragment.ListsPage;
+import com.android.dialer.app.widget.EmptyContentView;
+import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Fragments to show all contacts with phone numbers. */
+public class AllContactsFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+ implements ListsPage,
+ OnEmptyViewActionButtonClickedListener,
+ FragmentCompat.OnRequestPermissionsResultCallback {
+
+ private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
+
+ private EmptyContentView mEmptyListView;
+
+ /**
+ * Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS
+ * permission is granted via the UI in another fragment.
+ */
+ private BroadcastReceiver mReadContactsPermissionGrantedReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ reloadData();
+ }
+ };
+
+ public AllContactsFragment() {
+ setQuickContactEnabled(false);
+ setAdjustSelectionBoundsEnabled(true);
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ setDarkTheme(false);
+ setVisibleScrollbarEnabled(true);
+ }
+
+ @Override
+ public void onViewCreated(View view, android.os.Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
+ mEmptyListView.setImage(R.drawable.empty_contacts);
+ mEmptyListView.setDescription(R.string.all_contacts_empty);
+ mEmptyListView.setActionClickedListener(this);
+ getListView().setEmptyView(mEmptyListView);
+ mEmptyListView.setVisibility(View.GONE);
+
+ FabUtil.addBottomPaddingToListViewForFab(getListView(), getResources());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ PermissionsUtil.registerPermissionReceiver(
+ getActivity(), mReadContactsPermissionGrantedReceiver, READ_CONTACTS);
+ }
+
+ @Override
+ public void onStop() {
+ PermissionsUtil.unregisterPermissionReceiver(
+ getActivity(), mReadContactsPermissionGrantedReceiver);
+ super.onStop();
+ }
+
+ @Override
+ protected void startLoading() {
+ if (PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) {
+ super.startLoading();
+ mEmptyListView.setDescription(R.string.all_contacts_empty);
+ mEmptyListView.setActionLabel(R.string.all_contacts_empty_add_contact_action);
+ } else {
+ mEmptyListView.setDescription(R.string.permission_no_contacts);
+ mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
+ mEmptyListView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ super.onLoadFinished(loader, data);
+
+ if (data == null || data.getCount() == 0) {
+ mEmptyListView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ final DefaultContactListAdapter adapter =
+ new DefaultContactListAdapter(getActivity()) {
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+ itemView.setTag(this.getContactUri(partition, cursor));
+ }
+ };
+ adapter.setDisplayPhotos(true);
+ adapter.setFilter(
+ ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_DEFAULT));
+ adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled());
+ return adapter;
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.all_contacts_fragment, null);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final Uri uri = (Uri) view.getTag();
+ if (uri != null) {
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ QuickContact.showQuickContact(getContext(), view, uri, null, Phone.CONTENT_ITEM_TYPE);
+ } else {
+ QuickContact.showQuickContact(getActivity(), view, uri, QuickContact.MODE_LARGE, null);
+ }
+ }
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ // Do nothing. Implemented to satisfy ContactEntryListFragment.
+ }
+
+ @Override
+ public void onEmptyViewActionButtonClicked() {
+ final Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) {
+ FragmentCompat.requestPermissions(
+ this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
+ } else {
+ // Add new contact
+ DialerUtils.startActivityWithErrorToast(
+ activity, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+ // Force a refresh of the data since we were missing the permission before this.
+ reloadData();
+ }
+ }
+ }
+
+ @Override
+ public void onPageResume(@Nullable Activity activity) {
+ LogUtil.i("AllContactsFragment.onPageResume", null);
+ }
+
+ @Override
+ public void onPagePause(@Nullable Activity activity) {
+ LogUtil.i("AllContactsFragment.onPagePause", null);
+ }
+}
diff --git a/java/com/android/dialer/app/list/BlockedListSearchAdapter.java b/java/com/android/dialer/app/list/BlockedListSearchAdapter.java
new file mode 100644
index 0000000..a90ce7a
--- /dev/null
+++ b/java/com/android/dialer/app/list/BlockedListSearchAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 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.app.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.view.View;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+
+/** List adapter to display search results for adding a blocked number. */
+public class BlockedListSearchAdapter extends RegularSearchListAdapter {
+
+ private Resources mResources;
+ private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+
+ public BlockedListSearchAdapter(Context context) {
+ super(context);
+ mResources = context.getResources();
+ disableAllShortcuts();
+ setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, true);
+
+ mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(context);
+ }
+
+ @Override
+ protected boolean isChanged(boolean showNumberShortcuts) {
+ return setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, showNumberShortcuts || mIsQuerySipAddress);
+ }
+
+ public void setViewBlocked(ContactListItemView view, Integer id) {
+ view.setTag(R.id.block_id, id);
+ final int textColor = mResources.getColor(R.color.blocked_number_block_color);
+ view.getDataView().setTextColor(textColor);
+ view.getLabelView().setTextColor(textColor);
+ //TODO: Add icon
+ }
+
+ public void setViewUnblocked(ContactListItemView view) {
+ view.setTag(R.id.block_id, null);
+ final int textColor = mResources.getColor(R.color.dialer_secondary_text_color);
+ view.getDataView().setTextColor(textColor);
+ view.getLabelView().setTextColor(textColor);
+ //TODO: Remove icon
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+
+ final ContactListItemView view = (ContactListItemView) itemView;
+ // Reset view state to unblocked.
+ setViewUnblocked(view);
+
+ final String number = getPhoneNumber(position);
+ final String countryIso = GeoUtil.getCurrentCountryIso(mContext);
+ final FilteredNumberAsyncQueryHandler.OnCheckBlockedListener onCheckListener =
+ new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(Integer id) {
+ if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ setViewBlocked(view, id);
+ }
+ }
+ };
+ mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso);
+ }
+}
diff --git a/java/com/android/dialer/app/list/BlockedListSearchFragment.java b/java/com/android/dialer/app/list/BlockedListSearchFragment.java
new file mode 100644
index 0000000..2129981
--- /dev/null
+++ b/java/com/android/dialer/app/list/BlockedListSearchFragment.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2015 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.app.list;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.telephony.PhoneNumberUtils;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.Toast;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.R;
+import com.android.dialer.app.widget.SearchEditTextLayout;
+import com.android.dialer.blocking.BlockNumberDialogFragment;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+
+public class BlockedListSearchFragment extends RegularSearchFragment
+ implements BlockNumberDialogFragment.Callback {
+
+ private static final String TAG = BlockedListSearchFragment.class.getSimpleName();
+
+ private final TextWatcher mPhoneSearchQueryTextListener =
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ setQueryString(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ };
+ private final SearchEditTextLayout.Callback mSearchLayoutCallback =
+ new SearchEditTextLayout.Callback() {
+ @Override
+ public void onBackButtonClicked() {
+ getActivity().onBackPressed();
+ }
+
+ @Override
+ public void onSearchViewClicked() {}
+ };
+ private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+ private EditText mSearchView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setShowEmptyListForNullQuery(true);
+ /*
+ * Pass in the empty string here so ContactEntryListFragment#setQueryString interprets it as
+ * an empty search query, rather than as an uninitalized value. In the latter case, the
+ * adapter returned by #createListAdapter is used, which populates the view with contacts.
+ * Passing in the empty string forces ContactEntryListFragment to interpret it as an empty
+ * query, which results in showing an empty view
+ */
+ setQueryString(getQueryString() == null ? "" : getQueryString());
+ mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(getContext());
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ actionBar.setCustomView(R.layout.search_edittext);
+ actionBar.setDisplayShowCustomEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setDisplayShowHomeEnabled(false);
+
+ final SearchEditTextLayout searchEditTextLayout =
+ (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container);
+ searchEditTextLayout.expand(false, true);
+ searchEditTextLayout.setCallback(mSearchLayoutCallback);
+ searchEditTextLayout.setBackgroundDrawable(null);
+
+ mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view);
+ mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
+ mSearchView.setHint(R.string.block_number_search_hint);
+
+ searchEditTextLayout
+ .findViewById(R.id.search_box_expanded)
+ .setBackgroundColor(getContext().getResources().getColor(android.R.color.white));
+
+ if (!TextUtils.isEmpty(getQueryString())) {
+ mSearchView.setText(getQueryString());
+ }
+
+ // TODO: Don't set custom text size; use default search text size.
+ mSearchView.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ getResources().getDimension(R.dimen.blocked_number_search_text_size));
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ BlockedListSearchAdapter adapter = new BlockedListSearchAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ // Don't show SIP addresses.
+ adapter.setUseCallableUri(false);
+ // Keep in sync with the queryString set in #onCreate
+ adapter.setQueryString(getQueryString() == null ? "" : getQueryString());
+ return adapter;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ super.onItemClick(parent, view, position, id);
+ final int adapterPosition = position - getListView().getHeaderViewsCount();
+ final BlockedListSearchAdapter adapter = (BlockedListSearchAdapter) getAdapter();
+ final int shortcutType = adapter.getShortcutTypeFromPosition(adapterPosition);
+ final Integer blockId = (Integer) view.getTag(R.id.block_id);
+ final String number;
+ switch (shortcutType) {
+ case DialerPhoneNumberListAdapter.SHORTCUT_INVALID:
+ // Handles click on a search result, either contact or nearby places result.
+ number = adapter.getPhoneNumber(adapterPosition);
+ blockContactNumber(number, blockId);
+ break;
+ case DialerPhoneNumberListAdapter.SHORTCUT_BLOCK_NUMBER:
+ // Handles click on 'Block number' shortcut to add the user query as a number.
+ number = adapter.getQueryString();
+ blockNumber(number);
+ break;
+ default:
+ Log.w(TAG, "Ignoring unsupported shortcut type: " + shortcutType);
+ break;
+ }
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ // Prevent SearchFragment.onItemClicked from being called.
+ }
+
+ private void blockNumber(final String number) {
+ final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+ final OnCheckBlockedListener onCheckListener =
+ new OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(Integer id) {
+ if (id == null) {
+ BlockNumberDialogFragment.show(
+ id,
+ number,
+ countryIso,
+ PhoneNumberUtils.formatNumber(number, countryIso),
+ R.id.blocked_numbers_activity_container,
+ getFragmentManager(),
+ BlockedListSearchFragment.this);
+ } else if (id == FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ Toast.makeText(
+ getContext(),
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.invalidNumber, number),
+ Toast.LENGTH_SHORT)
+ .show();
+ } else {
+ Toast.makeText(
+ getContext(),
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.alreadyBlocked, number),
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+ };
+ mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso);
+ }
+
+ @Override
+ public void onFilterNumberSuccess() {
+ Logger.get(getContext()).logInteraction(InteractionEvent.Type.BLOCK_NUMBER_MANAGEMENT_SCREEN);
+ goBack();
+ }
+
+ @Override
+ public void onUnfilterNumberSuccess() {
+ Log.wtf(TAG, "Unblocked a number from the BlockedListSearchFragment");
+ goBack();
+ }
+
+ private void goBack() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+ activity.onBackPressed();
+ }
+
+ @Override
+ public void onChangeFilteredNumberUndo() {
+ getAdapter().notifyDataSetChanged();
+ }
+
+ private void blockContactNumber(final String number, final Integer blockId) {
+ if (blockId != null) {
+ Toast.makeText(
+ getContext(),
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.alreadyBlocked, number),
+ Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ BlockNumberDialogFragment.show(
+ blockId,
+ number,
+ GeoUtil.getCurrentCountryIso(getContext()),
+ number,
+ R.id.blocked_numbers_activity_container,
+ getFragmentManager(),
+ this);
+ }
+}
diff --git a/java/com/android/dialer/app/list/ContentChangedFilter.java b/java/com/android/dialer/app/list/ContentChangedFilter.java
new file mode 100644
index 0000000..663846d
--- /dev/null
+++ b/java/com/android/dialer/app/list/ContentChangedFilter.java
@@ -0,0 +1,56 @@
+/*
+ * 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.app.list;
+
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * AccessibilityDelegate that will filter out TYPE_WINDOW_CONTENT_CHANGED Used to suppress "Showing
+ * items x of y" from firing of ListView whenever it's content changes. AccessibilityEvent can only
+ * be rejected at a view's parent once it is generated, use addToParent() to add this delegate to
+ * the parent.
+ */
+public class ContentChangedFilter extends AccessibilityDelegate {
+
+ //the view we don't want TYPE_WINDOW_CONTENT_CHANGED to fire.
+ private View mView;
+
+ private ContentChangedFilter(View view) {
+ super();
+ mView = view;
+ }
+
+ /** Add this delegate to the parent of @param view to filter out TYPE_WINDOW_CONTENT_CHANGED */
+ public static void addToParent(View view) {
+ View parent = (View) view.getParent();
+ parent.setAccessibilityDelegate(new ContentChangedFilter(view));
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(
+ ViewGroup host, View child, AccessibilityEvent event) {
+ if (child == mView) {
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
+ return false;
+ }
+ }
+ return super.onRequestSendAccessibilityEvent(host, child, event);
+ }
+}
diff --git a/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java b/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java
new file mode 100644
index 0000000..7e2525f
--- /dev/null
+++ b/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java
@@ -0,0 +1,228 @@
+/*
+ * 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.app.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.PhoneNumberListAdapter;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.R;
+import com.android.dialer.util.CallUtil;
+
+/**
+ * {@link PhoneNumberListAdapter} with the following added shortcuts, that are displayed as list
+ * items: 1) Directly calling the phone number query 2) Adding the phone number query to a contact
+ *
+ * <p>These shortcuts can be enabled or disabled to toggle whether or not they show up in the list.
+ */
+public class DialerPhoneNumberListAdapter extends PhoneNumberListAdapter {
+
+ public static final int SHORTCUT_INVALID = -1;
+ public static final int SHORTCUT_DIRECT_CALL = 0;
+ public static final int SHORTCUT_CREATE_NEW_CONTACT = 1;
+ public static final int SHORTCUT_ADD_TO_EXISTING_CONTACT = 2;
+ public static final int SHORTCUT_SEND_SMS_MESSAGE = 3;
+ public static final int SHORTCUT_MAKE_VIDEO_CALL = 4;
+ public static final int SHORTCUT_BLOCK_NUMBER = 5;
+ public static final int SHORTCUT_COUNT = 6;
+ private final boolean[] mShortcutEnabled = new boolean[SHORTCUT_COUNT];
+ private final BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+ private String mFormattedQueryString;
+ private String mCountryIso;
+ private boolean mVideoCallingEnabled = false;
+
+ public DialerPhoneNumberListAdapter(Context context) {
+ super(context);
+
+ mCountryIso = GeoUtil.getCurrentCountryIso(context);
+ mVideoCallingEnabled = CallUtil.isVideoEnabled(context);
+ }
+
+ @Override
+ public int getCount() {
+ return super.getCount() + getShortcutCount();
+ }
+
+ /** @return The number of enabled shortcuts. Ranges from 0 to a maximum of SHORTCUT_COUNT */
+ public int getShortcutCount() {
+ int count = 0;
+ for (int i = 0; i < mShortcutEnabled.length; i++) {
+ if (mShortcutEnabled[i]) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public void disableAllShortcuts() {
+ for (int i = 0; i < mShortcutEnabled.length; i++) {
+ mShortcutEnabled[i] = false;
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ final int shortcut = getShortcutTypeFromPosition(position);
+ if (shortcut >= 0) {
+ // shortcutPos should always range from 1 to SHORTCUT_COUNT
+ return super.getViewTypeCount() + shortcut;
+ } else {
+ return super.getItemViewType(position);
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ // Number of item view types in the super implementation + 2 for the 2 new shortcuts
+ return super.getViewTypeCount() + SHORTCUT_COUNT;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final int shortcutType = getShortcutTypeFromPosition(position);
+ if (shortcutType >= 0) {
+ if (convertView != null) {
+ assignShortcutToView((ContactListItemView) convertView, shortcutType);
+ return convertView;
+ } else {
+ final ContactListItemView v =
+ new ContactListItemView(getContext(), null, mVideoCallingEnabled);
+ assignShortcutToView(v, shortcutType);
+ return v;
+ }
+ } else {
+ return super.getView(position, convertView, parent);
+ }
+ }
+
+ @Override
+ protected ContactListItemView newView(
+ Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+ final ContactListItemView view = super.newView(context, partition, cursor, position, parent);
+
+ view.setSupportVideoCallIcon(mVideoCallingEnabled);
+ return view;
+ }
+
+ /**
+ * @param position The position of the item
+ * @return The enabled shortcut type matching the given position if the item is a shortcut, -1
+ * otherwise
+ */
+ public int getShortcutTypeFromPosition(int position) {
+ int shortcutCount = position - super.getCount();
+ if (shortcutCount >= 0) {
+ // Iterate through the array of shortcuts, looking only for shortcuts where
+ // mShortcutEnabled[i] is true
+ for (int i = 0; shortcutCount >= 0 && i < mShortcutEnabled.length; i++) {
+ if (mShortcutEnabled[i]) {
+ shortcutCount--;
+ if (shortcutCount < 0) {
+ return i;
+ }
+ }
+ }
+ throw new IllegalArgumentException(
+ "Invalid position - greater than cursor count " + " but not a shortcut.");
+ }
+ return SHORTCUT_INVALID;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return getShortcutCount() == 0 && super.isEmpty();
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ final int shortcutType = getShortcutTypeFromPosition(position);
+ if (shortcutType >= 0) {
+ return true;
+ } else {
+ return super.isEnabled(position);
+ }
+ }
+
+ private void assignShortcutToView(ContactListItemView v, int shortcutType) {
+ final CharSequence text;
+ final int drawableId;
+ final Resources resources = getContext().getResources();
+ final String number = getFormattedQueryString();
+ switch (shortcutType) {
+ case SHORTCUT_DIRECT_CALL:
+ text =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ resources,
+ R.string.search_shortcut_call_number,
+ mBidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR));
+ drawableId = R.drawable.ic_search_phone;
+ break;
+ case SHORTCUT_CREATE_NEW_CONTACT:
+ text = resources.getString(R.string.search_shortcut_create_new_contact);
+ drawableId = R.drawable.ic_search_add_contact;
+ break;
+ case SHORTCUT_ADD_TO_EXISTING_CONTACT:
+ text = resources.getString(R.string.search_shortcut_add_to_contact);
+ drawableId = R.drawable.ic_person_24dp;
+ break;
+ case SHORTCUT_SEND_SMS_MESSAGE:
+ text = resources.getString(R.string.search_shortcut_send_sms_message);
+ drawableId = R.drawable.ic_message_24dp;
+ break;
+ case SHORTCUT_MAKE_VIDEO_CALL:
+ text = resources.getString(R.string.search_shortcut_make_video_call);
+ drawableId = R.drawable.ic_videocam;
+ break;
+ case SHORTCUT_BLOCK_NUMBER:
+ text = resources.getString(R.string.search_shortcut_block_number);
+ drawableId = R.drawable.ic_not_interested_googblue_24dp;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid shortcut type");
+ }
+ v.setDrawableResource(drawableId);
+ v.setDisplayName(text);
+ v.setPhotoPosition(super.getPhotoPosition());
+ v.setAdjustSelectionBoundsEnabled(false);
+ }
+
+ /** @return True if the shortcut state (disabled vs enabled) was changed by this operation */
+ public boolean setShortcutEnabled(int shortcutType, boolean visible) {
+ final boolean changed = mShortcutEnabled[shortcutType] != visible;
+ mShortcutEnabled[shortcutType] = visible;
+ return changed;
+ }
+
+ public String getFormattedQueryString() {
+ return mFormattedQueryString;
+ }
+
+ @Override
+ public void setQueryString(String queryString) {
+ mFormattedQueryString =
+ PhoneNumberUtils.formatNumber(PhoneNumberUtils.normalizeNumber(queryString), mCountryIso);
+ super.setQueryString(queryString);
+ }
+}
diff --git a/java/com/android/dialer/app/list/DragDropController.java b/java/com/android/dialer/app/list/DragDropController.java
new file mode 100644
index 0000000..c22dd13
--- /dev/null
+++ b/java/com/android/dialer/app/list/DragDropController.java
@@ -0,0 +1,106 @@
+/*
+ * 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.app.list;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.view.View;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class that handles and combines drag events generated from multiple views, and then fires off
+ * events to any OnDragDropListeners that have registered for callbacks.
+ */
+public class DragDropController {
+
+ private final List<OnDragDropListener> mOnDragDropListeners = new ArrayList<OnDragDropListener>();
+ private final DragItemContainer mDragItemContainer;
+ private final int[] mLocationOnScreen = new int[2];
+
+ public DragDropController(DragItemContainer dragItemContainer) {
+ mDragItemContainer = dragItemContainer;
+ }
+
+ /** @return True if the drag is started, false if the drag is cancelled for some reason. */
+ boolean handleDragStarted(View v, int x, int y) {
+ int screenX = x;
+ int screenY = y;
+ // The coordinates in dragEvent of DragEvent.ACTION_DRAG_STARTED before NYC is window-related.
+ // This is fixed in NYC.
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ v.getLocationOnScreen(mLocationOnScreen);
+ screenX = x + mLocationOnScreen[0];
+ screenY = y + mLocationOnScreen[1];
+ }
+ final PhoneFavoriteSquareTileView tileView =
+ mDragItemContainer.getViewForLocation(screenX, screenY);
+ if (tileView == null) {
+ return false;
+ }
+ for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+ mOnDragDropListeners.get(i).onDragStarted(screenX, screenY, tileView);
+ }
+
+ return true;
+ }
+
+ public void handleDragHovered(View v, int x, int y) {
+ v.getLocationOnScreen(mLocationOnScreen);
+ final int screenX = x + mLocationOnScreen[0];
+ final int screenY = y + mLocationOnScreen[1];
+ final PhoneFavoriteSquareTileView view =
+ mDragItemContainer.getViewForLocation(screenX, screenY);
+ for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+ mOnDragDropListeners.get(i).onDragHovered(screenX, screenY, view);
+ }
+ }
+
+ public void handleDragFinished(int x, int y, boolean isRemoveView) {
+ if (isRemoveView) {
+ for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+ mOnDragDropListeners.get(i).onDroppedOnRemove();
+ }
+ }
+
+ for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+ mOnDragDropListeners.get(i).onDragFinished(x, y);
+ }
+ }
+
+ public void addOnDragDropListener(OnDragDropListener listener) {
+ if (!mOnDragDropListeners.contains(listener)) {
+ mOnDragDropListeners.add(listener);
+ }
+ }
+
+ public void removeOnDragDropListener(OnDragDropListener listener) {
+ if (mOnDragDropListeners.contains(listener)) {
+ mOnDragDropListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Callback interface used to retrieve views based on the current touch coordinates of the drag
+ * event. The {@link DragItemContainer} houses the draggable views that this {@link
+ * DragDropController} controls.
+ */
+ public interface DragItemContainer {
+
+ PhoneFavoriteSquareTileView getViewForLocation(int x, int y);
+ }
+}
diff --git a/java/com/android/dialer/app/list/ListsFragment.java b/java/com/android/dialer/app/list/ListsFragment.java
new file mode 100644
index 0000000..725ad30
--- /dev/null
+++ b/java/com/android/dialer/app/list/ListsFragment.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Trace;
+import android.preference.PreferenceManager;
+import android.provider.VoicemailContract;
+import android.support.annotation.Nullable;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.list.ViewPagerTabs;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogFragment;
+import com.android.dialer.app.calllog.CallLogNotificationsHelper;
+import com.android.dialer.app.calllog.VisualVoicemailCallLogFragment;
+import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler;
+import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source;
+import com.android.dialer.app.widget.ActionBarController;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.CallLogQueryHandler;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.util.ViewUtil;
+import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker;
+import com.android.dialer.voicemailstatus.VoicemailStatusHelper;
+import com.android.dialer.voicemailstatus.VoicemailStatusHelperImpl;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fragment that is used as the main screen of the Dialer.
+ *
+ * <p>Contains a ViewPager that contains various contact lists like the Speed Dial list and the All
+ * Contacts list. This will also eventually contain the logic that allows sliding the ViewPager
+ * containing the lists up above the search bar and pin it against the top of the screen.
+ */
+public class ListsFragment extends Fragment
+ implements ViewPager.OnPageChangeListener, CallLogQueryHandler.Listener {
+
+ /** Every fragment in the list show implement this interface. */
+ public interface ListsPage {
+
+ /**
+ * Called when the page is resumed, including selecting the page or activity resume. Note: This
+ * is called before the page fragment is attached to a activity.
+ *
+ * @param activity the activity hosting the ListFragment
+ */
+ void onPageResume(@Nullable Activity activity);
+
+ /**
+ * Called when the page is paused, including selecting another page or activity pause. Note:
+ * This is called after the page fragment is detached from a activity.
+ *
+ * @param activity the activity hosting the ListFragment
+ */
+ void onPagePause(@Nullable Activity activity);
+ }
+
+ public static final int TAB_INDEX_SPEED_DIAL = 0;
+ public static final int TAB_INDEX_HISTORY = 1;
+ public static final int TAB_INDEX_ALL_CONTACTS = 2;
+ public static final int TAB_INDEX_VOICEMAIL = 3;
+ public static final int TAB_COUNT_DEFAULT = 3;
+ public static final int TAB_COUNT_WITH_VOICEMAIL = 4;
+ private static final String TAG = "ListsFragment";
+ private ActionBar mActionBar;
+ private ViewPager mViewPager;
+ private ViewPagerTabs mViewPagerTabs;
+ private ViewPagerAdapter mViewPagerAdapter;
+ private RemoveView mRemoveView;
+ private View mRemoveViewContent;
+ private SpeedDialFragment mSpeedDialFragment;
+ private CallLogFragment mHistoryFragment;
+ private AllContactsFragment mAllContactsFragment;
+ private CallLogFragment mVoicemailFragment;
+ private ListsPage mCurrentPage;
+ private SharedPreferences mPrefs;
+ private boolean mHasActiveVoicemailProvider;
+ private boolean mHasFetchedVoicemailStatus;
+ private boolean mShowVoicemailTabAfterVoicemailStatusIsFetched;
+ private VoicemailStatusHelper mVoicemailStatusHelper;
+ private ArrayList<OnPageChangeListener> mOnPageChangeListeners =
+ new ArrayList<OnPageChangeListener>();
+ private String[] mTabTitles;
+ private int[] mTabIcons;
+ /** The position of the currently selected tab. */
+ private int mTabIndex = TAB_INDEX_SPEED_DIAL;
+
+ private CallLogQueryHandler mCallLogQueryHandler;
+
+ private final ContentObserver mVoicemailStatusObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ mCallLogQueryHandler.fetchVoicemailStatus();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ LogUtil.d("ListsFragment.onCreate", null);
+ Trace.beginSection(TAG + " onCreate");
+ super.onCreate(savedInstanceState);
+
+ mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+ mHasFetchedVoicemailStatus = false;
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ mHasActiveVoicemailProvider =
+ mPrefs.getBoolean(
+ VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, false);
+
+ Trace.endSection();
+ }
+
+ @Override
+ public void onResume() {
+ LogUtil.d("ListsFragment.onResume", null);
+ Trace.beginSection(TAG + " onResume");
+ super.onResume();
+
+ mActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ if (getUserVisibleHint()) {
+ sendScreenViewForCurrentPosition();
+ }
+
+ // Fetch voicemail status to determine if we should show the voicemail tab.
+ mCallLogQueryHandler =
+ new CallLogQueryHandler(getActivity(), getActivity().getContentResolver(), this);
+ mCallLogQueryHandler.fetchVoicemailStatus();
+ mCallLogQueryHandler.fetchMissedCallsUnreadCount();
+ Trace.endSection();
+ mCurrentPage = getListsPage(mViewPager.getCurrentItem());
+ if (mCurrentPage != null) {
+ mCurrentPage.onPageResume(getActivity());
+ }
+ }
+
+ @Override
+ public void onPause() {
+ LogUtil.d("ListsFragment.onPause", null);
+ if (mCurrentPage != null) {
+ mCurrentPage.onPagePause(getActivity());
+ }
+ super.onPause();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mViewPager.removeOnPageChangeListener(this);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ LogUtil.d("ListsFragment.onCreateView", null);
+ Trace.beginSection(TAG + " onCreateView");
+ Trace.beginSection(TAG + " inflate view");
+ final View parentView = inflater.inflate(R.layout.lists_fragment, container, false);
+ Trace.endSection();
+ Trace.beginSection(TAG + " setup views");
+ mViewPager = (ViewPager) parentView.findViewById(R.id.lists_pager);
+ mViewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
+ mViewPager.setAdapter(mViewPagerAdapter);
+ mViewPager.setOffscreenPageLimit(TAB_COUNT_WITH_VOICEMAIL - 1);
+ mViewPager.addOnPageChangeListener(this);
+ showTab(TAB_INDEX_SPEED_DIAL);
+
+ mTabTitles = new String[TAB_COUNT_WITH_VOICEMAIL];
+ mTabTitles[TAB_INDEX_SPEED_DIAL] = getResources().getString(R.string.tab_speed_dial);
+ mTabTitles[TAB_INDEX_HISTORY] = getResources().getString(R.string.tab_history);
+ mTabTitles[TAB_INDEX_ALL_CONTACTS] = getResources().getString(R.string.tab_all_contacts);
+ mTabTitles[TAB_INDEX_VOICEMAIL] = getResources().getString(R.string.tab_voicemail);
+
+ mTabIcons = new int[TAB_COUNT_WITH_VOICEMAIL];
+ mTabIcons[TAB_INDEX_SPEED_DIAL] = R.drawable.ic_grade_24dp;
+ mTabIcons[TAB_INDEX_HISTORY] = R.drawable.ic_schedule_24dp;
+ mTabIcons[TAB_INDEX_ALL_CONTACTS] = R.drawable.ic_people_24dp;
+ mTabIcons[TAB_INDEX_VOICEMAIL] = R.drawable.ic_voicemail_24dp;
+
+ mViewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header);
+ mViewPagerTabs.configureTabIcons(mTabIcons);
+ mViewPagerTabs.setViewPager(mViewPager);
+ addOnPageChangeListener(mViewPagerTabs);
+
+ mRemoveView = (RemoveView) parentView.findViewById(R.id.remove_view);
+ mRemoveViewContent = parentView.findViewById(R.id.remove_view_content);
+
+ getActivity()
+ .getContentResolver()
+ .registerContentObserver(
+ VoicemailContract.Status.CONTENT_URI, true, mVoicemailStatusObserver);
+
+ Trace.endSection();
+ Trace.endSection();
+ return parentView;
+ }
+
+ @Override
+ public void onDestroy() {
+ getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
+ super.onDestroy();
+ }
+
+ public void addOnPageChangeListener(OnPageChangeListener onPageChangeListener) {
+ if (!mOnPageChangeListeners.contains(onPageChangeListener)) {
+ mOnPageChangeListeners.add(onPageChangeListener);
+ }
+ }
+
+ /**
+ * Shows the tab with the specified index. If the voicemail tab index is specified, but the
+ * voicemail status hasn't been fetched, it will try to show the tab after the voicemail status
+ * has been fetched.
+ */
+ public void showTab(int index) {
+ if (index == TAB_INDEX_VOICEMAIL) {
+ if (mHasActiveVoicemailProvider) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.VVM_TAB_VISIBLE);
+ mViewPager.setCurrentItem(getRtlPosition(TAB_INDEX_VOICEMAIL));
+ } else if (!mHasFetchedVoicemailStatus) {
+ // Try to show the voicemail tab after the voicemail status returns.
+ mShowVoicemailTabAfterVoicemailStatusIsFetched = true;
+ }
+ } else if (index < getTabCount()) {
+ mViewPager.setCurrentItem(getRtlPosition(index));
+ }
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ mTabIndex = getRtlPosition(position);
+
+ final int count = mOnPageChangeListeners.size();
+ for (int i = 0; i < count; i++) {
+ mOnPageChangeListeners.get(i).onPageScrolled(position, positionOffset, positionOffsetPixels);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ LogUtil.i("ListsFragment.onPageSelected", "position: %d", position);
+ mTabIndex = getRtlPosition(position);
+
+ // Show the tab which has been selected instead.
+ mShowVoicemailTabAfterVoicemailStatusIsFetched = false;
+
+ final int count = mOnPageChangeListeners.size();
+ for (int i = 0; i < count; i++) {
+ mOnPageChangeListeners.get(i).onPageSelected(position);
+ }
+ sendScreenViewForCurrentPosition();
+
+ if (mCurrentPage != null) {
+ mCurrentPage.onPagePause(getActivity());
+ }
+ mCurrentPage = getListsPage(position);
+ if (mCurrentPage != null) {
+ mCurrentPage.onPageResume(getActivity());
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ final int count = mOnPageChangeListeners.size();
+ for (int i = 0; i < count; i++) {
+ mOnPageChangeListeners.get(i).onPageScrollStateChanged(state);
+ }
+ }
+
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ mHasFetchedVoicemailStatus = true;
+
+ if (getActivity() == null || getActivity().isFinishing()) {
+ return;
+ }
+
+ VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus(
+ getContext(), statusCursor, Source.Activity);
+
+ // Update mHasActiveVoicemailProvider, which controls the number of tabs displayed.
+ boolean hasActiveVoicemailProvider =
+ mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0;
+ if (hasActiveVoicemailProvider != mHasActiveVoicemailProvider) {
+ mHasActiveVoicemailProvider = hasActiveVoicemailProvider;
+ mViewPagerAdapter.notifyDataSetChanged();
+
+ if (hasActiveVoicemailProvider) {
+ mViewPagerTabs.updateTab(TAB_INDEX_VOICEMAIL);
+ } else {
+ mViewPagerTabs.removeTab(TAB_INDEX_VOICEMAIL);
+ removeVoicemailFragment();
+ }
+
+ mPrefs
+ .edit()
+ .putBoolean(
+ VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER,
+ hasActiveVoicemailProvider)
+ .commit();
+ }
+
+ if (hasActiveVoicemailProvider) {
+ mCallLogQueryHandler.fetchVoicemailUnreadCount();
+ }
+
+ if (mHasActiveVoicemailProvider && mShowVoicemailTabAfterVoicemailStatusIsFetched) {
+ mShowVoicemailTabAfterVoicemailStatusIsFetched = false;
+ showTab(TAB_INDEX_VOICEMAIL);
+ }
+ }
+
+ @Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {
+ if (getActivity() == null || getActivity().isFinishing() || cursor == null) {
+ return;
+ }
+
+ int count = 0;
+ try {
+ count = cursor.getCount();
+ } finally {
+ cursor.close();
+ }
+
+ mViewPagerTabs.setUnreadCount(count, TAB_INDEX_VOICEMAIL);
+ mViewPagerTabs.updateTab(TAB_INDEX_VOICEMAIL);
+ }
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+ if (getActivity() == null || getActivity().isFinishing() || cursor == null) {
+ return;
+ }
+
+ int count = 0;
+ try {
+ count = cursor.getCount();
+ } finally {
+ cursor.close();
+ }
+
+ mViewPagerTabs.setUnreadCount(count, TAB_INDEX_HISTORY);
+ mViewPagerTabs.updateTab(TAB_INDEX_HISTORY);
+ }
+
+ @Override
+ public boolean onCallsFetched(Cursor statusCursor) {
+ // Return false; did not take ownership of cursor
+ return false;
+ }
+
+ public int getCurrentTabIndex() {
+ return mTabIndex;
+ }
+
+ /**
+ * External method to update unread count because the unread count changes when the user expands a
+ * voicemail in the call log or when the user expands an unread call in the call history tab.
+ */
+ public void updateTabUnreadCounts() {
+ if (mCallLogQueryHandler != null) {
+ mCallLogQueryHandler.fetchMissedCallsUnreadCount();
+ if (mHasActiveVoicemailProvider) {
+ mCallLogQueryHandler.fetchVoicemailUnreadCount();
+ }
+ }
+ }
+
+ /** External method to mark all missed calls as read. */
+ public void markMissedCallsAsReadAndRemoveNotifications() {
+ if (mCallLogQueryHandler != null) {
+ mCallLogQueryHandler.markMissedCallsAsRead();
+ CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
+ }
+ }
+
+ public void showRemoveView(boolean show) {
+ mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE);
+ mRemoveView.setAlpha(show ? 0 : 1);
+ mRemoveView.animate().alpha(show ? 1 : 0).start();
+ }
+
+ public boolean shouldShowActionBar() {
+ // TODO: Update this based on scroll state.
+ return mActionBar != null;
+ }
+
+ public SpeedDialFragment getSpeedDialFragment() {
+ return mSpeedDialFragment;
+ }
+
+ public RemoveView getRemoveView() {
+ return mRemoveView;
+ }
+
+ public int getTabCount() {
+ return mViewPagerAdapter.getCount();
+ }
+
+ private int getRtlPosition(int position) {
+ if (ViewUtil.isRtl()) {
+ return mViewPagerAdapter.getCount() - 1 - position;
+ }
+ return position;
+ }
+
+ public void sendScreenViewForCurrentPosition() {
+ if (!isResumed()) {
+ return;
+ }
+
+ int screenType;
+ switch (getCurrentTabIndex()) {
+ case TAB_INDEX_SPEED_DIAL:
+ screenType = ScreenEvent.Type.SPEED_DIAL;
+ break;
+ case TAB_INDEX_HISTORY:
+ screenType = ScreenEvent.Type.CALL_LOG;
+ break;
+ case TAB_INDEX_ALL_CONTACTS:
+ screenType = ScreenEvent.Type.ALL_CONTACTS;
+ break;
+ case TAB_INDEX_VOICEMAIL:
+ screenType = ScreenEvent.Type.VOICEMAIL_LOG;
+ break;
+ default:
+ return;
+ }
+ Logger.get(getActivity()).logScreenView(screenType, getActivity());
+ }
+
+ private void removeVoicemailFragment() {
+ if (mVoicemailFragment != null) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .remove(mVoicemailFragment)
+ .commitAllowingStateLoss();
+ mVoicemailFragment = null;
+ }
+ }
+
+ private ListsPage getListsPage(int position) {
+ switch (getRtlPosition(position)) {
+ case TAB_INDEX_SPEED_DIAL:
+ return mSpeedDialFragment;
+ case TAB_INDEX_HISTORY:
+ return mHistoryFragment;
+ case TAB_INDEX_ALL_CONTACTS:
+ return mAllContactsFragment;
+ case TAB_INDEX_VOICEMAIL:
+ return mVoicemailFragment;
+ }
+ throw new IllegalStateException("No fragment at position " + position);
+ }
+
+ public interface HostInterface {
+
+ ActionBarController getActionBarController();
+ }
+
+ public class ViewPagerAdapter extends FragmentPagerAdapter {
+
+ private final List<Fragment> mFragments = new ArrayList<>();
+
+ public ViewPagerAdapter(FragmentManager fm) {
+ super(fm);
+ for (int i = 0; i < TAB_COUNT_WITH_VOICEMAIL; i++) {
+ mFragments.add(null);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getRtlPosition(position);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ LogUtil.d("ViewPagerAdapter.getItem", "position: %d", position);
+ switch (getRtlPosition(position)) {
+ case TAB_INDEX_SPEED_DIAL:
+ if (mSpeedDialFragment == null) {
+ mSpeedDialFragment = new SpeedDialFragment();
+ }
+ return mSpeedDialFragment;
+ case TAB_INDEX_HISTORY:
+ if (mHistoryFragment == null) {
+ mHistoryFragment = new CallLogFragment();
+ }
+ return mHistoryFragment;
+ case TAB_INDEX_ALL_CONTACTS:
+ if (mAllContactsFragment == null) {
+ mAllContactsFragment = new AllContactsFragment();
+ }
+ return mAllContactsFragment;
+ case TAB_INDEX_VOICEMAIL:
+ if (mVoicemailFragment == null) {
+ mVoicemailFragment = new VisualVoicemailCallLogFragment();
+ LogUtil.v(
+ "ViewPagerAdapter.getItem",
+ "new VisualVoicemailCallLogFragment: %s",
+ mVoicemailFragment);
+ }
+ return mVoicemailFragment;
+ }
+ throw new IllegalStateException("No fragment at position " + position);
+ }
+
+ @Override
+ public Fragment instantiateItem(ViewGroup container, int position) {
+ LogUtil.d("ViewPagerAdapter.instantiateItem", "position: %d", position);
+ // On rotation the FragmentManager handles rotation. Therefore getItem() isn't called.
+ // Copy the fragments that the FragmentManager finds so that we can store them in
+ // instance variables for later.
+ final Fragment fragment = (Fragment) super.instantiateItem(container, position);
+ if (fragment instanceof SpeedDialFragment) {
+ mSpeedDialFragment = (SpeedDialFragment) fragment;
+ } else if (fragment instanceof CallLogFragment && position == TAB_INDEX_HISTORY) {
+ mHistoryFragment = (CallLogFragment) fragment;
+ } else if (fragment instanceof AllContactsFragment) {
+ mAllContactsFragment = (AllContactsFragment) fragment;
+ } else if (fragment instanceof CallLogFragment && position == TAB_INDEX_VOICEMAIL) {
+ mVoicemailFragment = (CallLogFragment) fragment;
+ LogUtil.v("ViewPagerAdapter.instantiateItem", mVoicemailFragment.toString());
+ }
+ mFragments.set(position, fragment);
+ return fragment;
+ }
+
+ /**
+ * When {@link android.support.v4.view.PagerAdapter#notifyDataSetChanged} is called, this method
+ * is called on all pages to determine whether they need to be recreated. When the voicemail tab
+ * is removed, the view needs to be recreated by returning POSITION_NONE. If
+ * notifyDataSetChanged is called for some other reason, the voicemail tab is recreated only if
+ * it is active. All other tabs do not need to be recreated and POSITION_UNCHANGED is returned.
+ */
+ @Override
+ public int getItemPosition(Object object) {
+ return !mHasActiveVoicemailProvider && mFragments.indexOf(object) == TAB_INDEX_VOICEMAIL
+ ? POSITION_NONE
+ : POSITION_UNCHANGED;
+ }
+
+ @Override
+ public int getCount() {
+ return mHasActiveVoicemailProvider ? TAB_COUNT_WITH_VOICEMAIL : TAB_COUNT_DEFAULT;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mTabTitles[position];
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/list/OnDragDropListener.java b/java/com/android/dialer/app/list/OnDragDropListener.java
new file mode 100644
index 0000000..b71c7fe
--- /dev/null
+++ b/java/com/android/dialer/app/list/OnDragDropListener.java
@@ -0,0 +1,58 @@
+/*
+ * 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.app.list;
+
+/**
+ * Classes that want to receive callbacks in response to drag events should implement this
+ * interface.
+ */
+public interface OnDragDropListener {
+
+ /**
+ * Called when a drag is started.
+ *
+ * @param x X-coordinate of the drag event
+ * @param y Y-coordinate of the drag event
+ * @param view The contact tile which the drag was started on
+ */
+ void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view);
+
+ /**
+ * Called when a drag is in progress and the user moves the dragged contact to a location.
+ *
+ * @param x X-coordinate of the drag event
+ * @param y Y-coordinate of the drag event
+ * @param view Contact tile in the ListView which is currently being displaced by the dragged
+ * contact
+ */
+ void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view);
+
+ /**
+ * Called when a drag is completed (whether by dropping it somewhere or simply by dragging the
+ * contact off the screen)
+ *
+ * @param x X-coordinate of the drag event
+ * @param y Y-coordinate of the drag event
+ */
+ void onDragFinished(int x, int y);
+
+ /**
+ * Called when a contact has been dropped on the remove view, indicating that the user wants to
+ * remove this contact.
+ */
+ void onDroppedOnRemove();
+}
diff --git a/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java b/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java
new file mode 100644
index 0000000..a76f3b5
--- /dev/null
+++ b/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to 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.app.list;
+
+/*
+ * Interface to provide callback to activity when a child fragment is scrolled
+ */
+public interface OnListFragmentScrolledListener {
+
+ void onListFragmentScrollStateChange(int scrollState);
+
+ void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount);
+}
diff --git a/java/com/android/dialer/app/list/PhoneFavoriteListView.java b/java/com/android/dialer/app/list/PhoneFavoriteListView.java
new file mode 100644
index 0000000..9516f06
--- /dev/null
+++ b/java/com/android/dialer/app/list/PhoneFavoriteListView.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to 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.app.list;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.GridView;
+import android.widget.ImageView;
+import com.android.dialer.app.R;
+import com.android.dialer.app.list.DragDropController.DragItemContainer;
+
+/** Viewgroup that presents the user's speed dial contacts in a grid. */
+public class PhoneFavoriteListView extends GridView
+ implements OnDragDropListener, DragItemContainer {
+
+ public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName();
+ final int[] mLocationOnScreen = new int[2];
+ private final long SCROLL_HANDLER_DELAY_MILLIS = 5;
+ private final int DRAG_SCROLL_PX_UNIT = 25;
+ private final float DRAG_SHADOW_ALPHA = 0.7f;
+ /**
+ * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be offseted to the top / bottom by
+ * {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels.
+ */
+ private final float BOUND_GAP_RATIO = 0.2f;
+
+ private float mTouchSlop;
+ private int mTopScrollBound;
+ private int mBottomScrollBound;
+ private int mLastDragY;
+ private Handler mScrollHandler;
+ private final Runnable mDragScroller =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mLastDragY <= mTopScrollBound) {
+ smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
+ } else if (mLastDragY >= mBottomScrollBound) {
+ smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
+ }
+ mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS);
+ }
+ };
+ private boolean mIsDragScrollerRunning = false;
+ private int mTouchDownForDragStartX;
+ private int mTouchDownForDragStartY;
+ private Bitmap mDragShadowBitmap;
+ private ImageView mDragShadowOverlay;
+ private final AnimatorListenerAdapter mDragShadowOverAnimatorListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mDragShadowBitmap != null) {
+ mDragShadowBitmap.recycle();
+ mDragShadowBitmap = null;
+ }
+ mDragShadowOverlay.setVisibility(GONE);
+ mDragShadowOverlay.setImageBitmap(null);
+ }
+ };
+ private View mDragShadowParent;
+ private int mAnimationDuration;
+ // X and Y offsets inside the item from where the user grabbed to the
+ // child's left coordinate. This is used to aid in the drawing of the drag shadow.
+ private int mTouchOffsetToChildLeft;
+ private int mTouchOffsetToChildTop;
+ private int mDragShadowLeft;
+ private int mDragShadowTop;
+ private DragDropController mDragDropController = new DragDropController(this);
+
+ public PhoneFavoriteListView(Context context) {
+ this(context, null);
+ }
+
+ public PhoneFavoriteListView(Context context, AttributeSet attrs) {
+ this(context, attrs, -1);
+ }
+
+ public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration);
+ mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
+ mDragDropController.addOnDragDropListener(this);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
+ }
+
+ /**
+ * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should be
+ * cleaned up and removed once drag to remove becomes the only way to remove contacts.
+ */
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mTouchDownForDragStartX = (int) ev.getX();
+ mTouchDownForDragStartY = (int) ev.getY();
+ }
+
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onDragEvent(DragEvent event) {
+ final int action = event.getAction();
+ final int eX = (int) event.getX();
+ final int eY = (int) event.getY();
+ switch (action) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ {
+ if (!PhoneFavoriteTileView.DRAG_PHONE_FAVORITE_TILE.equals(event.getLocalState())) {
+ // Ignore any drag events that were not propagated by long pressing
+ // on a {@link PhoneFavoriteTileView}
+ return false;
+ }
+ if (!mDragDropController.handleDragStarted(this, eX, eY)) {
+ return false;
+ }
+ break;
+ }
+ case DragEvent.ACTION_DRAG_LOCATION:
+ mLastDragY = eY;
+ mDragDropController.handleDragHovered(this, eX, eY);
+ // Kick off {@link #mScrollHandler} if it's not started yet.
+ if (!mIsDragScrollerRunning
+ &&
+ // And if the distance traveled while dragging exceeds the touch slop
+ (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) {
+ mIsDragScrollerRunning = true;
+ ensureScrollHandler();
+ mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS);
+ }
+ break;
+ case DragEvent.ACTION_DRAG_ENTERED:
+ final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO);
+ mTopScrollBound = (getTop() + boundGap);
+ mBottomScrollBound = (getBottom() - boundGap);
+ break;
+ case DragEvent.ACTION_DRAG_EXITED:
+ case DragEvent.ACTION_DRAG_ENDED:
+ case DragEvent.ACTION_DROP:
+ ensureScrollHandler();
+ mScrollHandler.removeCallbacks(mDragScroller);
+ mIsDragScrollerRunning = false;
+ // Either a successful drop or it's ended with out drop.
+ if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
+ mDragDropController.handleDragFinished(eX, eY, false);
+ }
+ break;
+ default:
+ break;
+ }
+ // This ListView will consume the drag events on behalf of its children.
+ return true;
+ }
+
+ public void setDragShadowOverlay(ImageView overlay) {
+ mDragShadowOverlay = overlay;
+ mDragShadowParent = (View) mDragShadowOverlay.getParent();
+ }
+
+ /** Find the view under the pointer. */
+ private View getViewAtPosition(int x, int y) {
+ final int count = getChildCount();
+ View child;
+ for (int childIdx = 0; childIdx < count; childIdx++) {
+ child = getChildAt(childIdx);
+ if (y >= child.getTop()
+ && y <= child.getBottom()
+ && x >= child.getLeft()
+ && x <= child.getRight()) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private void ensureScrollHandler() {
+ if (mScrollHandler == null) {
+ mScrollHandler = getHandler();
+ }
+ }
+
+ public DragDropController getDragDropController() {
+ return mDragDropController;
+ }
+
+ @Override
+ public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) {
+ if (mDragShadowOverlay == null) {
+ return;
+ }
+
+ mDragShadowOverlay.clearAnimation();
+ mDragShadowBitmap = createDraggedChildBitmap(tileView);
+ if (mDragShadowBitmap == null) {
+ return;
+ }
+
+ tileView.getLocationOnScreen(mLocationOnScreen);
+ mDragShadowLeft = mLocationOnScreen[0];
+ mDragShadowTop = mLocationOnScreen[1];
+
+ // x and y are the coordinates of the on-screen touch event. Using these
+ // and the on-screen location of the tileView, calculate the difference between
+ // the position of the user's finger and the position of the tileView. These will
+ // be used to offset the location of the drag shadow so that it appears that the
+ // tileView is positioned directly under the user's finger.
+ mTouchOffsetToChildLeft = x - mDragShadowLeft;
+ mTouchOffsetToChildTop = y - mDragShadowTop;
+
+ mDragShadowParent.getLocationOnScreen(mLocationOnScreen);
+ mDragShadowLeft -= mLocationOnScreen[0];
+ mDragShadowTop -= mLocationOnScreen[1];
+
+ mDragShadowOverlay.setImageBitmap(mDragShadowBitmap);
+ mDragShadowOverlay.setVisibility(VISIBLE);
+ mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA);
+
+ mDragShadowOverlay.setX(mDragShadowLeft);
+ mDragShadowOverlay.setY(mDragShadowTop);
+ }
+
+ @Override
+ public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) {
+ // Update the drag shadow location.
+ mDragShadowParent.getLocationOnScreen(mLocationOnScreen);
+ mDragShadowLeft = x - mTouchOffsetToChildLeft - mLocationOnScreen[0];
+ mDragShadowTop = y - mTouchOffsetToChildTop - mLocationOnScreen[1];
+ // Draw the drag shadow at its last known location if the drag shadow exists.
+ if (mDragShadowOverlay != null) {
+ mDragShadowOverlay.setX(mDragShadowLeft);
+ mDragShadowOverlay.setY(mDragShadowTop);
+ }
+ }
+
+ @Override
+ public void onDragFinished(int x, int y) {
+ if (mDragShadowOverlay != null) {
+ mDragShadowOverlay.clearAnimation();
+ mDragShadowOverlay
+ .animate()
+ .alpha(0.0f)
+ .setDuration(mAnimationDuration)
+ .setListener(mDragShadowOverAnimatorListener)
+ .start();
+ }
+ }
+
+ @Override
+ public void onDroppedOnRemove() {}
+
+ private Bitmap createDraggedChildBitmap(View view) {
+ view.setDrawingCacheEnabled(true);
+ final Bitmap cache = view.getDrawingCache();
+
+ Bitmap bitmap = null;
+ if (cache != null) {
+ try {
+ bitmap = cache.copy(Bitmap.Config.ARGB_8888, false);
+ } catch (final OutOfMemoryError e) {
+ Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e);
+ bitmap = null;
+ }
+ }
+
+ view.destroyDrawingCache();
+ view.setDrawingCacheEnabled(false);
+
+ return bitmap;
+ }
+
+ @Override
+ public PhoneFavoriteSquareTileView getViewForLocation(int x, int y) {
+ getLocationOnScreen(mLocationOnScreen);
+ // Calculate the X and Y coordinates of the drag event relative to the view
+ final int viewX = x - mLocationOnScreen[0];
+ final int viewY = y - mLocationOnScreen[1];
+ final View child = getViewAtPosition(viewX, viewY);
+
+ if (!(child instanceof PhoneFavoriteSquareTileView)) {
+ return null;
+ }
+
+ return (PhoneFavoriteSquareTileView) child;
+ }
+}
diff --git a/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java
new file mode 100644
index 0000000..5a18d03
--- /dev/null
+++ b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java
@@ -0,0 +1,119 @@
+/*
+
+* Copyright (C) 2011 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.app.list;
+
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.QuickContact;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import com.android.contacts.common.list.ContactEntry;
+import com.android.dialer.app.R;
+import com.android.dialer.compat.CompatUtils;
+
+/** Displays the contact's picture overlaid with their name and number type in a tile. */
+public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView {
+
+ private static final String TAG = PhoneFavoriteSquareTileView.class.getSimpleName();
+
+ private final float mHeightToWidthRatio;
+
+ private ImageButton mSecondaryButton;
+
+ private ContactEntry mContactEntry;
+
+ public PhoneFavoriteSquareTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mHeightToWidthRatio =
+ getResources().getFraction(R.dimen.contact_tile_height_to_width_ratio, 1, 1);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ final TextView nameView = (TextView) findViewById(R.id.contact_tile_name);
+ nameView.setElegantTextHeight(false);
+ final TextView phoneTypeView = (TextView) findViewById(R.id.contact_tile_phone_type);
+ phoneTypeView.setElegantTextHeight(false);
+ mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button);
+ }
+
+ @Override
+ protected int getApproximateImageSize() {
+ // The picture is the full size of the tile (minus some padding, but we can be generous)
+ return getWidth();
+ }
+
+ private void launchQuickContact() {
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ QuickContact.showQuickContact(
+ getContext(),
+ PhoneFavoriteSquareTileView.this,
+ getLookupUri(),
+ null,
+ Phone.CONTENT_ITEM_TYPE);
+ } else {
+ QuickContact.showQuickContact(
+ getContext(),
+ PhoneFavoriteSquareTileView.this,
+ getLookupUri(),
+ QuickContact.MODE_LARGE,
+ null);
+ }
+ }
+
+ @Override
+ public void loadFromContact(ContactEntry entry) {
+ super.loadFromContact(entry);
+ if (entry != null) {
+ mSecondaryButton.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ launchQuickContact();
+ }
+ });
+ }
+ mContactEntry = entry;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int height = (int) (mHeightToWidthRatio * width);
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i)
+ .measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected String getNameForView(ContactEntry contactEntry) {
+ return contactEntry.getPreferredDisplayName();
+ }
+
+ public ContactEntry getContactEntry() {
+ return mContactEntry;
+ }
+}
diff --git a/java/com/android/dialer/app/list/PhoneFavoriteTileView.java b/java/com/android/dialer/app/list/PhoneFavoriteTileView.java
new file mode 100644
index 0000000..db89cf3
--- /dev/null
+++ b/java/com/android/dialer/app/list/PhoneFavoriteTileView.java
@@ -0,0 +1,155 @@
+/*
+
+* Copyright (C) 2011 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.app.list;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.list.ContactEntry;
+import com.android.contacts.common.list.ContactTileView;
+import com.android.dialer.app.R;
+
+/**
+ * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in
+ * Dialtacts for frequently called contacts. Slightly different behavior from superclass when you
+ * tap it, you want to call the frequently-called number for the contact, even if that is not the
+ * default number for that contact. This abstract class is the super class to both the row and tile
+ * view.
+ */
+public abstract class PhoneFavoriteTileView extends ContactTileView {
+
+ // Constant to pass to the drag event so that the drag action only happens when a phone favorite
+ // tile is long pressed.
+ static final String DRAG_PHONE_FAVORITE_TILE = "PHONE_FAVORITE_TILE";
+ private static final String TAG = PhoneFavoriteTileView.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ // These parameters instruct the photo manager to display the default image/letter at 70% of
+ // its normal size, and vertically offset upwards 12% towards the top of the letter tile, to
+ // make room for the contact name and number label at the bottom of the image.
+ private static final float DEFAULT_IMAGE_LETTER_OFFSET = -0.12f;
+ private static final float DEFAULT_IMAGE_LETTER_SCALE = 0.70f;
+ // Dummy clip data object that is attached to drag shadows so that text views
+ // don't crash with an NPE if the drag shadow is released in their bounds
+ private static final ClipData EMPTY_CLIP_DATA = ClipData.newPlainText("", "");
+ /** View that contains the transparent shadow that is overlaid on top of the contact image. */
+ private View mShadowOverlay;
+ /** Users' most frequent phone number. */
+ private String mPhoneNumberString;
+
+ public PhoneFavoriteTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mShadowOverlay = findViewById(R.id.shadow_overlay);
+
+ setOnLongClickListener(
+ new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ final PhoneFavoriteTileView view = (PhoneFavoriteTileView) v;
+ // NOTE The drag shadow is handled in the ListView.
+ view.startDrag(
+ EMPTY_CLIP_DATA, new View.DragShadowBuilder(), DRAG_PHONE_FAVORITE_TILE, 0);
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public void loadFromContact(ContactEntry entry) {
+ super.loadFromContact(entry);
+ // Set phone number to null in case we're reusing the view.
+ mPhoneNumberString = null;
+ if (entry != null) {
+ // Grab the phone-number to call directly. See {@link onClick()}.
+ mPhoneNumberString = entry.phoneNumber;
+
+ // If this is a blank entry, don't show anything.
+ // TODO krelease: Just hide the view for now. For this to truly look like an empty row
+ // the entire ContactTileRow needs to be hidden.
+ if (entry == ContactEntry.BLANK_ENTRY) {
+ setVisibility(View.INVISIBLE);
+ } else {
+ final ImageView starIcon = (ImageView) findViewById(R.id.contact_star_icon);
+ starIcon.setVisibility(entry.isFavorite ? View.VISIBLE : View.GONE);
+ setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ protected boolean isDarkTheme() {
+ return false;
+ }
+
+ @Override
+ protected OnClickListener createClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mListener == null) {
+ return;
+ }
+ if (TextUtils.isEmpty(mPhoneNumberString)) {
+ // Copy "superclass" implementation
+ mListener.onContactSelected(
+ getLookupUri(), MoreContactUtils.getTargetRectFromView(PhoneFavoriteTileView.this));
+ } else {
+ // When you tap a frequently-called contact, you want to
+ // call them at the number that you usually talk to them
+ // at (i.e. the one displayed in the UI), regardless of
+ // whether that's their default number.
+ mListener.onCallNumberDirectly(mPhoneNumberString);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) {
+ return new DefaultImageRequest(
+ displayName,
+ lookupKey,
+ ContactPhotoManager.TYPE_DEFAULT,
+ DEFAULT_IMAGE_LETTER_SCALE,
+ DEFAULT_IMAGE_LETTER_OFFSET,
+ false);
+ }
+
+ @Override
+ protected void configureViewForImage(boolean isDefaultImage) {
+ // Hide the shadow overlay if the image is a default image (i.e. colored letter tile)
+ if (mShadowOverlay != null) {
+ mShadowOverlay.setVisibility(isDefaultImage ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected boolean isContactPhotoCircular() {
+ // Unlike Contacts' tiles, the Dialer's favorites tiles are square.
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java b/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java
new file mode 100644
index 0000000..c692eca
--- /dev/null
+++ b/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java
@@ -0,0 +1,627 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PinnedPositions;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactTileLoaderFactory;
+import com.android.contacts.common.list.ContactEntry;
+import com.android.contacts.common.list.ContactTileView;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.dialer.app.R;
+import com.android.dialer.shortcuts.ShortcutRefresher;
+import com.google.common.collect.ComparisonChain;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.PriorityQueue;
+
+/** Also allows for a configurable number of columns as well as a maximum row of tiled contacts. */
+public class PhoneFavoritesTileAdapter extends BaseAdapter implements OnDragDropListener {
+
+ // Pinned positions start from 1, so there are a total of 20 maximum pinned contacts
+ private static final int PIN_LIMIT = 21;
+ private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ /**
+ * The soft limit on how many contact tiles to show. NOTE This soft limit would not restrict the
+ * number of starred contacts to show, rather 1. If the count of starred contacts is less than
+ * this limit, show 20 tiles total. 2. If the count of starred contacts is more than or equal to
+ * this limit, show all starred tiles and no frequents.
+ */
+ private static final int TILES_SOFT_LIMIT = 20;
+ /** Contact data stored in cache. This is used to populate the associated view. */
+ private ArrayList<ContactEntry> mContactEntries = null;
+
+ private int mNumFrequents;
+ private int mNumStarred;
+
+ private ContactTileView.Listener mListener;
+ private OnDataSetChangedForAnimationListener mDataSetChangedListener;
+ private Context mContext;
+ private Resources mResources;
+ private ContactsPreferences mContactsPreferences;
+ private final Comparator<ContactEntry> mContactEntryComparator =
+ new Comparator<ContactEntry>() {
+ @Override
+ public int compare(ContactEntry lhs, ContactEntry rhs) {
+ return ComparisonChain.start()
+ .compare(lhs.pinned, rhs.pinned)
+ .compare(getPreferredSortName(lhs), getPreferredSortName(rhs))
+ .result();
+ }
+
+ private String getPreferredSortName(ContactEntry contactEntry) {
+ if (mContactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY
+ || TextUtils.isEmpty(contactEntry.nameAlternative)) {
+ return contactEntry.namePrimary;
+ }
+ return contactEntry.nameAlternative;
+ }
+ };
+ /** Back up of the temporarily removed Contact during dragging. */
+ private ContactEntry mDraggedEntry = null;
+ /** Position of the temporarily removed contact in the cache. */
+ private int mDraggedEntryIndex = -1;
+ /** New position of the temporarily removed contact in the cache. */
+ private int mDropEntryIndex = -1;
+ /** New position of the temporarily entered contact in the cache. */
+ private int mDragEnteredEntryIndex = -1;
+
+ private boolean mAwaitingRemove = false;
+ private boolean mDelayCursorUpdates = false;
+ private ContactPhotoManager mPhotoManager;
+
+ /** Indicates whether a drag is in process. */
+ private boolean mInDragging = false;
+
+ public PhoneFavoritesTileAdapter(
+ Context context,
+ ContactTileView.Listener listener,
+ OnDataSetChangedForAnimationListener dataSetChangedListener) {
+ mDataSetChangedListener = dataSetChangedListener;
+ mListener = listener;
+ mContext = context;
+ mResources = context.getResources();
+ mContactsPreferences = new ContactsPreferences(mContext);
+ mNumFrequents = 0;
+ mContactEntries = new ArrayList<>();
+ }
+
+ void setPhotoLoader(ContactPhotoManager photoLoader) {
+ mPhotoManager = photoLoader;
+ }
+
+ /**
+ * Indicates whether a drag is in process.
+ *
+ * @param inDragging Boolean variable indicating whether there is a drag in process.
+ */
+ private void setInDragging(boolean inDragging) {
+ mDelayCursorUpdates = inDragging;
+ mInDragging = inDragging;
+ }
+
+ void refreshContactsPreferences() {
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY);
+ }
+
+ /**
+ * Gets the number of frequents from the passed in cursor.
+ *
+ * <p>This methods is needed so the GroupMemberTileAdapter can override this.
+ *
+ * @param cursor The cursor to get number of frequents from.
+ */
+ private void saveNumFrequentsFromCursor(Cursor cursor) {
+ mNumFrequents = cursor.getCount() - mNumStarred;
+ }
+
+ /**
+ * Creates {@link ContactTileView}s for each item in {@link Cursor}.
+ *
+ * <p>Else use {@link ContactTileLoaderFactory}
+ */
+ void setContactCursor(Cursor cursor) {
+ if (!mDelayCursorUpdates && cursor != null && !cursor.isClosed()) {
+ mNumStarred = getNumStarredContacts(cursor);
+ if (mAwaitingRemove) {
+ mDataSetChangedListener.cacheOffsetsForDatasetChange();
+ }
+
+ saveNumFrequentsFromCursor(cursor);
+ saveCursorToCache(cursor);
+ // cause a refresh of any views that rely on this data
+ notifyDataSetChanged();
+ // about to start redraw
+ mDataSetChangedListener.onDataSetChangedForAnimation();
+ }
+ }
+
+ /**
+ * Saves the cursor data to the cache, to speed up UI changes.
+ *
+ * @param cursor Returned cursor from {@link ContactTileLoaderFactory} with data to populate the
+ * view.
+ */
+ private void saveCursorToCache(Cursor cursor) {
+ mContactEntries.clear();
+
+ if (cursor == null) {
+ return;
+ }
+
+ final LongSparseArray<Object> duplicates = new LongSparseArray<>(cursor.getCount());
+
+ // Track the length of {@link #mContactEntries} and compare to {@link #TILES_SOFT_LIMIT}.
+ int counter = 0;
+
+ // The cursor should not be closed since this is invoked from a CursorLoader.
+ if (cursor.moveToFirst()) {
+ int starredColumn = cursor.getColumnIndexOrThrow(Contacts.STARRED);
+ int contactIdColumn = cursor.getColumnIndexOrThrow(Phone.CONTACT_ID);
+ int photoUriColumn = cursor.getColumnIndexOrThrow(Contacts.PHOTO_URI);
+ int lookupKeyColumn = cursor.getColumnIndexOrThrow(Contacts.LOOKUP_KEY);
+ int pinnedColumn = cursor.getColumnIndexOrThrow(Contacts.PINNED);
+ int nameColumn = cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_PRIMARY);
+ int nameAlternativeColumn = cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_ALTERNATIVE);
+ int isDefaultNumberColumn = cursor.getColumnIndexOrThrow(Phone.IS_SUPER_PRIMARY);
+ int phoneTypeColumn = cursor.getColumnIndexOrThrow(Phone.TYPE);
+ int phoneLabelColumn = cursor.getColumnIndexOrThrow(Phone.LABEL);
+ int phoneNumberColumn = cursor.getColumnIndexOrThrow(Phone.NUMBER);
+ do {
+ final int starred = cursor.getInt(starredColumn);
+ final long id;
+
+ // We display a maximum of TILES_SOFT_LIMIT contacts, or the total number of starred
+ // whichever is greater.
+ if (starred < 1 && counter >= TILES_SOFT_LIMIT) {
+ break;
+ } else {
+ id = cursor.getLong(contactIdColumn);
+ }
+
+ final ContactEntry existing = (ContactEntry) duplicates.get(id);
+ if (existing != null) {
+ // Check if the existing number is a default number. If not, clear the phone number
+ // and label fields so that the disambiguation dialog will show up.
+ if (!existing.isDefaultNumber) {
+ existing.phoneLabel = null;
+ existing.phoneNumber = null;
+ }
+ continue;
+ }
+
+ final String photoUri = cursor.getString(photoUriColumn);
+ final String lookupKey = cursor.getString(lookupKeyColumn);
+ final int pinned = cursor.getInt(pinnedColumn);
+ final String name = cursor.getString(nameColumn);
+ final String nameAlternative = cursor.getString(nameAlternativeColumn);
+ final boolean isStarred = cursor.getInt(starredColumn) > 0;
+ final boolean isDefaultNumber = cursor.getInt(isDefaultNumberColumn) > 0;
+
+ final ContactEntry contact = new ContactEntry();
+
+ contact.id = id;
+ contact.namePrimary =
+ (!TextUtils.isEmpty(name)) ? name : mResources.getString(R.string.missing_name);
+ contact.nameAlternative =
+ (!TextUtils.isEmpty(nameAlternative))
+ ? nameAlternative
+ : mResources.getString(R.string.missing_name);
+ contact.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
+ contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
+ contact.lookupKey = lookupKey;
+ contact.lookupUri =
+ ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
+ contact.isFavorite = isStarred;
+ contact.isDefaultNumber = isDefaultNumber;
+
+ // Set phone number and label
+ final int phoneNumberType = cursor.getInt(phoneTypeColumn);
+ final String phoneNumberCustomLabel = cursor.getString(phoneLabelColumn);
+ contact.phoneLabel =
+ (String) Phone.getTypeLabel(mResources, phoneNumberType, phoneNumberCustomLabel);
+ contact.phoneNumber = cursor.getString(phoneNumberColumn);
+
+ contact.pinned = pinned;
+ mContactEntries.add(contact);
+
+ duplicates.put(id, contact);
+
+ counter++;
+ } while (cursor.moveToNext());
+ }
+
+ mAwaitingRemove = false;
+
+ arrangeContactsByPinnedPosition(mContactEntries);
+
+ ShortcutRefresher.refresh(mContext, mContactEntries);
+ notifyDataSetChanged();
+ }
+
+ /** Iterates over the {@link Cursor} Returns position of the first NON Starred Contact */
+ private int getNumStarredContacts(Cursor cursor) {
+ if (cursor == null) {
+ return 0;
+ }
+
+ if (cursor.moveToFirst()) {
+ int starredColumn = cursor.getColumnIndex(Contacts.STARRED);
+ do {
+ if (cursor.getInt(starredColumn) == 0) {
+ return cursor.getPosition();
+ }
+ } while (cursor.moveToNext());
+ }
+ // There are not NON Starred contacts in cursor
+ // Set divider position to end
+ return cursor.getCount();
+ }
+
+ /** Returns the number of frequents that will be displayed in the list. */
+ int getNumFrequents() {
+ return mNumFrequents;
+ }
+
+ @Override
+ public int getCount() {
+ if (mContactEntries == null) {
+ return 0;
+ }
+
+ return mContactEntries.size();
+ }
+
+ /**
+ * Returns an ArrayList of the {@link ContactEntry}s that are to appear on the row for the given
+ * position.
+ */
+ @Override
+ public ContactEntry getItem(int position) {
+ return mContactEntries.get(position);
+ }
+
+ /**
+ * For the top row of tiled contacts, the item id is the position of the row of contacts. For
+ * frequent contacts, the item id is the maximum number of rows of tiled contacts + the actual
+ * contact id. Since contact ids are always greater than 0, this guarantees that all items within
+ * this adapter will always have unique ids.
+ */
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).id;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return getCount() > 0;
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ if (DEBUG) {
+ Log.v(TAG, "notifyDataSetChanged");
+ }
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (DEBUG) {
+ Log.v(TAG, "get view for " + String.valueOf(position));
+ }
+
+ PhoneFavoriteTileView tileView = null;
+
+ if (convertView instanceof PhoneFavoriteTileView) {
+ tileView = (PhoneFavoriteTileView) convertView;
+ }
+
+ if (tileView == null) {
+ tileView =
+ (PhoneFavoriteTileView) View.inflate(mContext, R.layout.phone_favorite_tile_view, null);
+ }
+ tileView.setPhotoManager(mPhotoManager);
+ tileView.setListener(mListener);
+ tileView.loadFromContact(getItem(position));
+ return tileView;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return ViewTypes.COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return ViewTypes.TILE;
+ }
+
+ /**
+ * Temporarily removes a contact from the list for UI refresh. Stores data for this contact in the
+ * back-up variable.
+ *
+ * @param index Position of the contact to be removed.
+ */
+ private void popContactEntry(int index) {
+ if (isIndexInBound(index)) {
+ mDraggedEntry = mContactEntries.get(index);
+ mDraggedEntryIndex = index;
+ mDragEnteredEntryIndex = index;
+ markDropArea(mDragEnteredEntryIndex);
+ }
+ }
+
+ /**
+ * @param itemIndex Position of the contact in {@link #mContactEntries}.
+ * @return True if the given index is valid for {@link #mContactEntries}.
+ */
+ boolean isIndexInBound(int itemIndex) {
+ return itemIndex >= 0 && itemIndex < mContactEntries.size();
+ }
+
+ /**
+ * Mark the tile as drop area by given the item index in {@link #mContactEntries}.
+ *
+ * @param itemIndex Position of the contact in {@link #mContactEntries}.
+ */
+ private void markDropArea(int itemIndex) {
+ if (mDraggedEntry != null
+ && isIndexInBound(mDragEnteredEntryIndex)
+ && isIndexInBound(itemIndex)) {
+ mDataSetChangedListener.cacheOffsetsForDatasetChange();
+ // Remove the old placeholder item and place the new placeholder item.
+ mContactEntries.remove(mDragEnteredEntryIndex);
+ mDragEnteredEntryIndex = itemIndex;
+ mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY);
+ ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id;
+ mDataSetChangedListener.onDataSetChangedForAnimation();
+ notifyDataSetChanged();
+ }
+ }
+
+ /** Drops the temporarily removed contact to the desired location in the list. */
+ private void handleDrop() {
+ boolean changed = false;
+ if (mDraggedEntry != null) {
+ if (isIndexInBound(mDragEnteredEntryIndex) && mDragEnteredEntryIndex != mDraggedEntryIndex) {
+ // Don't add the ContactEntry here (to prevent a double animation from occuring).
+ // When we receive a new cursor the list of contact entries will automatically be
+ // populated with the dragged ContactEntry at the correct spot.
+ mDropEntryIndex = mDragEnteredEntryIndex;
+ mContactEntries.set(mDropEntryIndex, mDraggedEntry);
+ mDataSetChangedListener.cacheOffsetsForDatasetChange();
+ changed = true;
+ } else if (isIndexInBound(mDraggedEntryIndex)) {
+ // If {@link #mDragEnteredEntryIndex} is invalid,
+ // falls back to the original position of the contact.
+ mContactEntries.remove(mDragEnteredEntryIndex);
+ mContactEntries.add(mDraggedEntryIndex, mDraggedEntry);
+ mDropEntryIndex = mDraggedEntryIndex;
+ notifyDataSetChanged();
+ }
+
+ if (changed && mDropEntryIndex < PIN_LIMIT) {
+ final ArrayList<ContentProviderOperation> operations =
+ getReflowedPinningOperations(mContactEntries, mDraggedEntryIndex, mDropEntryIndex);
+ if (!operations.isEmpty()) {
+ // update the database here with the new pinned positions
+ try {
+ mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Exception thrown when pinning contacts", e);
+ }
+ }
+ }
+ mDraggedEntry = null;
+ }
+ }
+
+ /**
+ * Used when a contact is removed from speeddial. This will both unstar and set pinned position of
+ * the contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites list.
+ */
+ private void unstarAndUnpinContact(Uri contactUri) {
+ final ContentValues values = new ContentValues(2);
+ values.put(Contacts.STARRED, false);
+ values.put(Contacts.PINNED, PinnedPositions.DEMOTED);
+ mContext.getContentResolver().update(contactUri, values, null, null);
+ }
+
+ /**
+ * Given a list of contacts that each have pinned positions, rearrange the list (destructive) such
+ * that all pinned contacts are in their defined pinned positions, and unpinned contacts take the
+ * spaces between those pinned contacts. Demoted contacts should not appear in the resulting list.
+ *
+ * <p>This method also updates the pinned positions of pinned contacts so that they are all unique
+ * positive integers within range from 0 to toArrange.size() - 1. This is because when the contact
+ * entries are read from the database, it is possible for them to have overlapping pin positions
+ * due to sync or modifications by third party apps.
+ */
+ @VisibleForTesting
+ private void arrangeContactsByPinnedPosition(ArrayList<ContactEntry> toArrange) {
+ final PriorityQueue<ContactEntry> pinnedQueue =
+ new PriorityQueue<>(PIN_LIMIT, mContactEntryComparator);
+
+ final List<ContactEntry> unpinnedContacts = new LinkedList<>();
+
+ final int length = toArrange.size();
+ for (int i = 0; i < length; i++) {
+ final ContactEntry contact = toArrange.get(i);
+ // Decide whether the contact is hidden(demoted), pinned, or unpinned
+ if (contact.pinned > PIN_LIMIT || contact.pinned == PinnedPositions.UNPINNED) {
+ unpinnedContacts.add(contact);
+ } else if (contact.pinned > PinnedPositions.DEMOTED) {
+ // Demoted or contacts with negative pinned positions are ignored.
+ // Pinned contacts go into a priority queue where they are ranked by pinned
+ // position. This is required because the contacts provider does not return
+ // contacts ordered by pinned position.
+ pinnedQueue.add(contact);
+ }
+ }
+
+ final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size());
+
+ toArrange.clear();
+ for (int i = 1; i < maxToPin + 1; i++) {
+ if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) {
+ final ContactEntry toPin = pinnedQueue.poll();
+ toPin.pinned = i;
+ toArrange.add(toPin);
+ } else if (!unpinnedContacts.isEmpty()) {
+ toArrange.add(unpinnedContacts.remove(0));
+ }
+ }
+
+ // If there are still contacts in pinnedContacts at this point, it means that the pinned
+ // positions of these pinned contacts exceed the actual number of contacts in the list.
+ // For example, the user had 10 frequents, starred and pinned one of them at the last spot,
+ // and then cleared frequents. Contacts in this situation should become unpinned.
+ while (!pinnedQueue.isEmpty()) {
+ final ContactEntry entry = pinnedQueue.poll();
+ entry.pinned = PinnedPositions.UNPINNED;
+ toArrange.add(entry);
+ }
+
+ // Any remaining unpinned contacts that weren't in the gaps between the pinned contacts
+ // now just get appended to the end of the list.
+ toArrange.addAll(unpinnedContacts);
+ }
+
+ /**
+ * Given an existing list of contact entries and a single entry that is to be pinned at a
+ * particular position, return a list of {@link ContentProviderOperation}s that contains new
+ * pinned positions for all contacts that are forced to be pinned at new positions, trying as much
+ * as possible to keep pinned contacts at their original location.
+ *
+ * <p>At this point in time the pinned position of each contact in the list has already been
+ * updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned
+ * positions(within {@link #PIN_LIMIT} are unique positive integers.
+ */
+ @VisibleForTesting
+ private ArrayList<ContentProviderOperation> getReflowedPinningOperations(
+ ArrayList<ContactEntry> list, int oldPos, int newPinPos) {
+ final ArrayList<ContentProviderOperation> positions = new ArrayList<>();
+ final int lowerBound = Math.min(oldPos, newPinPos);
+ final int upperBound = Math.max(oldPos, newPinPos);
+ for (int i = lowerBound; i <= upperBound; i++) {
+ final ContactEntry entry = list.get(i);
+
+ // Pinned positions in the database start from 1 instead of being zero-indexed like
+ // arrays, so offset by 1.
+ final int databasePinnedPosition = i + 1;
+ if (entry.pinned == databasePinnedPosition) {
+ continue;
+ }
+
+ final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, String.valueOf(entry.id));
+ final ContentValues values = new ContentValues();
+ values.put(Contacts.PINNED, databasePinnedPosition);
+ positions.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
+ }
+ return positions;
+ }
+
+ @Override
+ public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
+ setInDragging(true);
+ final int itemIndex = mContactEntries.indexOf(view.getContactEntry());
+ popContactEntry(itemIndex);
+ }
+
+ @Override
+ public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {
+ if (view == null) {
+ // The user is hovering over a view that is not a contact tile, no need to do
+ // anything here.
+ return;
+ }
+ final int itemIndex = mContactEntries.indexOf(view.getContactEntry());
+ if (mInDragging
+ && mDragEnteredEntryIndex != itemIndex
+ && isIndexInBound(itemIndex)
+ && itemIndex < PIN_LIMIT
+ && itemIndex >= 0) {
+ markDropArea(itemIndex);
+ }
+ }
+
+ @Override
+ public void onDragFinished(int x, int y) {
+ setInDragging(false);
+ // A contact has been dragged to the RemoveView in order to be unstarred, so simply wait
+ // for the new contact cursor which will cause the UI to be refreshed without the unstarred
+ // contact.
+ if (!mAwaitingRemove) {
+ handleDrop();
+ }
+ }
+
+ @Override
+ public void onDroppedOnRemove() {
+ if (mDraggedEntry != null) {
+ unstarAndUnpinContact(mDraggedEntry.lookupUri);
+ mAwaitingRemove = true;
+ }
+ }
+
+ interface OnDataSetChangedForAnimationListener {
+
+ void onDataSetChangedForAnimation(long... idsInPlace);
+
+ void cacheOffsetsForDatasetChange();
+ }
+
+ private static class ViewTypes {
+
+ static final int TILE = 0;
+ static final int COUNT = 1;
+ }
+}
diff --git a/java/com/android/dialer/app/list/RegularSearchFragment.java b/java/com/android/dialer/app/list/RegularSearchFragment.java
new file mode 100644
index 0000000..2695953
--- /dev/null
+++ b/java/com/android/dialer/app/list/RegularSearchFragment.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import static android.Manifest.permission.READ_CONTACTS;
+
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.support.v13.app.FragmentCompat;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.list.PinnedHeaderListView;
+import com.android.dialer.app.R;
+import com.android.dialer.app.widget.EmptyContentView;
+import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+import com.android.dialer.util.PermissionsUtil;
+
+public class RegularSearchFragment extends SearchFragment
+ implements OnEmptyViewActionButtonClickedListener,
+ FragmentCompat.OnRequestPermissionsResultCallback {
+
+ public static final int PERMISSION_REQUEST_CODE = 1;
+
+ private static final int SEARCH_DIRECTORY_RESULT_LIMIT = 5;
+ protected String mPermissionToRequest;
+
+ public RegularSearchFragment() {
+ configureDirectorySearch();
+ }
+
+ public void configureDirectorySearch() {
+ setDirectorySearchEnabled(true);
+ setDirectoryResultLimit(SEARCH_DIRECTORY_RESULT_LIMIT);
+ }
+
+ @Override
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ super.onCreateView(inflater, container);
+ ((PinnedHeaderListView) getListView()).setScrollToSectionOnHeaderTouch(true);
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ RegularSearchListAdapter adapter = new RegularSearchListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ adapter.setUseCallableUri(usesCallableUri());
+ adapter.setListener(this);
+ return adapter;
+ }
+
+ @Override
+ protected void cacheContactInfo(int position) {
+ CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(getContext()).getCachedNumberLookupService();
+ if (cachedNumberLookupService != null) {
+ final RegularSearchListAdapter adapter = (RegularSearchListAdapter) getAdapter();
+ cachedNumberLookupService.addContact(
+ getContext(), adapter.getContactInfo(cachedNumberLookupService, position));
+ }
+ }
+
+ @Override
+ protected void setupEmptyView() {
+ if (mEmptyView != null && getActivity() != null) {
+ final int imageResource;
+ final int actionLabelResource;
+ final int descriptionResource;
+ final OnEmptyViewActionButtonClickedListener listener;
+ if (!PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) {
+ imageResource = R.drawable.empty_contacts;
+ actionLabelResource = R.string.permission_single_turn_on;
+ descriptionResource = R.string.permission_no_search;
+ listener = this;
+ mPermissionToRequest = READ_CONTACTS;
+ } else {
+ imageResource = EmptyContentView.NO_IMAGE;
+ actionLabelResource = EmptyContentView.NO_LABEL;
+ descriptionResource = EmptyContentView.NO_LABEL;
+ listener = null;
+ mPermissionToRequest = null;
+ }
+
+ mEmptyView.setImage(imageResource);
+ mEmptyView.setActionLabel(actionLabelResource);
+ mEmptyView.setDescription(descriptionResource);
+ if (listener != null) {
+ mEmptyView.setActionClickedListener(listener);
+ }
+ }
+ }
+
+ @Override
+ public void onEmptyViewActionButtonClicked() {
+ final Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ if (READ_CONTACTS.equals(mPermissionToRequest)) {
+ FragmentCompat.requestPermissions(
+ this, new String[] {mPermissionToRequest}, PERMISSION_REQUEST_CODE);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == PERMISSION_REQUEST_CODE) {
+ setupEmptyView();
+ if (grantResults != null
+ && grantResults.length == 1
+ && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+ PermissionsUtil.notifyPermissionGranted(getActivity(), permissions[0]);
+ }
+ }
+ }
+
+ @Override
+ protected int getCallInitiationType(boolean isRemoteDirectory) {
+ return isRemoteDirectory
+ ? CallInitiationType.Type.REMOTE_DIRECTORY
+ : CallInitiationType.Type.REGULAR_SEARCH;
+ }
+
+ public interface CapabilityChecker {
+
+ boolean isNearbyPlacesSearchEnabled();
+ }
+}
diff --git a/java/com/android/dialer/app/list/RegularSearchListAdapter.java b/java/com/android/dialer/app/list/RegularSearchListAdapter.java
new file mode 100644
index 0000000..94544d2
--- /dev/null
+++ b/java/com/android/dialer/app/list/RegularSearchListAdapter.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.list.DirectoryPartition;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.CallUtil;
+
+/** List adapter to display regular search results. */
+public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter {
+
+ protected boolean mIsQuerySipAddress;
+
+ public RegularSearchListAdapter(Context context) {
+ super(context);
+ setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, false);
+ setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, false);
+ }
+
+ public CachedContactInfo getContactInfo(CachedNumberLookupService lookupService, int position) {
+ ContactInfo info = new ContactInfo();
+ CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info);
+ final Cursor item = (Cursor) getItem(position);
+ if (item != null) {
+ final DirectoryPartition partition =
+ (DirectoryPartition) getPartition(getPartitionForPosition(position));
+ final long directoryId = partition.getDirectoryId();
+ final boolean isExtendedDirectory = isExtendedDirectory(directoryId);
+
+ info.name = item.getString(PhoneQuery.DISPLAY_NAME);
+ info.type = item.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = item.getString(PhoneQuery.PHONE_LABEL);
+ info.number = item.getString(PhoneQuery.PHONE_NUMBER);
+ final String photoUriStr = item.getString(PhoneQuery.PHOTO_URI);
+ info.photoUri = photoUriStr == null ? null : Uri.parse(photoUriStr);
+ /*
+ * An extended directory is custom directory in the app, but not a directory provided by
+ * framework. So it can't be USER_TYPE_WORK.
+ *
+ * When a search result is selected, RegularSearchFragment calls getContactInfo and
+ * cache the resulting @{link ContactInfo} into local db. Set usertype to USER_TYPE_WORK
+ * only if it's NOT extended directory id and is enterprise directory.
+ */
+ info.userType =
+ !isExtendedDirectory && DirectoryCompat.isEnterpriseDirectoryId(directoryId)
+ ? ContactsUtils.USER_TYPE_WORK
+ : ContactsUtils.USER_TYPE_CURRENT;
+
+ cacheInfo.setLookupKey(item.getString(PhoneQuery.LOOKUP_KEY));
+
+ final String sourceName = partition.getLabel();
+ if (isExtendedDirectory) {
+ cacheInfo.setExtendedSource(sourceName, directoryId);
+ } else {
+ cacheInfo.setDirectorySource(sourceName, directoryId);
+ }
+ }
+ return cacheInfo;
+ }
+
+ @Override
+ public String getFormattedQueryString() {
+ if (mIsQuerySipAddress) {
+ // Return unnormalized SIP address
+ return getQueryString();
+ }
+ return super.getFormattedQueryString();
+ }
+
+ @Override
+ public void setQueryString(String queryString) {
+ // Don't show actions if the query string contains a letter.
+ final boolean showNumberShortcuts =
+ !TextUtils.isEmpty(getFormattedQueryString()) && hasDigitsInQueryString();
+ mIsQuerySipAddress = PhoneNumberHelper.isUriNumber(queryString);
+
+ if (isChanged(showNumberShortcuts)) {
+ notifyDataSetChanged();
+ }
+ super.setQueryString(queryString);
+ }
+
+ protected boolean isChanged(boolean showNumberShortcuts) {
+ boolean changed = false;
+ changed |= setShortcutEnabled(SHORTCUT_DIRECT_CALL, showNumberShortcuts || mIsQuerySipAddress);
+ changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts);
+ changed |=
+ setShortcutEnabled(
+ SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext()));
+ return changed;
+ }
+
+ /** Whether there is at least one digit in the query string. */
+ private boolean hasDigitsInQueryString() {
+ String queryString = getQueryString();
+ int length = queryString.length();
+ for (int i = 0; i < length; i++) {
+ if (Character.isDigit(queryString.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/app/list/RemoveView.java b/java/com/android/dialer/app/list/RemoveView.java
new file mode 100644
index 0000000..3b917db
--- /dev/null
+++ b/java/com/android/dialer/app/list/RemoveView.java
@@ -0,0 +1,105 @@
+/*
+ * 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.app.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.app.R;
+
+public class RemoveView extends FrameLayout {
+
+ DragDropController mDragDropController;
+ TextView mRemoveText;
+ ImageView mRemoveIcon;
+ int mUnhighlightedColor;
+ int mHighlightedColor;
+ Drawable mRemoveDrawable;
+
+ public RemoveView(Context context) {
+ super(context);
+ }
+
+ public RemoveView(Context context, AttributeSet attrs) {
+ this(context, attrs, -1);
+ }
+
+ public RemoveView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mRemoveText = (TextView) findViewById(R.id.remove_view_text);
+ mRemoveIcon = (ImageView) findViewById(R.id.remove_view_icon);
+ final Resources r = getResources();
+ mUnhighlightedColor = r.getColor(R.color.remove_text_color);
+ mHighlightedColor = r.getColor(R.color.remove_highlighted_text_color);
+ mRemoveDrawable = r.getDrawable(R.drawable.ic_remove);
+ }
+
+ public void setDragDropController(DragDropController controller) {
+ mDragDropController = controller;
+ }
+
+ @Override
+ public boolean onDragEvent(DragEvent event) {
+ final int action = event.getAction();
+ switch (action) {
+ case DragEvent.ACTION_DRAG_ENTERED:
+ // TODO: This is temporary solution and should be removed once accessibility for
+ // drag and drop is supported by framework(b/26871588).
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ setAppearanceHighlighted();
+ break;
+ case DragEvent.ACTION_DRAG_EXITED:
+ setAppearanceNormal();
+ break;
+ case DragEvent.ACTION_DRAG_LOCATION:
+ if (mDragDropController != null) {
+ mDragDropController.handleDragHovered(this, (int) event.getX(), (int) event.getY());
+ }
+ break;
+ case DragEvent.ACTION_DROP:
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ if (mDragDropController != null) {
+ mDragDropController.handleDragFinished((int) event.getX(), (int) event.getY(), true);
+ }
+ setAppearanceNormal();
+ break;
+ }
+ return true;
+ }
+
+ private void setAppearanceNormal() {
+ mRemoveText.setTextColor(mUnhighlightedColor);
+ mRemoveIcon.setColorFilter(mUnhighlightedColor);
+ invalidate();
+ }
+
+ private void setAppearanceHighlighted() {
+ mRemoveText.setTextColor(mHighlightedColor);
+ mRemoveIcon.setColorFilter(mHighlightedColor);
+ invalidate();
+ }
+}
diff --git a/java/com/android/dialer/app/list/SearchFragment.java b/java/com/android/dialer/app/list/SearchFragment.java
new file mode 100644
index 0000000..4a7d48a
--- /dev/null
+++ b/java/com/android/dialer/app/list/SearchFragment.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Space;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
+import com.android.contacts.common.list.PhoneNumberPickerFragment;
+import com.android.contacts.common.util.FabUtil;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.app.R;
+import com.android.dialer.app.dialpad.DialpadFragment.ErrorDialogFragment;
+import com.android.dialer.app.widget.DialpadSearchEmptyContentView;
+import com.android.dialer.app.widget.EmptyContentView;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.PermissionsUtil;
+
+public class SearchFragment extends PhoneNumberPickerFragment {
+
+ protected EmptyContentView mEmptyView;
+ private OnListFragmentScrolledListener mActivityScrollListener;
+ private View.OnTouchListener mActivityOnTouchListener;
+ /*
+ * Stores the untouched user-entered string that is used to populate the add to contacts
+ * intent.
+ */
+ private String mAddToContactNumber;
+ private int mActionBarHeight;
+ private int mShadowHeight;
+ private int mPaddingTop;
+ private int mShowDialpadDuration;
+ private int mHideDialpadDuration;
+ /**
+ * Used to resize the list view containing search results so that it fits the available space
+ * above the dialpad. Does not have a user-visible effect in regular touch usage (since the
+ * dialpad hides that portion of the ListView anyway), but improves usability in accessibility
+ * mode.
+ */
+ private Space mSpacer;
+
+ private HostInterface mActivity;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ setQuickContactEnabled(true);
+ setAdjustSelectionBoundsEnabled(false);
+ setDarkTheme(false);
+ setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(false /* opposite */));
+ setUseCallableUri(true);
+
+ try {
+ mActivityScrollListener = (OnListFragmentScrolledListener) activity;
+ } catch (ClassCastException e) {
+ LogUtil.v(
+ "SearchFragment.onAttach",
+ activity.toString()
+ + " doesn't implement OnListFragmentScrolledListener. "
+ + "Ignoring.");
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (isSearchMode()) {
+ getAdapter().setHasHeader(0, false);
+ }
+
+ mActivity = (HostInterface) getActivity();
+
+ final Resources res = getResources();
+ mActionBarHeight = mActivity.getActionBarHeight();
+ mShadowHeight = res.getDrawable(R.drawable.search_shadow).getIntrinsicHeight();
+ mPaddingTop = res.getDimensionPixelSize(R.dimen.search_list_padding_top);
+ mShowDialpadDuration = res.getInteger(R.integer.dialpad_slide_in_duration);
+ mHideDialpadDuration = res.getInteger(R.integer.dialpad_slide_out_duration);
+
+ final ListView listView = getListView();
+
+ if (mEmptyView == null) {
+ if (this instanceof SmartDialSearchFragment) {
+ mEmptyView = new DialpadSearchEmptyContentView(getActivity());
+ } else {
+ mEmptyView = new EmptyContentView(getActivity());
+ }
+ ((ViewGroup) getListView().getParent()).addView(mEmptyView);
+ getListView().setEmptyView(mEmptyView);
+ setupEmptyView();
+ }
+
+ listView.setBackgroundColor(res.getColor(R.color.background_dialer_results));
+ listView.setClipToPadding(false);
+ setVisibleScrollbarEnabled(false);
+
+ //Turn of accessibility live region as the list constantly update itself and spam messages.
+ listView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE);
+ ContentChangedFilter.addToParent(listView);
+
+ listView.setOnScrollListener(
+ new OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (mActivityScrollListener != null) {
+ mActivityScrollListener.onListFragmentScrollStateChange(scrollState);
+ }
+ }
+
+ @Override
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
+ });
+ if (mActivityOnTouchListener != null) {
+ listView.setOnTouchListener(mActivityOnTouchListener);
+ }
+
+ updatePosition(false /* animate */);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ FabUtil.addBottomPaddingToListViewForFab(getListView(), getResources());
+ }
+
+ @Override
+ public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
+ Animator animator = null;
+ if (nextAnim != 0) {
+ animator = AnimatorInflater.loadAnimator(getActivity(), nextAnim);
+ }
+ if (animator != null) {
+ final View view = getView();
+ final int oldLayerType = view.getLayerType();
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setLayerType(oldLayerType, null);
+ }
+ });
+ }
+ return animator;
+ }
+
+ @Override
+ protected void setSearchMode(boolean flag) {
+ super.setSearchMode(flag);
+ // This hides the "All contacts with phone numbers" header in the search fragment
+ final ContactEntryListAdapter adapter = getAdapter();
+ if (adapter != null) {
+ adapter.setHasHeader(0, false);
+ }
+ }
+
+ public void setAddToContactNumber(String addToContactNumber) {
+ mAddToContactNumber = addToContactNumber;
+ }
+
+ /**
+ * Return true if phone number is prohibited by a value -
+ * (R.string.config_prohibited_phone_number_regexp) in the config files. False otherwise.
+ */
+ public boolean checkForProhibitedPhoneNumber(String number) {
+ // Regular expression prohibiting manual phone call. Can be empty i.e. "no rule".
+ String prohibitedPhoneNumberRegexp =
+ getResources().getString(R.string.config_prohibited_phone_number_regexp);
+
+ // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated
+ // test equipment.
+ if (number != null
+ && !TextUtils.isEmpty(prohibitedPhoneNumberRegexp)
+ && number.matches(prohibitedPhoneNumberRegexp)) {
+ LogUtil.i(
+ "SearchFragment.checkForProhibitedPhoneNumber",
+ "the phone number is prohibited explicitly by a rule");
+ if (getActivity() != null) {
+ DialogFragment dialogFragment =
+ ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message);
+ dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog");
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ DialerPhoneNumberListAdapter adapter = new DialerPhoneNumberListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ adapter.setUseCallableUri(super.usesCallableUri());
+ adapter.setListener(this);
+ return adapter;
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter();
+ final int shortcutType = adapter.getShortcutTypeFromPosition(position);
+ final OnPhoneNumberPickerActionListener listener;
+ final Intent intent;
+ final String number;
+
+ LogUtil.i("SearchFragment.onItemClick", "shortcutType: " + shortcutType);
+
+ switch (shortcutType) {
+ case DialerPhoneNumberListAdapter.SHORTCUT_INVALID:
+ super.onItemClick(position, id);
+ break;
+ case DialerPhoneNumberListAdapter.SHORTCUT_DIRECT_CALL:
+ number = adapter.getQueryString();
+ listener = getOnPhoneNumberPickerListener();
+ if (listener != null && !checkForProhibitedPhoneNumber(number)) {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType =
+ getCallInitiationType(false /* isRemoteDirectory */);
+ callSpecificAppData.positionOfSelectedSearchResult = position;
+ callSpecificAppData.charactersInSearchString =
+ getQueryString() == null ? 0 : getQueryString().length();
+ listener.onPickPhoneNumber(number, false /* isVideoCall */, callSpecificAppData);
+ }
+ break;
+ case DialerPhoneNumberListAdapter.SHORTCUT_CREATE_NEW_CONTACT:
+ number =
+ TextUtils.isEmpty(mAddToContactNumber)
+ ? adapter.getFormattedQueryString()
+ : mAddToContactNumber;
+ intent = IntentUtil.getNewContactIntent(number);
+ DialerUtils.startActivityWithErrorToast(getActivity(), intent);
+ break;
+ case DialerPhoneNumberListAdapter.SHORTCUT_ADD_TO_EXISTING_CONTACT:
+ number =
+ TextUtils.isEmpty(mAddToContactNumber)
+ ? adapter.getFormattedQueryString()
+ : mAddToContactNumber;
+ intent = IntentUtil.getAddToExistingContactIntent(number);
+ DialerUtils.startActivityWithErrorToast(
+ getActivity(), intent, R.string.add_contact_not_available);
+ break;
+ case DialerPhoneNumberListAdapter.SHORTCUT_SEND_SMS_MESSAGE:
+ number = adapter.getFormattedQueryString();
+ intent = IntentUtil.getSendSmsIntent(number);
+ DialerUtils.startActivityWithErrorToast(getActivity(), intent);
+ break;
+ case DialerPhoneNumberListAdapter.SHORTCUT_MAKE_VIDEO_CALL:
+ number =
+ TextUtils.isEmpty(mAddToContactNumber) ? adapter.getQueryString() : mAddToContactNumber;
+ listener = getOnPhoneNumberPickerListener();
+ if (listener != null && !checkForProhibitedPhoneNumber(number)) {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType =
+ getCallInitiationType(false /* isRemoteDirectory */);
+ callSpecificAppData.positionOfSelectedSearchResult = position;
+ callSpecificAppData.charactersInSearchString =
+ getQueryString() == null ? 0 : getQueryString().length();
+ listener.onPickPhoneNumber(number, true /* isVideoCall */, callSpecificAppData);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Updates the position and padding of the search fragment, depending on whether the dialpad is
+ * shown. This can be optionally animated.
+ */
+ public void updatePosition(boolean animate) {
+ if (mActivity == null) {
+ // Activity will be set in onStart, and this method will be called again
+ return;
+ }
+
+ // Use negative shadow height instead of 0 to account for the 9-patch's shadow.
+ int startTranslationValue =
+ mActivity.isDialpadShown() ? mActionBarHeight - mShadowHeight : -mShadowHeight;
+ int endTranslationValue = 0;
+ // Prevents ListView from being translated down after a rotation when the ActionBar is up.
+ if (animate || mActivity.isActionBarShowing()) {
+ endTranslationValue = mActivity.isDialpadShown() ? 0 : mActionBarHeight - mShadowHeight;
+ }
+ if (animate) {
+ // If the dialpad will be shown, then this animation involves sliding the list up.
+ final boolean slideUp = mActivity.isDialpadShown();
+
+ Interpolator interpolator = slideUp ? AnimUtils.EASE_IN : AnimUtils.EASE_OUT;
+ int duration = slideUp ? mShowDialpadDuration : mHideDialpadDuration;
+ getView().setTranslationY(startTranslationValue);
+ getView()
+ .animate()
+ .translationY(endTranslationValue)
+ .setInterpolator(interpolator)
+ .setDuration(duration)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (!slideUp) {
+ resizeListView();
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (slideUp) {
+ resizeListView();
+ }
+ }
+ });
+
+ } else {
+ getView().setTranslationY(endTranslationValue);
+ resizeListView();
+ }
+
+ // There is padding which should only be applied when the dialpad is not shown.
+ int paddingTop = mActivity.isDialpadShown() ? 0 : mPaddingTop;
+ final ListView listView = getListView();
+ listView.setPaddingRelative(
+ listView.getPaddingStart(),
+ paddingTop,
+ listView.getPaddingEnd(),
+ listView.getPaddingBottom());
+ }
+
+ public void resizeListView() {
+ if (mSpacer == null) {
+ return;
+ }
+ int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0;
+ if (spacerHeight != mSpacer.getHeight()) {
+ final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpacer.getLayoutParams();
+ lp.height = spacerHeight;
+ mSpacer.setLayoutParams(lp);
+ }
+ }
+
+ @Override
+ protected void startLoading() {
+ if (getActivity() == null) {
+ return;
+ }
+
+ if (PermissionsUtil.hasContactsPermissions(getActivity())) {
+ super.startLoading();
+ } else if (TextUtils.isEmpty(getQueryString())) {
+ // Clear out any existing call shortcuts.
+ final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter();
+ adapter.disableAllShortcuts();
+ } else {
+ // The contact list is not going to change (we have no results since permissions are
+ // denied), but the shortcuts might because of the different query, so update the
+ // list.
+ getAdapter().notifyDataSetChanged();
+ }
+
+ setupEmptyView();
+ }
+
+ public void setOnTouchListener(View.OnTouchListener onTouchListener) {
+ mActivityOnTouchListener = onTouchListener;
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ final LinearLayout parent = (LinearLayout) super.inflateView(inflater, container);
+ final int orientation = getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ mSpacer = new Space(getActivity());
+ parent.addView(
+ mSpacer, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0));
+ }
+ return parent;
+ }
+
+ protected void setupEmptyView() {}
+
+ public interface HostInterface {
+
+ boolean isActionBarShowing();
+
+ boolean isDialpadShown();
+
+ int getDialpadHeight();
+
+ int getActionBarHideOffset();
+
+ int getActionBarHeight();
+ }
+}
diff --git a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java
new file mode 100644
index 0000000..566a15d
--- /dev/null
+++ b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.dialer.app.dialpad.SmartDialCursorLoader;
+import com.android.dialer.smartdial.SmartDialMatchPosition;
+import com.android.dialer.smartdial.SmartDialNameMatcher;
+import com.android.dialer.smartdial.SmartDialPrefix;
+import com.android.dialer.util.CallUtil;
+import java.util.ArrayList;
+
+/** List adapter to display the SmartDial search results. */
+public class SmartDialNumberListAdapter extends DialerPhoneNumberListAdapter {
+
+ private static final String TAG = SmartDialNumberListAdapter.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ @NonNull private final SmartDialNameMatcher mNameMatcher;
+
+ public SmartDialNumberListAdapter(Context context) {
+ super(context);
+ mNameMatcher = new SmartDialNameMatcher("", SmartDialPrefix.getMap());
+ setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false);
+
+ if (DEBUG) {
+ Log.v(TAG, "Constructing List Adapter");
+ }
+ }
+
+ /** Sets query for the SmartDialCursorLoader. */
+ public void configureLoader(SmartDialCursorLoader loader) {
+ if (DEBUG) {
+ Log.v(TAG, "Configure Loader with query" + getQueryString());
+ }
+
+ if (getQueryString() == null) {
+ loader.configureQuery("");
+ mNameMatcher.setQuery("");
+ } else {
+ loader.configureQuery(getQueryString());
+ mNameMatcher.setQuery(PhoneNumberUtils.normalizeNumber(getQueryString()));
+ }
+ }
+
+ /**
+ * Sets highlight options for a List item in the SmartDial search results.
+ *
+ * @param view ContactListItemView where the result will be displayed.
+ * @param cursor Object containing information of the associated List item.
+ */
+ @Override
+ protected void setHighlight(ContactListItemView view, Cursor cursor) {
+ view.clearHighlightSequences();
+
+ if (mNameMatcher.matches(cursor.getString(PhoneQuery.DISPLAY_NAME))) {
+ final ArrayList<SmartDialMatchPosition> nameMatches = mNameMatcher.getMatchPositions();
+ for (SmartDialMatchPosition match : nameMatches) {
+ view.addNameHighlightSequence(match.start, match.end);
+ if (DEBUG) {
+ Log.v(
+ TAG,
+ cursor.getString(PhoneQuery.DISPLAY_NAME)
+ + " "
+ + mNameMatcher.getQuery()
+ + " "
+ + String.valueOf(match.start));
+ }
+ }
+ }
+
+ final SmartDialMatchPosition numberMatch =
+ mNameMatcher.matchesNumber(cursor.getString(PhoneQuery.PHONE_NUMBER));
+ if (numberMatch != null) {
+ view.addNumberHighlightSequence(numberMatch.start, numberMatch.end);
+ }
+ }
+
+ @Override
+ public void setQueryString(String queryString) {
+ final boolean showNumberShortcuts = !TextUtils.isEmpty(getFormattedQueryString());
+ boolean changed = false;
+ changed |= setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, showNumberShortcuts);
+ changed |= setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, showNumberShortcuts);
+ changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts);
+ changed |=
+ setShortcutEnabled(
+ SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext()));
+ if (changed) {
+ notifyDataSetChanged();
+ }
+ super.setQueryString(queryString);
+ }
+
+ public void setShowEmptyListForNullQuery(boolean show) {
+ mNameMatcher.setShouldMatchEmptyQuery(!show);
+ }
+}
diff --git a/java/com/android/dialer/app/list/SmartDialSearchFragment.java b/java/com/android/dialer/app/list/SmartDialSearchFragment.java
new file mode 100644
index 0000000..c783d3a
--- /dev/null
+++ b/java/com/android/dialer/app/list/SmartDialSearchFragment.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import static android.Manifest.permission.CALL_PHONE;
+
+import android.app.Activity;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v13.app.FragmentCompat;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.dialer.app.R;
+import com.android.dialer.app.dialpad.SmartDialCursorLoader;
+import com.android.dialer.app.widget.EmptyContentView;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Implements a fragment to load and display SmartDial search results. */
+public class SmartDialSearchFragment extends SearchFragment
+ implements EmptyContentView.OnEmptyViewActionButtonClickedListener,
+ FragmentCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = SmartDialSearchFragment.class.getSimpleName();
+
+ private static final int CALL_PHONE_PERMISSION_REQUEST_CODE = 1;
+
+ /** Creates a SmartDialListAdapter to display and operate on search results. */
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity());
+ adapter.setUseCallableUri(super.usesCallableUri());
+ adapter.setQuickContactEnabled(true);
+ adapter.setShowEmptyListForNullQuery(getShowEmptyListForNullQuery());
+ // Set adapter's query string to restore previous instance state.
+ adapter.setQueryString(getQueryString());
+ adapter.setListener(this);
+ return adapter;
+ }
+
+ /** Creates a SmartDialCursorLoader object to load query results. */
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // Smart dialing does not support Directory Load, falls back to normal search instead.
+ if (id == getDirectoryLoaderId()) {
+ return super.onCreateLoader(id, args);
+ } else {
+ final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter();
+ SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext());
+ loader.setShowEmptyListForNullQuery(getShowEmptyListForNullQuery());
+ adapter.configureLoader(loader);
+ return loader;
+ }
+ }
+
+ @Override
+ protected void setupEmptyView() {
+ if (mEmptyView != null && getActivity() != null) {
+ if (!PermissionsUtil.hasPermission(getActivity(), CALL_PHONE)) {
+ mEmptyView.setImage(R.drawable.empty_contacts);
+ mEmptyView.setActionLabel(R.string.permission_single_turn_on);
+ mEmptyView.setDescription(R.string.permission_place_call);
+ mEmptyView.setActionClickedListener(this);
+ } else {
+ mEmptyView.setImage(EmptyContentView.NO_IMAGE);
+ mEmptyView.setActionLabel(EmptyContentView.NO_LABEL);
+ mEmptyView.setDescription(EmptyContentView.NO_LABEL);
+ }
+ }
+ }
+
+ @Override
+ public void onEmptyViewActionButtonClicked() {
+ final Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ FragmentCompat.requestPermissions(
+ this, new String[] {CALL_PHONE}, CALL_PHONE_PERMISSION_REQUEST_CODE);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == CALL_PHONE_PERMISSION_REQUEST_CODE) {
+ setupEmptyView();
+ }
+ }
+
+ @Override
+ protected int getCallInitiationType(boolean isRemoteDirectory) {
+ return CallInitiationType.Type.SMART_DIAL;
+ }
+
+ public boolean isShowingPermissionRequest() {
+ return mEmptyView != null && mEmptyView.isShowingContent();
+ }
+
+ @Override
+ public void setShowEmptyListForNullQuery(boolean show) {
+ if (getAdapter() != null) {
+ ((SmartDialNumberListAdapter) getAdapter()).setShowEmptyListForNullQuery(show);
+ }
+ super.setShowEmptyListForNullQuery(show);
+ }
+}
diff --git a/java/com/android/dialer/app/list/SpeedDialFragment.java b/java/com/android/dialer/app/list/SpeedDialFragment.java
new file mode 100644
index 0000000..8e0f890
--- /dev/null
+++ b/java/com/android/dialer/app/list/SpeedDialFragment.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright (C) 2013 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.app.list;
+
+import static android.Manifest.permission.READ_CONTACTS;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Trace;
+import android.support.annotation.Nullable;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.util.LongSparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.LayoutAnimationController;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.ListView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactTileLoaderFactory;
+import com.android.contacts.common.list.ContactTileView;
+import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
+import com.android.dialer.app.R;
+import com.android.dialer.app.list.ListsFragment.ListsPage;
+import com.android.dialer.app.widget.EmptyContentView;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.util.ViewUtil;
+import java.util.ArrayList;
+
+/** This fragment displays the user's favorite/frequent contacts in a grid. */
+public class SpeedDialFragment extends Fragment
+ implements ListsPage,
+ OnItemClickListener,
+ PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener,
+ EmptyContentView.OnEmptyViewActionButtonClickedListener,
+ FragmentCompat.OnRequestPermissionsResultCallback {
+
+ private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
+
+ /**
+ * By default, the animation code assumes that all items in a list view are of the same height
+ * when animating new list items into view (e.g. from the bottom of the screen into view). This
+ * can cause incorrect translation offsets when a item that is larger or smaller than other list
+ * item is removed from the list. This key is used to provide the actual height of the removed
+ * object so that the actual translation appears correct to the user.
+ */
+ private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE;
+
+ private static final String TAG = "SpeedDialFragment";
+ private static final boolean DEBUG = false;
+ /** Used with LoaderManager. */
+ private static final int LOADER_ID_CONTACT_TILE = 1;
+
+ private final LongSparseArray<Integer> mItemIdTopMap = new LongSparseArray<>();
+ private final LongSparseArray<Integer> mItemIdLeftMap = new LongSparseArray<>();
+ private final ContactTileView.Listener mContactTileAdapterListener =
+ new ContactTileAdapterListener();
+ private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
+ new ContactTileLoaderListener();
+ private final ScrollListener mScrollListener = new ScrollListener();
+ private int mAnimationDuration;
+ private OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener;
+ private OnListFragmentScrolledListener mActivityScrollListener;
+ private PhoneFavoritesTileAdapter mContactTileAdapter;
+ private View mParentView;
+ private PhoneFavoriteListView mListView;
+ private View mContactTileFrame;
+ /** Layout used when there are no favorites. */
+ private EmptyContentView mEmptyView;
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ if (DEBUG) {
+ LogUtil.d("SpeedDialFragment.onCreate", null);
+ }
+ Trace.beginSection(TAG + " onCreate");
+ super.onCreate(savedState);
+
+ // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
+ // We don't construct the resultant adapter at this moment since it requires LayoutInflater
+ // that will be available on onCreateView().
+ mContactTileAdapter =
+ new PhoneFavoritesTileAdapter(getActivity(), mContactTileAdapterListener, this);
+ mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(getActivity()));
+ mAnimationDuration = getResources().getInteger(R.integer.fade_duration);
+ Trace.endSection();
+ }
+
+ @Override
+ public void onResume() {
+ Trace.beginSection(TAG + " onResume");
+ super.onResume();
+ if (mContactTileAdapter != null) {
+ mContactTileAdapter.refreshContactsPreferences();
+ }
+ if (PermissionsUtil.hasContactsPermissions(getActivity())) {
+ if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) {
+ getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
+
+ } else {
+ getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad();
+ }
+
+ mEmptyView.setDescription(R.string.speed_dial_empty);
+ mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action);
+ } else {
+ mEmptyView.setDescription(R.string.permission_no_speeddial);
+ mEmptyView.setActionLabel(R.string.permission_single_turn_on);
+ }
+ Trace.endSection();
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ Trace.beginSection(TAG + " onCreateView");
+ mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false);
+
+ mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list);
+ mListView.setOnItemClickListener(this);
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+ mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter);
+
+ final ImageView dragShadowOverlay =
+ (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay);
+ mListView.setDragShadowOverlay(dragShadowOverlay);
+
+ mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view);
+ mEmptyView.setImage(R.drawable.empty_speed_dial);
+ mEmptyView.setActionClickedListener(this);
+
+ mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame);
+
+ final LayoutAnimationController controller =
+ new LayoutAnimationController(
+ AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in));
+ controller.setDelay(0);
+ mListView.setLayoutAnimation(controller);
+ mListView.setAdapter(mContactTileAdapter);
+
+ mListView.setOnScrollListener(mScrollListener);
+ mListView.setFastScrollEnabled(false);
+ mListView.setFastScrollAlwaysVisible(false);
+
+ //prevent content changes of the list from firing accessibility events.
+ mListView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE);
+ ContentChangedFilter.addToParent(mListView);
+
+ Trace.endSection();
+ return mParentView;
+ }
+
+ public boolean hasFrequents() {
+ if (mContactTileAdapter == null) {
+ return false;
+ }
+ return mContactTileAdapter.getNumFrequents() > 0;
+ }
+
+ /* package */ void setEmptyViewVisibility(final boolean visible) {
+ final int previousVisibility = mEmptyView.getVisibility();
+ final int emptyViewVisibility = visible ? View.VISIBLE : View.GONE;
+ final int listViewVisibility = visible ? View.GONE : View.VISIBLE;
+
+ if (previousVisibility != emptyViewVisibility) {
+ final FrameLayout.LayoutParams params = (LayoutParams) mContactTileFrame.getLayoutParams();
+ params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT;
+ mContactTileFrame.setLayoutParams(params);
+ mEmptyView.setVisibility(emptyViewVisibility);
+ mListView.setVisibility(listViewVisibility);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ final Activity activity = getActivity();
+
+ try {
+ mActivityScrollListener = (OnListFragmentScrolledListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(
+ activity.toString() + " must implement OnListFragmentScrolledListener");
+ }
+
+ try {
+ OnDragDropListener listener = (OnDragDropListener) activity;
+ mListView.getDragDropController().addOnDragDropListener(listener);
+ ((HostInterface) activity).setDragDropController(mListView.getDragDropController());
+ } catch (ClassCastException e) {
+ throw new ClassCastException(
+ activity.toString() + " must implement OnDragDropListener and HostInterface");
+ }
+
+ try {
+ mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(
+ activity.toString() + " must implement PhoneFavoritesFragment.listener");
+ }
+
+ // Use initLoader() instead of restartLoader() to refraining unnecessary reload.
+ // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
+ // be called, on which we'll check if "all" contacts should be reloaded again or not.
+ if (PermissionsUtil.hasContactsPermissions(activity)) {
+ getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
+ } else {
+ setEmptyViewVisibility(true);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This is only effective for elements provided by {@link #mContactTileAdapter}. {@link
+ * #mContactTileAdapter} has its own logic for click events.
+ */
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ if (position <= contactTileAdapterCount) {
+ LogUtil.e(
+ "SpeedDialFragment.onItemClick",
+ "event for unexpected position. The position "
+ + position
+ + " is before \"all\" section. Ignored.");
+ }
+ }
+
+ /**
+ * Cache the current view offsets into memory. Once a relayout of views in the ListView has
+ * happened due to a dataset change, the cached offsets are used to create animations that slide
+ * views from their previous positions to their new ones, to give the appearance that the views
+ * are sliding into their new positions.
+ */
+ private void saveOffsets(int removedItemHeight) {
+ final int firstVisiblePosition = mListView.getFirstVisiblePosition();
+ if (DEBUG) {
+ LogUtil.d("SpeedDialFragment.saveOffsets", "Child count : " + mListView.getChildCount());
+ }
+ for (int i = 0; i < mListView.getChildCount(); i++) {
+ final View child = mListView.getChildAt(i);
+ final int position = firstVisiblePosition + i;
+ // Since we are getting the position from mListView and then querying
+ // mContactTileAdapter, its very possible that things are out of sync
+ // and we might index out of bounds. Let's make sure that this doesn't happen.
+ if (!mContactTileAdapter.isIndexInBound(position)) {
+ continue;
+ }
+ final long itemId = mContactTileAdapter.getItemId(position);
+ if (DEBUG) {
+ LogUtil.d(
+ "SpeedDialFragment.saveOffsets",
+ "Saving itemId: " + itemId + " for listview child " + i + " Top: " + child.getTop());
+ }
+ mItemIdTopMap.put(itemId, child.getTop());
+ mItemIdLeftMap.put(itemId, child.getLeft());
+ }
+ mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight);
+ }
+
+ /*
+ * Performs animations for the gridView
+ */
+ private void animateGridView(final long... idsInPlace) {
+ if (mItemIdTopMap.size() == 0) {
+ // Don't do animations if the database is being queried for the first time and
+ // the previous item offsets have not been cached, or the user hasn't done anything
+ // (dragging, swiping etc) that requires an animation.
+ return;
+ }
+
+ ViewUtil.doOnPreDraw(
+ mListView,
+ true,
+ new Runnable() {
+ @Override
+ public void run() {
+
+ final int firstVisiblePosition = mListView.getFirstVisiblePosition();
+ final AnimatorSet animSet = new AnimatorSet();
+ final ArrayList<Animator> animators = new ArrayList<Animator>();
+ for (int i = 0; i < mListView.getChildCount(); i++) {
+ final View child = mListView.getChildAt(i);
+ int position = firstVisiblePosition + i;
+
+ // Since we are getting the position from mListView and then querying
+ // mContactTileAdapter, its very possible that things are out of sync
+ // and we might index out of bounds. Let's make sure that this doesn't happen.
+ if (!mContactTileAdapter.isIndexInBound(position)) {
+ continue;
+ }
+
+ final long itemId = mContactTileAdapter.getItemId(position);
+
+ if (containsId(idsInPlace, itemId)) {
+ animators.add(ObjectAnimator.ofFloat(child, "alpha", 0.0f, 1.0f));
+ break;
+ } else {
+ Integer startTop = mItemIdTopMap.get(itemId);
+ Integer startLeft = mItemIdLeftMap.get(itemId);
+ final int top = child.getTop();
+ final int left = child.getLeft();
+ int deltaX = 0;
+ int deltaY = 0;
+
+ if (startLeft != null) {
+ if (startLeft != left) {
+ deltaX = startLeft - left;
+ animators.add(ObjectAnimator.ofFloat(child, "translationX", deltaX, 0.0f));
+ }
+ }
+
+ if (startTop != null) {
+ if (startTop != top) {
+ deltaY = startTop - top;
+ animators.add(ObjectAnimator.ofFloat(child, "translationY", deltaY, 0.0f));
+ }
+ }
+
+ if (DEBUG) {
+ LogUtil.d(
+ "SpeedDialFragment.onPreDraw",
+ "Found itemId: "
+ + itemId
+ + " for listview child "
+ + i
+ + " Top: "
+ + top
+ + " Delta: "
+ + deltaY);
+ }
+ }
+ }
+
+ if (animators.size() > 0) {
+ animSet.setDuration(mAnimationDuration).playTogether(animators);
+ animSet.start();
+ }
+
+ mItemIdTopMap.clear();
+ mItemIdLeftMap.clear();
+ }
+ });
+ }
+
+ private boolean containsId(long[] ids, long target) {
+ // Linear search on array is fine because this is typically only 0-1 elements long
+ for (int i = 0; i < ids.length; i++) {
+ if (ids[i] == target) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onDataSetChangedForAnimation(long... idsInPlace) {
+ animateGridView(idsInPlace);
+ }
+
+ @Override
+ public void cacheOffsetsForDatasetChange() {
+ saveOffsets(0);
+ }
+
+ @Override
+ public void onEmptyViewActionButtonClicked() {
+ final Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) {
+ FragmentCompat.requestPermissions(
+ this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
+ } else {
+ // Switch tabs
+ ((HostInterface) activity).showAllContactsTab();
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+ PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS);
+ }
+ }
+ }
+
+ @Override
+ public void onPageResume(@Nullable Activity activity) {
+ LogUtil.i("SpeedDialFragment.onPageResume", null);
+ }
+
+ @Override
+ public void onPagePause(@Nullable Activity activity) {
+ LogUtil.i("SpeedDialFragment.onPagePause", null);
+ }
+
+ public interface HostInterface {
+
+ void setDragDropController(DragDropController controller);
+
+ void showAllContactsTab();
+ }
+
+ private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
+
+ @Override
+ public CursorLoader onCreateLoader(int id, Bundle args) {
+ if (DEBUG) {
+ LogUtil.d("ContactTileLoaderListener.onCreateLoader", null);
+ }
+ return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (DEBUG) {
+ LogUtil.d("ContactTileLoaderListener.onLoadFinished", null);
+ }
+ mContactTileAdapter.setContactCursor(data);
+ setEmptyViewVisibility(mContactTileAdapter.getCount() == 0);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ if (DEBUG) {
+ LogUtil.d("ContactTileLoaderListener.onLoaderReset", null);
+ }
+ }
+ }
+
+ private class ContactTileAdapterListener implements ContactTileView.Listener {
+
+ @Override
+ public void onContactSelected(Uri contactUri, Rect targetRect) {
+ if (mPhoneNumberPickerActionListener != null) {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL;
+ mPhoneNumberPickerActionListener.onPickDataUri(
+ contactUri, false /* isVideoCall */, callSpecificAppData);
+ }
+ }
+
+ @Override
+ public void onCallNumberDirectly(String phoneNumber) {
+ if (mPhoneNumberPickerActionListener != null) {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL;
+ mPhoneNumberPickerActionListener.onPickPhoneNumber(
+ phoneNumber, false /* isVideoCall */, callSpecificAppData);
+ }
+ }
+ }
+
+ private class ScrollListener implements ListView.OnScrollListener {
+
+ @Override
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (mActivityScrollListener != null) {
+ mActivityScrollListener.onListFragmentScroll(
+ firstVisibleItem, visibleItemCount, totalItemCount);
+ }
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ mActivityScrollListener.onListFragmentScrollStateChange(scrollState);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
new file mode 100644
index 0000000..247b34f
--- /dev/null
+++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
@@ -0,0 +1,129 @@
+<!-- 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.
+-->
+
+<!-- This manifest file contains activites that are subclasses by
+ Google Dialer. TODO: Need to stop subclassing activities and move this
+ back into the main manifest file. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.app">
+
+ <application>
+
+ <activity
+ android:exported="false"
+ android:label="@string/dialer_settings_label"
+ android:name="com.android.dialer.app.settings.DialerSettingsActivity"
+ android:parentActivityName="com.android.dialer.app.DialtactsActivity"
+ android:theme="@style/SettingsStyle">
+ </activity>
+
+ <activity
+ android:label="@string/callDetailTitle"
+ android:name="com.android.dialer.app.CallDetailActivity"
+ android:parentActivityName="com.android.dialer.calllog.CallLogActivity"
+ android:theme="@style/CallDetailActivityTheme">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:mimeType="vnd.android.cursor.item/calls"/>
+ </intent-filter>
+ </activity>
+
+ <!-- The entrance point for Phone UI.
+ stateAlwaysHidden is set to suppress keyboard show up on
+ dialpad screen. -->
+ <activity
+ android:clearTaskOnLaunch="true"
+ android:directBootAware="true"
+ android:label="@string/launcherActivityLabel"
+ android:launchMode="singleTask"
+ android:name="com.android.dialer.app.DialtactsActivity"
+ android:resizeableActivity="true"
+ android:theme="@style/DialtactsActivityTheme"
+ android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:mimeType="vnd.android.cursor.item/phone"/>
+ <data android:mimeType="vnd.android.cursor.item/person"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="voicemail"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <action android:name="android.intent.action.DIAL"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="tel"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:mimeType="vnd.android.cursor.dir/calls"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.CALL_BUTTON"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+ </intent-filter>
+ <!-- This was never intended to be public, but is here for backward
+ compatibility. Use Intent.ACTION_DIAL instead. -->
+ <intent-filter>
+ <action android:name="com.android.phone.action.TOUCH_DIALER"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.TAB"/>
+ </intent-filter>
+ <intent-filter android:label="@string/callHistoryIconLabel">
+ <action android:name="com.android.phone.action.RECENT_CALLS"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.TAB"/>
+ </intent-filter>
+
+ <meta-data
+ android:name="com.android.keyguard.layout"
+ android:resource="@layout/keyguard_preview"/>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/app/res/color/settings_text_color_primary.xml b/java/com/android/dialer/app/res/color/settings_text_color_primary.xml
new file mode 100644
index 0000000..ba25908
--- /dev/null
+++ b/java/com/android/dialer/app/res/color/settings_text_color_primary.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 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.
+ */
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/setting_disabled_color" android:state_enabled="false"/>
+ <item android:color="@color/setting_primary_color"/>
+</selector>
diff --git a/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml b/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml
new file mode 100644
index 0000000..2f78992
--- /dev/null
+++ b/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 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.
+ */
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/setting_disabled_color" android:state_enabled="false"/>
+ <item android:color="@color/setting_secondary_color"/>
+</selector>
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png
new file mode 100644
index 0000000..d6f6daa
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png
new file mode 100644
index 0000000..d3c0378
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png
new file mode 100644
index 0000000..3e9232f
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png
new file mode 100644
index 0000000..3cad4c6
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..bb72e89
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png
new file mode 100644
index 0000000..14a33e3
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png
new file mode 100644
index 0000000..70eb073
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png
new file mode 100644
index 0000000..9fb43b0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png
new file mode 100644
index 0000000..4e0d564
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png
new file mode 100644
index 0000000..2cf41d5
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png
new file mode 100644
index 0000000..043685f
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png
new file mode 100644
index 0000000..86eecdd
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png
new file mode 100644
index 0000000..34310aa
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png
new file mode 100644
index 0000000..a36323c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..4b67cf7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png
new file mode 100644
index 0000000..67f07e4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..26a26f9
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png
new file mode 100644
index 0000000..bf413f9
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png
new file mode 100644
index 0000000..4d2ea05
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png
new file mode 100644
index 0000000..ff698af
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..b27dfba
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png
new file mode 100644
index 0000000..57c9fa5
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png
new file mode 100644
index 0000000..1ee6adf
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png
new file mode 100644
index 0000000..3a1a7a7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png
new file mode 100644
index 0000000..f3581d1
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png
new file mode 100644
index 0000000..b09a692
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png
new file mode 100644
index 0000000..62e1f8a
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png
new file mode 100644
index 0000000..03643b2
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png
new file mode 100644
index 0000000..47e3249
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png
new file mode 100644
index 0000000..2bfe0c0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png
new file mode 100644
index 0000000..90b5238
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png
new file mode 100644
index 0000000..7556637
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png
new file mode 100644
index 0000000..03a62e1
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png
new file mode 100644
index 0000000..e22e92c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png
new file mode 100644
index 0000000..57d7871
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png
new file mode 100644
index 0000000..3dc1c17
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png
new file mode 100644
index 0000000..44b06f2
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png
new file mode 100644
index 0000000..3cd59b3
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png
new file mode 100644
index 0000000..2ce7eae
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png
new file mode 100644
index 0000000..98152e0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png
new file mode 100644
index 0000000..4c854e1
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..f6aa3f9
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png
new file mode 100644
index 0000000..169cf29
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png
new file mode 100644
index 0000000..80c0695
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png
new file mode 100644
index 0000000..c903fd1
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png
new file mode 100644
index 0000000..56ac2a3
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png
new file mode 100644
index 0000000..16a44a0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png
new file mode 100644
index 0000000..66df69e
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png
new file mode 100644
index 0000000..d2cbe4c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png
new file mode 100644
index 0000000..81a67ba
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png
new file mode 100644
index 0000000..3597a5e
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..2310c73
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png
new file mode 100644
index 0000000..017e45e
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..d7d5c58
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png
new file mode 100644
index 0000000..b1f1c7e
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png
new file mode 100644
index 0000000..2272d47
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png
new file mode 100644
index 0000000..270e4de
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..c1766b8
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png
new file mode 100644
index 0000000..c61e948
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png
new file mode 100644
index 0000000..2c134ea
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png
new file mode 100644
index 0000000..74ccf14
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png
new file mode 100644
index 0000000..501ee84
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png
new file mode 100644
index 0000000..e944fd7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png
new file mode 100644
index 0000000..d2af0ba
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png
new file mode 100644
index 0000000..d80fb2f
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png
new file mode 100644
index 0000000..4c671ec
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png
new file mode 100644
index 0000000..41044b4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png
new file mode 100644
index 0000000..c6040c0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png
new file mode 100644
index 0000000..ac6a69c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png
new file mode 100644
index 0000000..e5aa7db
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png
new file mode 100644
index 0000000..10992ed
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png
new file mode 100644
index 0000000..7cfd4c7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png
new file mode 100644
index 0000000..0c33905
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png
new file mode 100644
index 0000000..8665d83
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png
new file mode 100644
index 0000000..14ec04b
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png
new file mode 100644
index 0000000..65b1de3
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png
new file mode 100644
index 0000000..a3a7675
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png
new file mode 100644
index 0000000..398a03c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..3513bd9
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png
new file mode 100644
index 0000000..6f13660
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png
new file mode 100644
index 0000000..537fd4e
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png
new file mode 100644
index 0000000..be1ee4d
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png
new file mode 100644
index 0000000..aff140f
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png
new file mode 100644
index 0000000..8975727
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png
new file mode 100644
index 0000000..4d48ea9
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png
new file mode 100644
index 0000000..d65f39d
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png
new file mode 100644
index 0000000..0ad8392
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png
new file mode 100644
index 0000000..6b411cb
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..a9a83b3
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png
new file mode 100644
index 0000000..efab8a7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..3e6ec07
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png
new file mode 100644
index 0000000..138f27c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png
new file mode 100644
index 0000000..f49aed7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png
new file mode 100644
index 0000000..323981c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..83167f4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png
new file mode 100644
index 0000000..a3c80e7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png
new file mode 100644
index 0000000..be81592
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png
new file mode 100644
index 0000000..0e24fa4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png
new file mode 100644
index 0000000..2e27936
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png
new file mode 100644
index 0000000..22a8783
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png
new file mode 100644
index 0000000..2071f42
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png
new file mode 100644
index 0000000..f7dfa21
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png
new file mode 100644
index 0000000..36b5e20
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png
new file mode 100644
index 0000000..99d7fd5
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png
new file mode 100644
index 0000000..468023d
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png
new file mode 100644
index 0000000..9703294
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png
new file mode 100644
index 0000000..59126d7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png
new file mode 100644
index 0000000..2621bc1
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png
new file mode 100644
index 0000000..2ed0034
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png
new file mode 100644
index 0000000..5667ab3
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png
new file mode 100644
index 0000000..8359a50
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png
new file mode 100644
index 0000000..501d7f1
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png
new file mode 100644
index 0000000..407d78c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png
new file mode 100644
index 0000000..fb2ea5f
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png
new file mode 100644
index 0000000..5f1cd45
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..00e04e4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png
new file mode 100644
index 0000000..0364ee0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png
new file mode 100644
index 0000000..9dff893
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png
new file mode 100644
index 0000000..eb63792
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png
new file mode 100644
index 0000000..1657da4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png
new file mode 100644
index 0000000..f25cce6
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png
new file mode 100644
index 0000000..7ac4d8b
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png
new file mode 100644
index 0000000..aa58792
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png
new file mode 100644
index 0000000..d07a1d0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png
new file mode 100644
index 0000000..779bc06
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..07128dd
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png
new file mode 100644
index 0000000..d322813
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..7c256b5
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png
new file mode 100644
index 0000000..f699959
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png
new file mode 100644
index 0000000..7192ad4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png
new file mode 100644
index 0000000..6c68435
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..8fff728
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png
new file mode 100644
index 0000000..547ef30
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png
new file mode 100644
index 0000000..2722f23
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png
new file mode 100644
index 0000000..9594619
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png
new file mode 100644
index 0000000..bfc7273
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png
new file mode 100644
index 0000000..a35b3cd
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png
new file mode 100644
index 0000000..f3c8304
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png
new file mode 100644
index 0000000..828a487
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png
new file mode 100644
index 0000000..bab4a43
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png
new file mode 100644
index 0000000..1c13101
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png
new file mode 100644
index 0000000..ed3a173
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png
new file mode 100644
index 0000000..c04b8d1
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png
new file mode 100644
index 0000000..28b8e93
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png
new file mode 100644
index 0000000..5eb8b67
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png
new file mode 100644
index 0000000..2e751a4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png
new file mode 100644
index 0000000..ff55620
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png
new file mode 100644
index 0000000..bfeb0ff
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png
new file mode 100644
index 0000000..fbac1e4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png
new file mode 100644
index 0000000..5893965
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png
new file mode 100644
index 0000000..9361aa8
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..34cd3fd
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png
new file mode 100644
index 0000000..8243c25
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png
new file mode 100644
index 0000000..4ddee9e
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png
new file mode 100644
index 0000000..2f250f6
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png
new file mode 100644
index 0000000..7f38d09
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png
new file mode 100644
index 0000000..72641c7
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..b7403ff
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png
new file mode 100644
index 0000000..2f2cb3d
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..6591ed4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png
new file mode 100644
index 0000000..2a18de2
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png
new file mode 100644
index 0000000..660ac65
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png
new file mode 100644
index 0000000..5676f70
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..30d141d
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png
new file mode 100644
index 0000000..be5c062
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png
new file mode 100644
index 0000000..395652c
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png
new file mode 100644
index 0000000..b94f4df
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png
new file mode 100644
index 0000000..e351c7b
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png
new file mode 100644
index 0000000..99a1842
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png
new file mode 100644
index 0000000..820ff50
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png
new file mode 100644
index 0000000..4ab55ab
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png
new file mode 100644
index 0000000..82972b4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml b/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml
new file mode 100644
index 0000000..35afbe0
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="270"
+ android:endColor="#ff0a242d"
+ android:startColor="#ff020709"/>
+</shape>
diff --git a/java/com/android/dialer/app/res/drawable/floating_action_button.xml b/java/com/android/dialer/app/res/drawable/floating_action_button.xml
new file mode 100644
index 0000000..0b9af52
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/floating_action_button.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/floating_action_button_touch_tint">
+ <item android:id="@android:id/mask">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml
new file mode 100644
index 0000000..87e0fbc
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_content_copy_24dp"
+ android:tint="@color/call_detail_footer_icon_tint"/>
diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml
new file mode 100644
index 0000000..e6d5c47
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_create_24dp"
+ android:tint="@color/call_detail_footer_icon_tint"/>
diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml
new file mode 100644
index 0000000..e90e83e
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_report_24dp"
+ android:tint="@color/call_detail_footer_icon_tint"/>
diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml
new file mode 100644
index 0000000..3b614cf
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_unblock"
+ android:tint="@color/call_detail_footer_icon_tint"/>
diff --git a/java/com/android/dialer/app/res/drawable/ic_pause.xml b/java/com/android/dialer/app/res/drawable/ic_pause.xml
new file mode 100644
index 0000000..5bea581
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_pause.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false">
+ <bitmap
+ android:src="@drawable/ic_pause_24dp"
+ android:tint="@color/voicemail_icon_disabled_tint"/>
+ </item>
+
+ <item>
+ <bitmap
+ android:src="@drawable/ic_pause_24dp"
+ android:tint="@color/voicemail_playpause_icon_tint"/>
+ </item>
+
+</selector>
diff --git a/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml b/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml
new file mode 100644
index 0000000..d7d9350
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true">
+
+ <item android:state_enabled="false">
+ <bitmap
+ android:src="@drawable/ic_play_arrow_24dp"
+ android:tint="@color/voicemail_icon_disabled_tint"/>
+ </item>
+
+ <item>
+ <bitmap
+ android:src="@drawable/ic_play_arrow_24dp"
+ android:tint="@color/voicemail_playpause_icon_tint"/>
+ </item>
+
+</selector>
diff --git a/java/com/android/dialer/app/res/drawable/ic_search_phone.xml b/java/com/android/dialer/app/res/drawable/ic_search_phone.xml
new file mode 100644
index 0000000..5d449ee
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_search_phone.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_results_phone"
+ android:tint="@color/search_shortcut_icon_color"/>
diff --git a/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml b/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml
new file mode 100644
index 0000000..f07d0a8
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_vm_sound_off_dis" android:state_enabled="false"/>
+ <item android:drawable="@drawable/ic_vm_sound_off_dk"/>
+</selector>
diff --git a/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml b/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml
new file mode 100644
index 0000000..456a048
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_vm_sound_on_dis" android:state_enabled="false"/>
+ <item android:drawable="@drawable/ic_vm_sound_on_dk"/>
+</selector>
diff --git a/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml
new file mode 100644
index 0000000..84cda03
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_handle"
+ android:tint="@color/actionbar_background_color">
+</bitmap>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml
new file mode 100644
index 0000000..5e974c4
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_handle"
+ android:tint="@color/voicemail_icon_disabled_tint">
+</bitmap>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/drawable/oval_ripple.xml b/java/com/android/dialer/app/res/drawable/oval_ripple.xml
new file mode 100644
index 0000000..abb0025
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/oval_ripple.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="#fff"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/dialer/app/res/drawable/overflow_menu.xml b/java/com/android/dialer/app/res/drawable/overflow_menu.xml
new file mode 100644
index 0000000..81be5dc
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/overflow_menu.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_overflow_menu"
+ android:tint="@color/actionbar_icon_color"/>
diff --git a/java/com/android/dialer/app/res/drawable/rounded_corner.xml b/java/com/android/dialer/app/res/drawable/rounded_corner.xml
new file mode 100644
index 0000000..97b58b6
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/rounded_corner.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/searchbox_background_color"/>
+ <corners android:radius="2dp"/>
+</shape>
diff --git a/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml b/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml
new file mode 100644
index 0000000..e47a640
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="true">
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/background">
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_yet_to_play"
+ android:width="2dip"
+ />
+ </shape>
+ </item>
+ <!-- I am not defining a secondary progress colour - we don't use it. -->
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_already_played"
+ android:width="2dip"
+ />
+ </shape>
+ </clip>
+ </item>
+ </layer-list>
+ </item>
+ <item>
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/background">
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_yet_to_play"
+ android:width="2dip"
+ />
+ </shape>
+ </item>
+ <!-- I am not defining a secondary progress colour - we don't use it. -->
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_yet_to_play"
+ android:width="2dip"
+ />
+ </shape>
+ </clip>
+ </item>
+ </layer-list>
+ </item>
+</selector>
diff --git a/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml b/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml
new file mode 100644
index 0000000..47d1152
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.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.
+ */
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false">
+ <shape>
+ <solid android:color="@color/material_grey_300"/>
+ </shape>
+ </item>
+ <item>
+ <shape>
+ <solid android:color="@color/dialer_theme_color"/>
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml b/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml
new file mode 100644
index 0000000..6271a8f
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:angle="0"
+ android:endColor="#1a000000"
+ android:startColor="@null"
+ android:type="linear"/>
+</shape>
diff --git a/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml b/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml
new file mode 100644
index 0000000..86d37a9
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:angle="90"
+ android:endColor="@null"
+ android:startColor="#1a000000"
+ android:type="linear"/>
+</shape>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml b/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml
new file mode 100644
index 0000000..8d8236a
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.dialer.app.dialpad.DialpadFragment$DialpadSlidingRelativeLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <!-- spacer view -->
+ <View
+ android:id="@+id/spacer"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="4"
+ android:background="#00000000"/>
+
+ <!-- Dialpad shadow -->
+ <View
+ android:layout_width="@dimen/shadow_length"
+ android:layout_height="match_parent"
+ android:background="@drawable/shadow_fade_left"/>
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="6">
+
+ <include
+ layout="@layout/dialpad_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <!-- "Dialpad chooser" UI, shown only when the user brings up the
+ Dialer while a call is already in progress.
+ When this UI is visible, the other Dialer elements
+ (the textfield/button and the dialpad) are hidden. -->
+
+ <ListView
+ android:id="@+id/dialpadChooser"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/background_dialer_light"
+ android:visibility="gone"/>
+
+ <!-- Margin bottom and alignParentBottom don't work well together, so use a Space instead. -->
+ <Space
+ android:id="@+id/dialpad_floating_action_button_margin_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/floating_action_button_margin_bottom"
+ android:layout_alignParentBottom="true"/>
+
+ <FrameLayout
+ android:id="@+id/dialpad_floating_action_button_container"
+ android:layout_width="@dimen/floating_action_button_width"
+ android:layout_height="@dimen/floating_action_button_height"
+ android:layout_above="@id/dialpad_floating_action_button_margin_bottom"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/fab_green">
+
+ <ImageButton
+ android:id="@+id/dialpad_floating_action_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/floating_action_button"
+ android:contentDescription="@string/description_dial_button"
+ android:src="@drawable/fab_ic_call"/>
+
+ </FrameLayout>
+
+ </RelativeLayout>
+
+ </LinearLayout>
+</view>
diff --git a/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml b/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml
new file mode 100644
index 0000000..5f80680
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml
@@ -0,0 +1,71 @@
+<?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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="4"
+ android:orientation="vertical">
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ <ImageView
+ android:id="@+id/emptyListViewImage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:importantForAccessibility="no"/>
+
+ <TextView
+ android:id="@+id/emptyListViewMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:gravity="center_horizontal|top"
+ android:textColor="@color/empty_list_text_color"
+ android:textSize="@dimen/empty_list_message_text_size"/>
+
+ <TextView
+ android:id="@+id/emptyListViewAction"
+ style="@style/TextActionStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:gravity="center_horizontal"/>
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ </LinearLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="6"/>
+
+</merge>
diff --git a/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml b/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml
new file mode 100644
index 0000000..c6e1862
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- Layout showing the type of account filter for phone favorite screen
+ (or, new phone "all" screen).
+ This is very similar to account_filter_header.xml but different in its
+ top padding. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/account_filter_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/contact_browser_list_header_left_margin"
+ android:layout_marginEnd="@dimen/contact_browser_list_header_right_margin"
+ android:paddingTop="8dip"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="vertical"
+ android:visibility="gone">
+ <TextView
+ android:id="@+id/account_filter_header"
+ style="@style/ContactListSeparatorTextViewStyle"
+ android:paddingStart="@dimen/contact_browser_list_item_text_indent"/>
+ <TextView
+ android:id="@+id/contact_list_all_empty"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/contact_phone_list_empty_description_padding"
+ android:paddingBottom="@dimen/contact_phone_list_empty_description_padding"
+ android:paddingStart="8dip"
+ android:text="@string/listFoundAllContactsZero"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/contact_phone_list_empty_description_size"
+ android:visibility="gone"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/all_contacts_activity.xml b/java/com/android/dialer/app/res/layout/all_contacts_activity.xml
new file mode 100644
index 0000000..72f0a14
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/all_contacts_activity.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/all_contacts_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <fragment
+ android:id="@+id/all_contacts_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:name="com.android.dialer.app.list.AllContactsFragment"/>
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml b/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml
new file mode 100644
index 0000000..f598478
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pinned_header_list_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- Shown only when an Account filter is set.
+ - paddingTop should be here to show "shade" effect correctly. -->
+ <!-- TODO: Remove the filter header. -->
+ <include layout="@layout/account_filter_header"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+ <view
+ android:id="@android:id/list"
+ style="@style/DialtactsTheme"
+ class="com.android.contacts.common.list.PinnedHeaderListView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="?attr/contact_browser_list_padding_left"
+ android:layout_marginEnd="?attr/contact_browser_list_padding_right"
+ android:paddingTop="18dp"
+ android:fadingEdge="none"
+ android:fastScrollEnabled="true"
+ android:nestedScrollingEnabled="true"/>
+
+ <com.android.dialer.app.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+
+ </FrameLayout>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/blocked_number_footer.xml b/java/com/android/dialer/app/res/layout/blocked_number_footer.xml
new file mode 100644
index 0000000..9e05cfb
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/blocked_number_footer.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/blocked_number_container_padding"
+ android:background="@android:color/white"
+ android:focusable="true"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/blocked_number_footer_textview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/block_number_footer_message_vvm"
+ android:textColor="@color/blocked_number_secondary_text_color"
+ android:textSize="@dimen/blocked_number_settings_description_text_size"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml b/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml
new file mode 100644
index 0000000..745b913
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/blocked_number_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/blocked_number_background"
+ android:orientation="vertical">
+
+ <ListView
+ android:id="@id/android:list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:headerDividersEnabled="false"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/blocked_number_header.xml b/java/com/android/dialer/app/res/layout/blocked_number_header.xml
new file mode 100644
index 0000000..e34510b
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/blocked_number_header.xml
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/blocked_numbers_disabled_for_emergency"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="27dp"
+ android:paddingBottom="29dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:paddingEnd="44dp"
+ android:background="@color/blocked_number_disabled_emergency_background_color"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <TextView
+ style="@style/BlockedNumbersDescriptionTextStyle"
+ android:textStyle="bold"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/blocked_numbers_disabled_emergency_header_label"/>
+
+ <TextView
+ style="@style/BlockedNumbersDescriptionTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/blocked_numbers_disabled_emergency_desc"/>
+
+ </LinearLayout>
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ card_view:cardCornerRadius="0dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/white"
+ android:focusable="true"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/blocked_number_text_view"
+ style="@android:style/TextAppearance.Material.Subhead"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:gravity="center_vertical"
+ android:text="@string/block_list"
+ android:textColor="@color/blocked_number_header_color"/>
+
+ <RelativeLayout
+ android:id="@+id/import_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <TextView
+ android:id="@+id/import_description"
+ style="@style/BlockedNumbersDescriptionTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="11dp"
+ android:paddingBottom="27dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:paddingEnd="@dimen/blocked_number_container_padding"
+ android:text="@string/blocked_call_settings_import_description"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/blocked_number_settings_description_text_size"/>
+
+ <Button
+ android:id="@+id/import_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/import_description"
+ android:text="@string/blocked_call_settings_import_button"/>
+
+ <Button
+ android:id="@+id/view_numbers_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_below="@id/import_description"
+ android:layout_toStartOf="@id/import_button"
+ android:text="@string/blocked_call_settings_view_numbers_button"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="8dp"
+ android:layout_below="@id/import_button"
+ android:background="@color/divider_line_color"/>
+
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/migrate_promo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <TextView
+ android:id="@+id/migrate_promo_header"
+ style="@android:style/TextAppearance.Material.Subhead"
+ android:textStyle="bold"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:paddingEnd="@dimen/blocked_number_container_padding"
+ android:gravity="center_vertical"
+ android:text="@string/migrate_blocked_numbers_dialog_title"
+ android:textColor="@color/blocked_number_header_color"/>
+
+ <TextView
+ android:id="@+id/migrate_promo_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/blocked_number_container_padding"
+ android:layout_marginStart="@dimen/blocked_number_container_padding"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:text="@string/migrate_blocked_numbers_dialog_message"
+ android:textColor="@color/secondary_text_color"/>
+
+ <Button
+ android:id="@+id/migrate_promo_allow_button"
+ style="@style/DialerPrimaryFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/blocked_number_container_padding"
+ android:layout_marginStart="@dimen/blocked_number_container_padding"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:layout_gravity="end"
+ android:text="@string/migrate_blocked_numbers_dialog_allow_button"/>
+
+ <View
+ style="@style/FullWidthDivider"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/add_number_linear_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/blocked_number_add_top_margin"
+ android:paddingBottom="@dimen/blocked_number_add_bottom_margin"
+ android:paddingStart="@dimen/blocked_number_horizontal_margin"
+ android:background="?android:attr/selectableItemBackground"
+ android:baselineAligned="false"
+ android:clickable="true"
+ android:contentDescription="@string/addBlockedNumber"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/add_number_icon"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:importantForAccessibility="no"/>
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/blocked_number_horizontal_margin"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/add_number_textview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:text="@string/addBlockedNumber"
+ android:textColor="@color/blocked_number_primary_text_color"
+ android:textSize="@dimen/blocked_number_primary_text_size"/>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/blocked_number_list_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginStart="72dp"
+ android:background="@color/divider_line_color"/>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/blocked_number_item.xml b/java/com/android/dialer/app/res/layout/blocked_number_item.xml
new file mode 100644
index 0000000..92ebdc3
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/blocked_number_item.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/caller_information"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/blocked_number_horizontal_margin"
+ android:background="@android:color/white"
+ android:baselineAligned="false"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:layout_marginTop="@dimen/blocked_number_top_margin"
+ android:layout_marginBottom="@dimen/blocked_number_bottom_margin"
+ android:focusable="true"/>
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/blocked_number_horizontal_margin"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/caller_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/blocked_number_primary_text_color"
+ android:textSize="@dimen/blocked_number_primary_text_size"/>
+
+ <TextView
+ android:id="@+id/caller_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/blocked_number_secondary_text_color"
+ android:textSize="@dimen/blocked_number_settings_description_text_size"/>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/delete_button"
+ android:layout_width="@dimen/blocked_number_delete_icon_size"
+ android:layout_height="@dimen/blocked_number_delete_icon_size"
+ android:layout_marginEnd="24dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/description_blocked_number_list_delete"
+ android:scaleType="center"
+ android:src="@drawable/ic_remove"
+ android:tint="@color/blocked_number_icon_tint"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml b/java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml
new file mode 100644
index 0000000..0c4874c
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/blocked_numbers_activity_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/action_bar_height">
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail.xml b/java/com/android/dialer/app/res/layout/call_detail.xml
new file mode 100644
index 0000000..58a7bf0
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_detail"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_call_log">
+
+ <!--
+ The list view is under everything.
+ It contains a first header element which is hidden under the controls UI.
+ When scrolling, the controls move up until the name bar hits the top.
+ -->
+ <ListView
+ android:id="@+id/history"
+ android:layout_width="match_parent"
+ android:layout_height="fill_parent"/>
+
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_footer.xml b/java/com/android/dialer/app/res/layout/call_detail_footer.xml
new file mode 100644
index 0000000..5771344
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail_footer.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/divider_line_thickness"
+ android:background="@color/call_log_action_divider"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_copy"
+ style="@style/CallDetailActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_call_detail_content_copy"
+ android:text="@string/action_copy_number_text"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_edit_before_call"
+ style="@style/CallDetailActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_call_detail_edit"
+ android:text="@string/action_edit_number_before_call"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_report"
+ style="@style/CallDetailActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_call_detail_report"
+ android:text="@string/action_report_number"
+ android:visibility="gone"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_header.xml b/java/com/android/dialer/app/res/layout/call_detail_header.xml
new file mode 100644
index 0000000..fd85f0a
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail_header.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/caller_information"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/call_detail_top_margin"
+ android:paddingBottom="@dimen/call_detail_bottom_margin"
+ android:paddingStart="@dimen/call_detail_horizontal_margin"
+ android:background="@color/background_dialer_white"
+ android:baselineAligned="false"
+ android:elevation="@dimen/call_detail_elevation"
+ android:focusable="true"
+ android:orientation="horizontal">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:layout_marginTop="3dp"
+ android:layout_alignParentStart="true"
+ android:layout_gravity="top"
+ android:focusable="true"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/call_detail_horizontal_margin"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/caller_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="2dp"
+ android:layout_marginBottom="3dp"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+
+ <TextView
+ android:id="@+id/caller_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="1dp"
+ android:singleLine="true"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+
+ <TextView
+ android:id="@+id/phone_account_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/call_back_button"
+ android:layout_width="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_height="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_marginEnd="4dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/description_call_log_call_action"
+ android:scaleType="center"
+ android:src="@drawable/ic_call_24dp"
+ android:tint="@color/call_log_list_item_primary_action_icon_tint"
+ android:visibility="gone"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
new file mode 100644
index 0000000..5958ee8
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/call_log_inner_margin"
+ android:paddingBottom="@dimen/call_log_inner_margin"
+ android:paddingStart="@dimen/call_detail_horizontal_margin"
+ android:paddingEnd="@dimen/call_log_outer_margin"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <view
+ android:id="@+id/call_type_icon"
+ class="com.android.dialer.app.calllog.CallTypeIconsView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"/>
+ <TextView
+ android:id="@+id/call_type_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ <TextView
+ android:id="@+id/duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_alert_item.xml b/java/com/android/dialer/app/res/layout/call_log_alert_item.xml
new file mode 100644
index 0000000..1e487c2
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_alert_item.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/layout/call_log_fragment.xml b/java/com/android/dialer/app/res/layout/call_log_fragment.xml
new file mode 100644
index 0000000..64f7c10
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_fragment.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- Layout parameters are set programmatically. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_call_log"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/modal_message_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/floating_action_button_list_bottom_padding"
+ android:paddingStart="@dimen/call_log_horizontal_margin"
+ android:paddingEnd="@dimen/call_log_horizontal_margin"
+ android:background="@color/background_dialer_call_log"
+ android:clipToPadding="false"/>
+
+ <com.android.dialer.app.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_gravity="center"
+ android:gravity="center_vertical"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
new file mode 100644
index 0000000..d544153
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_log_list_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Day group heading. Used to show a "today", "yesterday", "last week" or "other" heading
+ above a group of call log entries. -->
+ <TextView
+ android:id="@+id/call_log_day_group_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_marginStart="@dimen/call_log_start_margin"
+ android:layout_marginEnd="@dimen/call_log_outer_margin"
+ android:fontFamily="sans-serif-medium"
+ android:textColor="@color/call_log_day_group_heading_color"
+ android:textSize="@dimen/call_log_day_group_heading_size"
+ android:paddingTop="@dimen/call_log_day_group_padding_top"
+ android:paddingBottom="@dimen/call_log_day_group_padding_bottom"/>
+
+ <android.support.v7.widget.CardView
+ android:id="@+id/call_log_row"
+ style="@style/CallLogCardStyle">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Primary area containing the contact badge and caller information -->
+ <LinearLayout
+ android:id="@+id/primary_action_view"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/call_log_start_margin"
+ android:paddingEnd="@dimen/call_log_outer_margin"
+ android:paddingTop="@dimen/call_log_vertical_padding"
+ android:paddingBottom="@dimen/call_log_vertical_padding"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:focusable="true"
+ android:nextFocusRight="@+id/call_back_action"
+ android:nextFocusLeft="@+id/quick_contact_photo">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:paddingTop="2dp"
+ android:nextFocusRight="@id/primary_action_view"
+ android:layout_gravity="top"
+ android:focusable="true"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_vertical"
+ android:layout_marginStart="@dimen/call_log_list_item_info_margin_start">
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/call_log_name_margin_bottom"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:textColor="@color/call_log_primary_color"
+ android:textSize="@dimen/call_log_primary_text_size"
+ android:singleLine="true"/>
+
+ <LinearLayout
+ android:id="@+id/call_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <view
+ class="com.android.dialer.app.calllog.CallTypeIconsView"
+ android:id="@+id/call_type_icons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:layout_gravity="center_vertical"/>
+
+ <ImageView
+ android:id="@+id/work_profile_icon"
+ android:src="@drawable/ic_work_profile"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:scaleType="center"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/call_location_and_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:layout_gravity="center_vertical"
+ android:textColor="@color/call_log_detail_color"
+ android:textSize="@dimen/call_log_detail_text_size"
+ android:singleLine="true"/>
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/call_account_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_log_call_account_margin_bottom"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"
+ android:visibility="gone"
+ android:singleLine="true"/>
+
+ <TextView
+ android:id="@+id/voicemail_transcription"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:textColor="@color/call_log_voicemail_transcript_color"
+ android:textSize="@dimen/call_log_voicemail_transcription_text_size"
+ android:ellipsize="marquee"
+ android:autoLink="all"
+ android:visibility="gone"
+ android:singleLine="false"
+ android:maxLines="10"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/primary_action_button"
+ android:layout_width="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_height="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:scaleType="center"
+ android:tint="@color/call_log_list_item_primary_action_icon_tint"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ <!-- Viewstub with additional expandable actions for a call log entry -->
+ <ViewStub
+ android:id="@+id/call_log_entry_actions_stub"
+ android:inflatedId="@+id/call_log_entry_actions"
+ android:layout="@layout/call_log_list_item_actions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"/>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml b/java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml
new file mode 100644
index 0000000..5b857af
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_log_action_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:importantForAccessibility="1"
+ android:orientation="vertical"
+ android:visibility="visible">
+
+ <com.android.dialer.app.voicemail.VoicemailPlaybackLayout
+ android:id="@+id/voicemail_playback_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/call_log_action_divider"/>
+
+ <LinearLayout
+ android:id="@+id/call_action"
+ style="@style/CallLogActionStyle"
+ android:paddingTop="@dimen/call_log_actions_top_padding">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_call_24dp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/call_action_text"
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/description_call_log_call_action"/>
+
+ <TextView
+ android:id="@+id/call_type_or_location_text"
+ style="@style/CallLogActionSupportTextStyle"/>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/video_call_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/quantum_ic_videocam_white_24"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_video_call"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/create_new_contact_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_person_add_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/search_shortcut_create_new_contact"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/add_to_existing_contact_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_person_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/search_shortcut_add_to_contact"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/send_message_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_message_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_send_message"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/call_with_note_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_call_note_white_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_with_a_note"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/call_compose_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_phone_attach"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/share_and_call"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/report_not_spam_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_not_spam"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_remove_spam"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/block_report_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_block_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_block_report_number"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/block_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_block_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_block_number"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/unblock_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_unblock"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_unblock_number"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/details_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_info_outline_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_details"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/share_voicemail"
+ android:visibility="gone"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/quantum_ic_send_black_24"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_share_voicemail"/>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml b/java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml
new file mode 100644
index 0000000..e005296
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<!-- Layout of a single item in the Dialer's "Dialpad chooser" UI. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:scaleType="center"/>
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@color/dialpad_primary_text_color"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/dialpad_fragment.xml b/java/com/android/dialer/app/res/layout/dialpad_fragment.xml
new file mode 100644
index 0000000..2cf198f
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/dialpad_fragment.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.dialer.app.dialpad.DialpadFragment$DialpadSlidingRelativeLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- spacer view -->
+ <View
+ android:id="@+id/spacer"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="#00000000"/>
+ <!-- Dialpad shadow -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/shadow_length"
+ android:background="@drawable/shadow_fade_up"/>
+ <include layout="@layout/dialpad_view"/>
+ <!-- "Dialpad chooser" UI, shown only when the user brings up the
+ Dialer while a call is already in progress.
+ When this UI is visible, the other Dialer elements
+ (the textfield/button and the dialpad) are hidden. -->
+ <ListView
+ android:id="@+id/dialpadChooser"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/background_dialer_light"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ <!-- Margin bottom and alignParentBottom don't work well together, so use a Space instead. -->
+ <Space
+ android:id="@+id/dialpad_floating_action_button_margin_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/floating_action_button_margin_bottom"
+ android:layout_alignParentBottom="true"/>
+
+ <FrameLayout
+ android:id="@+id/dialpad_floating_action_button_container"
+ android:layout_width="@dimen/floating_action_button_width"
+ android:layout_height="@dimen/floating_action_button_height"
+ android:layout_above="@id/dialpad_floating_action_button_margin_bottom"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/fab_green">
+
+ <ImageButton
+ android:id="@+id/dialpad_floating_action_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/floating_action_button"
+ android:contentDescription="@string/description_dial_button"
+ android:src="@drawable/fab_ic_call"/>
+
+ </FrameLayout>
+
+</view>
diff --git a/java/com/android/dialer/app/res/layout/dialtacts_activity.xml b/java/com/android/dialer/app/res/layout/dialtacts_activity.xml
new file mode 100644
index 0000000..042b4a5
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/dialtacts_activity.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<android.support.design.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/dialtacts_mainlayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_light"
+ android:clipChildren="false"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/dialtacts_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false">
+ <!-- The main contacts grid -->
+ <FrameLayout
+ android:id="@+id/dialtacts_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/floating_action_button_container"
+ android:layout_width="@dimen/floating_action_button_width"
+ android:layout_height="@dimen/floating_action_button_height"
+ android:layout_marginBottom="@dimen/floating_action_button_margin_bottom"
+ android:layout_gravity="center_horizontal|bottom"
+ android:background="@drawable/dialer_fab"
+ app:layout_behavior="com.android.dialer.app.FloatingActionButtonBehavior">
+
+ <ImageButton
+ android:id="@+id/floating_action_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/floating_action_button"
+ android:contentDescription="@string/action_menu_dialpad_button"
+ android:src="@drawable/fab_ic_dial"/>
+
+ </FrameLayout>
+
+ <!-- Host container for the contact tile drag shadow -->
+ <FrameLayout
+ android:id="@+id/activity_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ImageView
+ android:id="@+id/contact_tile_drag_shadow_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:importantForAccessibility="no"
+ android:visibility="gone"/>
+ </FrameLayout>
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/java/com/android/dialer/app/res/layout/empty_content_view.xml b/java/com/android/dialer/app/res/layout/empty_content_view.xml
new file mode 100644
index 0000000..96a6a02
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/empty_content_view.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:id="@+id/emptyListViewImage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"/>
+
+ <TextView
+ android:id="@+id/emptyListViewMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:gravity="center_horizontal|top"
+ android:textColor="@color/empty_list_text_color"
+ android:textSize="@dimen/empty_list_message_text_size"/>
+
+ <TextView
+ android:id="@+id/emptyListViewAction"
+ style="@style/TextActionStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:gravity="center_horizontal"/>
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="40dp"/>
+
+</merge>
diff --git a/java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml b/java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml
new file mode 100644
index 0000000..e245aac
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:id="@+id/emptyListViewImage"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal" />
+
+ <TextView
+ android:id="@+id/emptyListViewMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal|top"
+ android:textSize="@dimen/empty_list_message_text_size"
+ android:textColor="@color/empty_list_text_color"
+ android:paddingRight="16dp"
+ android:paddingLeft="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/emptyListViewAction"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_gravity="center_horizontal"
+ android:paddingRight="16dp"
+ android:paddingLeft="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ style="@style/TextActionStyle" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="40dp" />
+
+</merge>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/layout/keyguard_preview.xml b/java/com/android/dialer/app/res/layout/keyguard_preview.xml
new file mode 100644
index 0000000..41fe891
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/keyguard_preview.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="25dp"
+ android:background="@color/dialer_theme_color_dark"/>
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="#ffffff"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/lists_fragment.xml b/java/com/android/dialer/app/res/layout/lists_fragment.xml
new file mode 100644
index 0000000..442b428
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/lists_fragment.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lists_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:animateLayoutChanges="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- TODO: Apply background color to ActionBar instead of a FrameLayout. For now, this is
+ the easiest way to preserve correct pane scrolling and searchbar collapse/expand
+ behaviors. -->
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/action_bar_height_large"
+ android:background="@color/actionbar_background_color"
+ android:elevation="@dimen/tab_elevation"/>
+
+ <com.android.contacts.common.list.ViewPagerTabs
+ android:id="@+id/lists_pager_header"
+ style="@style/DialtactsActionBarTabTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/tab_height"
+ android:layout_gravity="top"
+ android:elevation="@dimen/tab_elevation"
+ android:orientation="horizontal"
+ android:textAllCaps="true"/>
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/lists_pager"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ </LinearLayout>
+
+ <!-- Sets android:importantForAccessibility="no" to avoid being announced when navigating with
+ talkback enabled. It will still be announced when user drag or drop contact onto it.
+ This is required since drag and drop event is only sent to views are visible when drag
+ starts. -->
+ <com.android.dialer.app.list.RemoveView
+ android:id="@+id/remove_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/tab_height"
+ android:layout_marginTop="@dimen/action_bar_height_large"
+ android:contentDescription="@string/remove_contact"
+ android:importantForAccessibility="no">
+
+ <LinearLayout
+ android:id="@+id/remove_view_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/actionbar_background_color"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <ImageView
+ android:id="@+id/remove_view_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:src="@drawable/ic_remove"
+ android:tint="@color/remove_text_color"/>
+
+ <TextView
+ android:id="@+id/remove_view_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/remove_contact"
+ android:textColor="@color/remove_text_color"
+ android:textSize="@dimen/remove_text_size"/>
+
+ </LinearLayout>
+
+ </com.android.dialer.app.list.RemoveView>
+
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml b/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml
new file mode 100644
index 0000000..92b2e8e
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<view
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contact_tile"
+ class="com.android.dialer.app.list.PhoneFavoriteSquareTileView"
+ android:paddingBottom="@dimen/contact_tile_divider_width"
+ android:paddingEnd="@dimen/contact_tile_divider_width">
+
+ <RelativeLayout
+ android:id="@+id/contact_favorite_card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:nextFocusRight="@+id/contact_tile_secondary_button">
+
+ <com.android.contacts.common.widget.LayoutSuppressingImageView
+ android:id="@+id/contact_tile_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="6"/>
+ <View
+ android:id="@+id/shadow_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="4"
+ android:background="@drawable/shadow_contact_photo"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:paddingBottom="@dimen/contact_tile_text_bottom_padding"
+ android:paddingStart="@dimen/contact_tile_text_side_padding"
+ android:paddingEnd="@dimen/contact_tile_text_side_padding"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/contact_tile_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="3dip"
+ android:fontFamily="sans-serif-medium"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textColor="@color/contact_tile_name_color"
+ android:textSize="15sp"/>
+ <ImageView
+ android:id="@+id/contact_star_icon"
+ android:layout_width="@dimen/favorites_star_icon_size"
+ android:layout_height="@dimen/favorites_star_icon_size"
+ android:layout_marginStart="3dp"
+ android:src="@drawable/ic_star"
+ android:visibility="gone"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/contact_tile_phone_type"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="3dip"
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textColor="@color/contact_tile_name_color"
+ android:textSize="11sp"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/contact_tile_push_state"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/item_background_material_dark"
+ android:importantForAccessibility="no"/>
+
+ <ImageButton
+ android:id="@id/contact_tile_secondary_button"
+ android:layout_width="@dimen/contact_tile_info_button_height_and_width"
+ android:layout_height="@dimen/contact_tile_info_button_height_and_width"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:paddingTop="8dp"
+ android:paddingBottom="4dp"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ android:paddingLeft="4dp"
+ android:paddingRight="9dp"
+ android:background="@drawable/item_background_material_dark"
+ android:contentDescription="@string/description_view_contact_detail"
+ android:scaleType="center"
+ android:src="@drawable/ic_more_vert_24dp"/>
+
+ </RelativeLayout>
+</view>
diff --git a/java/com/android/dialer/app/res/layout/search_edittext.xml b/java/com/android/dialer/app/res/layout/search_edittext.xml
new file mode 100644
index 0000000..1b4f9c4
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/search_edittext.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_view_container"
+ class="com.android.dialer.app.widget.SearchEditTextLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/search_top_margin"
+ android:layout_marginBottom="@dimen/search_bottom_margin"
+ android:layout_marginLeft="@dimen/search_margin_horizontal"
+ android:layout_marginRight="@dimen/search_margin_horizontal"
+ android:background="@drawable/rounded_corner"
+ android:elevation="@dimen/search_box_elevation"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/search_box_collapsed"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="@dimen/search_box_left_padding"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/search_magnifying_glass"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="@dimen/search_box_icon_size"
+ android:padding="@dimen/search_box_search_icon_padding"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ android:src="@drawable/ic_ab_search"
+ android:tint="@color/searchbox_icon_tint"/>
+
+ <TextView
+ android:id="@+id/search_box_start_search"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/search_box_collapsed_text_margin_left"
+ android:fontFamily="@string/search_font_family"
+ android:gravity="center_vertical"
+ android:hint="@string/dialer_hint_find_contact"
+ android:textColorHint="@color/searchbox_hint_text_color"
+ android:textSize="@dimen/search_collapsed_text_size"/>
+
+ <ImageView
+ android:id="@+id/voice_search_button"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/description_start_voice_search"
+ android:scaleType="center"
+ android:src="@drawable/ic_mic_grey600"
+ android:tint="@color/searchbox_icon_tint"/>
+
+ <ImageButton
+ android:id="@+id/dialtacts_options_menu_button"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="match_parent"
+ android:paddingEnd="@dimen/search_box_right_padding"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/action_menu_overflow_description"
+ android:scaleType="center"
+ android:src="@drawable/ic_overflow_menu"
+ android:tint="@color/searchbox_icon_tint"/>
+
+ </LinearLayout>
+
+ <include layout="@layout/search_bar_expanded"/>
+
+</view>
diff --git a/java/com/android/dialer/app/res/layout/speed_dial_fragment.xml b/java/com/android/dialer/app/res/layout/speed_dial_fragment.xml
new file mode 100644
index 0000000..c778c6b
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/speed_dial_fragment.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false">
+
+ <FrameLayout
+ android:id="@+id/contact_tile_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:paddingStart="@dimen/favorites_row_start_padding"
+ android:paddingEnd="@dimen/favorites_row_end_padding">
+ <com.android.dialer.app.list.PhoneFavoriteListView
+ android:id="@+id/contact_tile_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/favorites_row_top_padding"
+ android:paddingBottom="@dimen/floating_action_button_list_bottom_padding"
+ android:clipToPadding="false"
+ android:divider="@null"
+ android:fadingEdge="none"
+ android:nestedScrollingEnabled="true"
+ android:numColumns="@integer/contact_tile_column_count_in_favorites"/>
+ </FrameLayout>
+
+ <com.android.dialer.app.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml b/java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml
new file mode 100644
index 0000000..be69174
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/blocked_number_background"
+ android:orientation="vertical">
+
+ <ListView
+ android:id="@id/android:list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:divider="@null"
+ android:headerDividersEnabled="false"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:background="@android:color/white">
+
+ <Button
+ android:id="@+id/import_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:layout_alignParentEnd="true"
+ android:text="@string/blocked_call_settings_import_button"/>
+
+ <Button
+ android:id="@+id/cancel_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/import_description"
+ android:layout_toLeftOf="@id/import_button"
+ android:text="@android:string/cancel"/>
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml b/java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml
new file mode 100644
index 0000000..7fff9d2
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="64dp"
+ android:layout_marginEnd="24dp"
+ android:background="@color/background_dialer_call_log_list_item"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/playback_state_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textSize="14sp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/voicemail_playback_top_padding"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/playback_position_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:importantForAccessibility="no"
+ android:textSize="14sp"/>
+
+ <SeekBar
+ android:id="@+id/playback_seek"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:contentDescription="@string/description_playback_seek"
+ android:max="0"
+ android:progress="0"
+ android:progressDrawable="@drawable/seekbar_drawable"
+ android:thumb="@drawable/ic_voicemail_seek_handle"/>
+
+ <TextView
+ android:id="@+id/total_duration_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:importantForAccessibility="no"
+ android:textSize="14sp"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/playback_speakerphone"
+ style="@style/VoicemailPlaybackLayoutButtonStyle"
+ android:contentDescription="@string/description_playback_speakerphone"
+ android:src="@drawable/ic_volume_down_24dp"
+ android:tint="@color/voicemail_icon_tint"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/playback_start_stop"
+ style="@style/VoicemailPlaybackLayoutButtonStyle"
+ android:contentDescription="@string/voicemail_play_start_pause"
+ android:src="@drawable/ic_play_arrow"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/delete_voicemail"
+ style="@style/VoicemailPlaybackLayoutButtonStyle"
+ android:contentDescription="@string/call_log_trash_voicemail"
+ android:src="@drawable/ic_delete_24dp"
+ android:tint="@color/voicemail_icon_tint"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/menu/dialpad_options.xml b/java/com/android/dialer/app/res/menu/dialpad_options.xml
new file mode 100644
index 0000000..2921ea3
--- /dev/null
+++ b/java/com/android/dialer/app/res/menu/dialpad_options.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_2s_pause"
+ android:showAsAction="withText"
+ android:title="@string/add_2sec_pause"/>
+ <item
+ android:id="@+id/menu_add_wait"
+ android:showAsAction="withText"
+ android:title="@string/add_wait"/>
+ <item
+ android:id="@+id/menu_call_with_note"
+ android:showAsAction="withText"
+ android:title="@string/call_with_a_note"/>
+</menu>
diff --git a/java/com/android/dialer/app/res/menu/dialtacts_options.xml b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
new file mode 100644
index 0000000..434aa81
--- /dev/null
+++ b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_delete_all"
+ android:title="@string/call_log_delete_all"/>
+ <item
+ android:id="@+id/menu_clear_frequents"
+ android:title="@string/menu_clear_frequents"/>
+ <item
+ android:id="@+id/menu_call_settings"
+ android:title="@string/dialer_settings_label"/>
+
+</menu>
diff --git a/java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png
new file mode 100644
index 0000000..15c4142
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png
new file mode 100644
index 0000000..3088f75
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png
new file mode 100644
index 0000000..e87de01
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png
new file mode 100644
index 0000000..b866b79
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png
new file mode 100644
index 0000000..26f51f1
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/values/animation_constants.xml b/java/com/android/dialer/app/res/values/animation_constants.xml
new file mode 100644
index 0000000..91230cd
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/animation_constants.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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>
+ <integer name="fade_duration">300</integer>
+
+ <!-- Swipe constants -->
+ <integer name="swipe_escape_velocity">100</integer>
+ <integer name="escape_animation_duration">200</integer>
+ <integer name="max_escape_animation_duration">400</integer>
+ <integer name="max_dismiss_velocity">2000</integer>
+ <integer name="snap_animation_duration">350</integer>
+ <integer name="swipe_scroll_slop">2</integer>
+ <dimen name="min_swipe">0dip</dimen>
+ <dimen name="min_vert">10dip</dimen>
+ <dimen name="min_lock">20dip</dimen>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/attrs.xml b/java/com/android/dialer/app/res/values/attrs.xml
new file mode 100644
index 0000000..b346390
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <declare-styleable name="SearchEditTextLayout"/>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml
new file mode 100644
index 0000000..b88e552
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/colors.xml
@@ -0,0 +1,115 @@
+<!--
+ ~ Copyright (C) 2012 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="dialer_red_highlight_color">#ff1744</color>
+ <color name="dialer_green_highlight_color">#00c853</color>
+
+ <color name="dialer_button_text_color">#fff</color>
+ <color name="dialer_flat_button_text_color">@color/dialer_theme_color</color>
+
+ <!-- Color for the setting text. -->
+ <color name="setting_primary_color">@color/dialer_primary_text_color</color>
+ <!-- Color for the setting description text. -->
+ <color name="setting_secondary_color">@color/dialer_secondary_text_color</color>
+ <color name="setting_disabled_color">#aaaaaa</color>
+ <color name="setting_background_color">#ffffff</color>
+ <color name="setting_button_color">#eee</color>
+
+ <!-- 54% black -->
+ <color name="call_log_icon_tint">#8a000000</color>
+ <!-- 87% black -->
+ <color name="call_log_primary_color">#de000000</color>
+ <!-- 54% black -->
+ <color name="call_log_detail_color">#8a000000</color>
+ <!-- 87% black -->
+ <color name="call_log_voicemail_transcript_color">#de000000</color>
+ <!-- 70% black -->
+ <color name="call_log_action_color">#b3000000</color>
+ <!-- 54% black -->
+ <color name="call_log_day_group_heading_color">#8a000000</color>
+ <!-- 87% black-->
+ <color name="call_log_unread_text_color">#de000000</color>
+ <color name="call_log_list_item_primary_action_icon_tint">@color/call_log_icon_tint</color>
+
+ <color name="voicemail_icon_tint">@color/call_log_icon_tint</color>
+ <color name="voicemail_icon_disabled_tint">#80000000</color>
+ <color name="voicemail_playpause_icon_tint">@color/voicemail_icon_tint</color>
+ <!-- Colour of voicemail progress bar to the right of position indicator. -->
+ <color name="voicemail_playback_seek_bar_yet_to_play">#cecece</color>
+ <!-- Colour of voicemail progress bar to the left of position indicator. -->
+ <color name="voicemail_playback_seek_bar_already_played">@color/dialer_theme_color</color>
+
+ <!-- Background color of new dialer activity -->
+ <color name="background_dialer_light">#fafafa</color>
+ <!-- Background color for search results and call details -->
+ <color name="background_dialer_results">#f9f9f9</color>
+ <color name="background_dialer_call_log">@color/background_dialer_light</color>
+
+ <!-- Color of the 1dp divider that separates favorites -->
+ <color name="favorite_contacts_separator_color">#d0d0d0</color>
+
+ <!-- Color of the contact name in favorite tiles -->
+ <color name="contact_tile_name_color">#ffffff</color>
+
+ <color name="contact_list_name_text_color">@color/dialer_primary_text_color</color>
+
+ <!-- Undo dialogue color -->
+ <color name="undo_dialogue_text_color">#4d4d4d</color>
+
+ <color name="empty_list_text_color">#b2b2b2</color>
+
+ <color name="remove_text_color">#ffffff</color>
+
+ <!-- Text color for the "Remove" text when a contact is dragged on top of the remove view -->
+ <color name="remove_highlighted_text_color">#FF3F3B</color>
+
+ <!-- Color of the bottom border below the contacts grid on the main dialer screen. -->
+ <color name="contacts_grid_bottom_border_color">#16000000</color>
+
+ <!-- Color of actions in expanded call log entries. This text color represents actions such
+ as call back, play voicemail, etc. -->
+ <color name="call_log_action_text">@color/dialer_theme_color</color>
+
+ <!-- Color for missed call icons. -->
+ <color name="missed_call">#ff2e58</color>
+ <!-- Color for answered or outgoing call icons. -->
+ <color name="answered_call">@color/dialer_green_highlight_color</color>
+ <!-- Color for blocked call icons. -->
+ <color name="blocked_call">@color/dialer_secondary_text_color</color>
+
+ <color name="dialer_dialpad_touch_tint">@color/dialer_theme_color_20pct</color>
+
+ <color name="floating_action_button_touch_tint">#80ffffff</color>
+
+ <color name="call_log_action_divider">#eeeeee</color>
+ <color name="divider_line_color">#D8D8D8</color>
+
+ <!-- Colors for blocked numbers list -->
+ <color name="blocked_number_primary_text_color">@color/dialer_primary_text_color</color>
+ <color name="blocked_number_secondary_text_color">@color/dialer_secondary_text_color</color>
+ <color name="blocked_number_icon_tint">#616161</color>
+ <color name="blocked_number_background">#FFFFFF</color>
+ <color name="blocked_number_block_color">#F44336</color>
+ <color name="blocked_number_header_color">@color/dialer_theme_color</color>
+ <color name="blocked_number_disabled_emergency_header_color">#616161</color>
+ <color name="blocked_number_disabled_emergency_background_color">#E0E0E0</color>
+ <color name="add_blocked_number_icon_color">#bdbdbd</color>
+ <!-- Grey 700 -->
+ <color name="call_detail_footer_text_color">#616161</color>
+ <color name="call_detail_footer_icon_tint">@color/call_detail_footer_text_color</color>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/dimens.xml b/java/com/android/dialer/app/res/values/dimens.xml
new file mode 100644
index 0000000..f3fd633
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/dimens.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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="button_horizontal_padding">16dp</dimen>
+ <dimen name="divider_line_thickness">1dp</dimen>
+
+ <!--
+ Drag to remove view (in dp because it is used in conjunction with a statically
+ sized icon
+ -->
+ <dimen name="remove_text_size">16dp</dimen>
+
+ <!-- Call Log -->
+ <dimen name="call_log_horizontal_margin">8dp</dimen>
+ <dimen name="call_log_call_action_size">32dp</dimen>
+ <dimen name="call_log_call_action_width">54dp</dimen>
+ <dimen name="call_log_icon_margin">4dp</dimen>
+ <dimen name="call_log_inner_margin">13dp</dimen>
+ <dimen name="call_log_outer_margin">8dp</dimen>
+ <dimen name="call_log_start_margin">8dp</dimen>
+ <dimen name="call_log_indent_margin">24dp</dimen>
+ <dimen name="call_log_name_margin_bottom">2dp</dimen>
+ <dimen name="call_log_call_account_margin_bottom">2dp</dimen>
+ <dimen name="call_log_vertical_padding">8dp</dimen>
+ <dimen name="call_log_list_item_height">56dp</dimen>
+ <dimen name="call_log_list_item_info_margin_start">16dp</dimen>
+ <dimen name="show_call_history_list_item_height">72dp</dimen>
+
+ <!-- Size of contact photos in the call log and call details. -->
+ <dimen name="contact_photo_size">48dp</dimen>
+ <dimen name="call_detail_button_spacing">2dip</dimen>
+ <dimen name="call_detail_horizontal_margin">20dp</dimen>
+ <dimen name="call_detail_top_margin">16dp</dimen>
+ <dimen name="call_detail_bottom_margin">16dp</dimen>
+ <dimen name="call_detail_header_top_margin">20dp</dimen>
+ <dimen name="call_detail_header_bottom_margin">9dp</dimen>
+ <dimen name="call_detail_elevation">0.5dp</dimen>
+ <dimen name="call_detail_action_item_padding_horizontal">28dp</dimen>
+ <dimen name="call_detail_action_item_padding_vertical">16dp</dimen>
+ <dimen name="call_detail_action_item_drawable_padding">28dp</dimen>
+ <dimen name="call_detail_action_item_text_size">16sp</dimen>
+ <dimen name="transcription_top_margin">18dp</dimen>
+ <dimen name="transcription_bottom_margin">18dp</dimen>
+
+ <!-- Size of call provider icon width and height -->
+ <dimen name="call_provider_small_icon_size">12dp</dimen>
+
+ <!-- Match call_button_height to Phone's dimens/in_call_end_button_height -->
+ <dimen name="call_button_height">74dp</dimen>
+
+ <!-- Dimensions for speed dial tiles -->
+ <dimen name="contact_tile_divider_width">1dp</dimen>
+ <dimen name="contact_tile_info_button_height_and_width">36dp</dimen>
+ <item name="contact_tile_height_to_width_ratio" type="dimen">76%</item>
+ <dimen name="contact_tile_text_side_padding">12dp</dimen>
+ <dimen name="contact_tile_text_bottom_padding">9dp</dimen>
+ <dimen name="favorites_row_top_padding">2dp</dimen>
+ <dimen name="favorites_row_bottom_padding">0dp</dimen>
+ <dimen name="favorites_row_start_padding">1dp</dimen>
+
+ <!-- Padding from the last contact tile will provide the end padding. -->
+ <dimen name="favorites_row_end_padding">0dp</dimen>
+ <dimen name="favorites_row_undo_text_side_padding">32dp</dimen>
+
+ <!-- Size of the star icon on the favorites tile. -->
+ <dimen name="favorites_star_icon_size">12dp</dimen>
+
+ <!-- Padding for the tooltip -->
+ <dimen name="dismiss_button_padding_start">20dip</dimen>
+ <dimen name="dismiss_button_padding_end">28dip</dimen>
+
+ <!-- Margin to the left and right of the search box. -->
+ <dimen name="search_margin_horizontal">8dp</dimen>
+ <!-- Margin above the search box. -->
+ <dimen name="search_top_margin">8dp</dimen>
+ <!-- Margin below the search box. -->
+ <dimen name="search_bottom_margin">8dp</dimen>
+ <dimen name="search_collapsed_text_size">14sp</dimen>
+ <!-- Search box interior padding - left -->
+ <dimen name="search_box_left_padding">8dp</dimen>
+ <!-- Search box interior padding - right -->
+ <dimen name="search_box_right_padding">8dp</dimen>
+ <dimen name="search_box_search_icon_padding">2dp</dimen>
+ <dimen name="search_box_collapsed_text_margin_left">22dp</dimen>
+ <dimen name="search_list_padding_top">16dp</dimen>
+ <dimen name="search_box_elevation">3dp</dimen>
+
+ <!-- Padding for icons to increase their touch target. Icons are typically 24 dps in size
+ so this extra padding makes the entire touch target 40dp -->
+ <dimen name="icon_padding">8dp</dimen>
+
+ <!-- Length of dialpad's shadows in dialer. -->
+ <dimen name="shadow_length">10dp</dimen>
+
+ <dimen name="empty_list_message_top_padding">20dp</dimen>
+ <dimen name="empty_list_message_text_size">16sp</dimen>
+
+ <!-- Dimensions for individual preference cards -->
+ <dimen name="preference_padding_top">16dp</dimen>
+ <dimen name="preference_padding_bottom">16dp</dimen>
+ <dimen name="preference_side_margin">16dp</dimen>
+ <dimen name="preference_summary_line_spacing_extra">4dp</dimen>
+
+ <dimen name="call_log_list_item_primary_action_dimen">48dp</dimen>
+
+ <!-- Dimensions for promo cards -->
+ <dimen name="promo_card_icon_size">24dp</dimen>
+ <dimen name="promo_card_start_padding">16dp</dimen>
+ <dimen name="promo_card_top_padding">21dp</dimen>
+ <dimen name="promo_card_main_padding">24dp</dimen>
+ <dimen name="promo_card_title_padding">12dp</dimen>
+ <dimen name="promo_card_action_vertical_padding">4dp</dimen>
+ <dimen name="promo_card_action_end_padding">4dp</dimen>
+ <dimen name="promo_card_action_between_padding">11dp</dimen>
+ <dimen name="promo_card_line_spacing">4dp</dimen>
+
+ <dimen name="voicemail_playback_top_padding">12dp</dimen>
+
+ <!-- Size of entries in blocked numbers list -->
+ <dimen name="blocked_number_container_padding">16dp</dimen>
+ <dimen name="blocked_number_horizontal_margin">16dp</dimen>
+ <dimen name="blocked_number_top_margin">16dp</dimen>
+ <dimen name="blocked_number_bottom_margin">16dp</dimen>
+ <dimen name="blocked_number_add_top_margin">8dp</dimen>
+ <dimen name="blocked_number_add_bottom_margin">8dp</dimen>
+ <dimen name="blocked_number_primary_text_size">16sp</dimen>
+ <dimen name="blocked_number_secondary_text_size">12sp</dimen>
+ <dimen name="blocked_number_delete_icon_size">32dp</dimen>
+ <dimen name="blocked_number_search_text_size">14sp</dimen>
+ <dimen name="blocked_number_settings_description_text_size">14sp</dimen>
+ <dimen name="blocked_number_header_height">48dp</dimen>
+
+ <dimen name="call_type_icon_size">12dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/donottranslate_config.xml b/java/com/android/dialer/app/res/values/donottranslate_config.xml
new file mode 100644
index 0000000..e7a8e6f
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/donottranslate_config.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <!-- If true, enable vibration (haptic feedback) for dialer key presses.
+ The pattern is set on a per-platform basis using config_virtualKeyVibePattern.
+ TODO: If enough users are annoyed by this, we might eventually
+ need to make it a user preference rather than a per-platform
+ resource. -->
+ <bool name="config_enable_dialer_key_vibration">true</bool>
+
+ <!-- If true, show an onscreen "Dial" button in the dialer.
+ In practice this is used on all platforms even the ones with hard SEND/END
+ keys, but for maximum flexibility it's controlled by a flag here
+ (which can be overridden on a per-product basis.) -->
+ <bool name="config_show_onscreen_dial_button">true</bool>
+
+ <!-- Regular expression for prohibiting certain phone numbers in dialpad.
+ Ignored if empty. -->
+ <string name="config_prohibited_phone_number_regexp"></string>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/ids.xml b/java/com/android/dialer/app/res/values/ids.xml
new file mode 100644
index 0000000..8566f26
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/ids.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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>
+ <item name="call_detail_delete_menu_item" type="id"/>
+ <item name="context_menu_copy_to_clipboard" type="id"/>
+ <item name="context_menu_copy_transcript_to_clipboard" type="id"/>
+ <item name="context_menu_edit_before_call" type="id"/>
+ <item name="context_menu_block_report_spam" type="id"/>
+ <item name="context_menu_block" type="id"/>
+ <item name="context_menu_unblock" type="id"/>
+ <item name="context_menu_report_not_spam" type="id"/>
+ <item name="settings_header_sounds_and_vibration" type="id"/>
+ <item name="block_id" type="id"/>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/strings.xml b/java/com/android/dialer/app/res/values/strings.xml
new file mode 100644
index 0000000..689ee1b
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/strings.xml
@@ -0,0 +1,960 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Application name used in Settings/Apps. Default label for activities
+ that don't specify a label. -->
+ <string name="applicationLabel">Phone</string>
+
+ <!-- Title for the activity that dials the phone, when launched directly into the dialpad -->
+ <string name="launcherDialpadActivityLabel">Phone Keypad</string>
+ <!-- The description text for the dialer tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE] -->
+ <string name="dialerIconLabel">Phone</string>
+
+ <!-- The description text for the call log tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE] -->
+ <string name="callHistoryIconLabel">Call history</string>
+
+ <!-- Text for a menu item to report a call as having been incorrectly identified. [CHAR LIMIT=48] -->
+ <string name="action_report_number">Report inaccurate number</string>
+
+ <!-- Option displayed in context menu to copy long pressed phone number. [CHAR LIMIT=48] -->
+ <string name="action_copy_number_text">Copy number</string>
+
+ <!-- Option displayed in context menu to copy long pressed voicemail transcription. [CHAR LIMIT=48] -->
+ <string name="copy_transcript_text">Copy transcription</string>
+
+ <!-- Label for action to block a number. [CHAR LIMIT=48] -->
+ <string name="action_block_number">Block number</string>
+
+ <!-- Label for action to unblock a number [CHAR LIMIT=48]-->
+ <string name="action_unblock_number">Unblock number</string>
+
+ <!-- Menu item in call details used to remove a call or voicemail from the call log. -->
+ <string name="call_details_delete">Delete</string>
+
+ <!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] -->
+ <string name="action_edit_number_before_call">Edit number before call</string>
+
+ <!-- Menu item used to remove all calls from the call log -->
+ <string name="call_log_delete_all">Clear call history</string>
+
+ <!-- Menu item used to delete a voicemail. [CHAR LIMIT=30] -->
+ <string name="call_log_trash_voicemail">Delete voicemail</string>
+
+ <!-- Text for snackbar to undo a voicemail delete. [CHAR LIMIT=30] -->
+ <string name="snackbar_voicemail_deleted">Voicemail deleted</string>
+
+ <!-- Text for undo button in snackbar for voicemail deletion. [CHAR LIMIT=10] -->
+ <string name="snackbar_voicemail_deleted_undo">UNDO</string>
+
+ <!-- Title of the confirmation dialog for clearing the call log. [CHAR LIMIT=37] -->
+ <string name="clearCallLogConfirmation_title">Clear call history?</string>
+
+ <!-- Confirmation dialog for clearing the call log. [CHAR LIMIT=NONE] -->
+ <string name="clearCallLogConfirmation">This will delete all calls from your history</string>
+
+ <!-- Title of the "Clearing call log" progress-dialog [CHAR LIMIT=35] -->
+ <string name="clearCallLogProgress_title">Clearing call history\u2026</string>
+
+ <!-- Title used for the activity for placing a call. This name appears
+ in activity disambig dialogs -->
+ <string name="userCallActivityLabel" product="default">Phone</string>
+
+ <!-- Notification strings -->
+ <!-- Missed call notification label, used when there's exactly one missed call -->
+ <string name="notification_missedCallTitle">Missed call</string>
+ <!-- Missed call notification label, used when there's exactly one missed call from work contact -->
+ <string name="notification_missedWorkCallTitle">Missed work call</string>
+ <!-- Missed call notification label, used when there are two or more missed calls -->
+ <string name="notification_missedCallsTitle">Missed calls</string>
+ <!-- Missed call notification message used when there are multiple missed calls -->
+ <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%s</xliff:g> missed calls</string>
+ <!-- Message for "call back" Action, which is displayed in the missed call notificaiton.
+ The user will be able to call back to the person or the phone number.
+ [CHAR LIMIT=18] -->
+ <string name="notification_missedCall_call_back">Call back</string>
+ <!-- Message for "reply via sms" action, which is displayed in the missed call notification.
+ The user will be able to send text messages using the phone number.
+ [CHAR LIMIT=18] -->
+ <string name="notification_missedCall_message">Message</string>
+ <!-- Hardcoded number used for restricted incoming phone numbers. -->
+ <string name="handle_restricted" translatable="false">RESTRICTED</string>
+ <!-- Format for a post call message. (ex. John Doe: Give me a call when you're free.) -->
+ <string name="post_call_notification_message"><xliff:g id="name">%1$s</xliff:g>: <xliff:g id="message">%2$s</xliff:g></string>
+
+ <!-- Title of the notification of new voicemails. [CHAR LIMIT=30] -->
+ <plurals name="notification_voicemail_title">
+ <item quantity="one">Voicemail</item>
+ <item quantity="other">
+ <xliff:g id="count">%1$d</xliff:g>
+ Voicemails
+ </item>
+ </plurals>
+
+ <!-- Used in the notification of a new voicemail for the action to play the voicemail. -->
+ <string name="notification_action_voicemail_play">Play</string>
+
+ <!-- Used to build a list of names or phone numbers, to indicate the callers who left
+ voicemails.
+ The first argument may be one or more callers, the most recent ones.
+ The second argument is an additional callers.
+ This string is used to build a list of callers.
+
+ [CHAR LIMIT=10]
+ -->
+ <string name="notification_voicemail_callers_list"><xliff:g id="newer_callers">%1$s</xliff:g>,
+ <xliff:g id="older_caller">%2$s</xliff:g>
+ </string>
+
+ <!-- Text used in the ticker to notify the user of the latest voicemail. [CHAR LIMIT=30] -->
+ <string name="notification_new_voicemail_ticker">New voicemail from
+ <xliff:g id="caller">%1$s</xliff:g>
+ </string>
+
+ <!-- Message to show when there is an error playing back the voicemail. [CHAR LIMIT=40] -->
+ <string name="voicemail_playback_error">Couldn\'t play voicemail</string>
+
+ <!-- Message to display whilst we are waiting for the content to be fetched. [CHAR LIMIT=40] -->
+ <string name="voicemail_fetching_content">Loading voicemail\u2026</string>
+
+ <!-- Message to display whilst we are waiting for the content to be archived. [CHAR LIMIT=40] -->
+ <string name="voicemail_archiving_content">Archiving voicemail\u2026</string>
+
+ <!-- Message to display if we fail to get content within a suitable time period. [CHAR LIMIT=40] -->
+ <string name="voicemail_fetching_timout">Couldn\'t load voicemail</string>
+
+ <!-- The header to show that call log is only showing voicemail calls. [CHAR LIMIT=40] -->
+ <string name="call_log_voicemail_header">Calls with voicemail only</string>
+
+ <!-- The header to show that call log is only showing incoming calls. [CHAR LIMIT=40] -->
+ <string name="call_log_incoming_header">Incoming calls only</string>
+
+ <!-- The header to show that call log is only showing outgoing calls. [CHAR LIMIT=40] -->
+ <string name="call_log_outgoing_header">Outgoing calls only</string>
+
+ <!-- The header to show that call log is only showing missed calls. [CHAR LIMIT=40] -->
+ <string name="call_log_missed_header">Missed calls only</string>
+
+ <!-- The counter for calls in a group and the date of the latest call as shown in the call log [CHAR LIMIT=15] -->
+ <string name="call_log_item_count_and_date">(<xliff:g id="count">%1$d</xliff:g>)
+ <xliff:g id="date">%2$s</xliff:g>
+ </string>
+
+ <!-- String describing the Search ImageButton
+
+ Used by AccessibilityService to announce the purpose of the button.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_search_button">search</string>
+
+ <!-- String describing the Dial ImageButton
+
+ Used by AccessibilityService to announce the purpose of the button.
+ -->
+ <string name="description_dial_button">dial</string>
+
+ <!-- String describing the digits text box containing the number to dial.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_digits_edittext">number to dial</string>
+
+ <!-- String describing the button in the voicemail playback to start/stop playback.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_playback_start_stop">Play or stop playback</string>
+
+ <!-- String describing the button in the voicemail playback to switch on/off speakerphone.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_playback_speakerphone">Switch on or off speakerphone</string>
+
+ <!-- String describing the seekbar in the voicemail playback to seek playback position.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_playback_seek">Seek playback position</string>
+
+ <!-- String describing the button in the voicemail playback to decrease playback rate.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_rate_decrease">Decrease playback rate</string>
+
+ <!-- String describing the button in the voicemail playback to increase playback rate.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_rate_increase">Increase playback rate</string>
+
+ <!-- Content description for the fake action menu button that brings up the call history
+ activity -->
+ <string name="action_menu_call_history_description">Call history</string>
+
+ <!-- Content description for the fake action menu overflow button.
+ This should be same as the description for the real action menu
+ overflow button available in ActionBar.
+ [CHAR LIMIT=NONE] -->
+ <string msgid="2295659037509008453" name="action_menu_overflow_description">More options</string>
+
+ <!-- Content description for the button that displays the dialpad
+ [CHAR LIMIT=NONE] -->
+ <string name="action_menu_dialpad_button">key pad</string>
+
+ <!-- Menu item used to show only outgoing in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_outgoing_only">Show outgoing only</string>
+
+ <!-- Menu item used to show only incoming in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_incoming_only">Show incoming only</string>
+
+ <!-- Menu item used to show only missed in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_missed_only">Show missed only</string>
+
+ <!-- Menu item used to show only voicemails in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_voicemails_only">Show voicemails only</string>
+
+ <!-- Menu item used to show all calls in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_all_calls">Show all calls</string>
+
+ <!-- Menu items for dialpad options as part of Pause and Wait ftr [CHAR LIMIT=30] -->
+ <string name="add_2sec_pause">Add 2-sec pause</string>
+ <string name="add_wait">Add wait</string>
+
+ <!-- Label for the dialer app setting page [CHAR LIMIT=30]-->
+ <string name="dialer_settings_label">Settings</string>
+
+ <!-- Menu item to display all contacts [CHAR LIMIT=30] -->
+ <string name="menu_allContacts">All contacts</string>
+
+ <!-- Title bar for call detail screen -->
+ <string name="callDetailTitle">Call details</string>
+
+ <!-- Toast for call detail screen when couldn't read the requested details -->
+ <string name="toast_call_detail_error">Details not available</string>
+
+ <!-- Item label: jump to the in-call DTMF dialpad.
+ (Part of a list of options shown in the dialer when another call
+ is already in progress.) -->
+ <string name="dialer_useDtmfDialpad">Use touch tone keypad</string>
+
+ <!-- Item label: jump to the in-call UI.
+ (Part of a list of options shown in the dialer when another call
+ is already in progress.) -->
+ <string name="dialer_returnToInCallScreen">Return to call in progress</string>
+
+ <!-- Item label: use the Dialer's keypad to add another call.
+ (Part of a list of options shown in the dialer when another call
+ is already in progress.) -->
+ <string name="dialer_addAnotherCall">Add call</string>
+
+ <!-- Title for incoming call type. [CHAR LIMIT=40] -->
+ <string name="type_incoming">Incoming call</string>
+
+ <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] -->
+ <string name="type_incoming_pulled">Incoming call transferred to another device</string>
+
+ <!-- Title for outgoing call type. [CHAR LIMIT=40] -->
+ <string name="type_outgoing">Outgoing call</string>
+
+ <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] -->
+ <string name="type_outgoing_pulled">Outgoing call transferred to another device</string>
+
+ <!-- Title for missed call type. [CHAR LIMIT=40] -->
+ <string name="type_missed">Missed call</string>
+
+ <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_incoming_video">Incoming video call</string>
+
+ <!-- Title for incoming video call in call details screen which was transferred to another device.
+ [CHAR LIMIT=60] -->
+ <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string>
+
+ <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_outgoing_video">Outgoing video call</string>
+
+ <!-- Title for outgoing video call in call details screen which was transferred to another device.
+ [CHAR LIMIT=60] -->
+ <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string>
+
+ <!-- Title for missed video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_missed_video">Missed video call</string>
+
+ <!-- Title for voicemail details screen -->
+ <string name="type_voicemail">Voicemail</string>
+
+ <!-- Title for rejected call type. [CHAR LIMIT=40] -->
+ <string name="type_rejected">Declined call</string>
+
+ <!-- Title for blocked call type. [CHAR LIMIT=40] -->
+ <string name="type_blocked">Blocked call</string>
+
+ <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing
+ simultaneously on multiple devices, and the user answered it on a device other than the
+ current device. [CHAR LIMIT=60] -->
+ <string name="type_answered_elsewhere">Call answered on another device</string>
+
+ <!-- Description for incoming calls going to voice mail vs. not -->
+ <string name="actionIncomingCall">Incoming calls</string>
+
+ <!-- String describing the icon in the call log used to play a voicemail.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_call_log_play_button">Play voicemail</string>
+
+ <!-- String describing the button to view the contact for the current number.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_view_contact">View contact <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- String describing the button to call a number or contact.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_call">Call <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- String describing the button to access the contact details for a name or number.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_contact_details">Contact details for <xliff:g id="nameOrNumber">%1$s</xliff:g></string>
+
+ <!-- String describing the button to access the contact details for a name or number when the
+ when the number is a suspected spam.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_spam_contact_details">Contact details for suspected spam caller <xliff:g id="nameOrNumber">%1$s</xliff:g></string>
+
+ <!-- String indicating the number of calls to/from a caller in the call log.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_num_calls"><xliff:g id="numberOfCalls">%1$s</xliff:g> calls.</string>
+
+ <!-- String indicating a call log entry had video capabilities.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_video_call">Video call.</string>
+
+ <!-- String describing the button to SMS a number or contact.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_send_text_message">Send SMS to <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- String describing the icon in the call log used to represent an unheard voicemail left to
+ the user.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_call_log_unheard_voicemail">Unheard voicemail</string>
+
+ <!-- String describing the icon used to start a voice search -->
+ <string name="description_start_voice_search">Start voice search</string>
+
+ <!-- Menu item used to call a contact, containing the number of the contact to call -->
+ <string name="menu_callNumber">Call <xliff:g id="number">%s</xliff:g></string>
+
+ <!-- String used for displaying calls to the voicemail number in the call log -->
+ <string name="voicemail">Voicemail</string>
+
+ <!-- A nicely formatted call duration displayed when viewing call details for duration less than 1 minute. For example "28 sec" -->
+ <string name="callDetailsShortDurationFormat"><xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+
+ <!-- A nicely formatted call duration displayed when viewing call details. For example "42 min 28 sec" -->
+ <string name="callDetailsDurationFormat"><xliff:g example="42" id="minutes">%s</xliff:g> min <xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+
+ <!-- The string 'Today'. This value is used in the voicemailCallLogDateTimeFormat rather than an
+ explicit date string, e.g. Jul 25, 2014, in the event that a voicemail was created on the
+ current day -->
+ <string name="voicemailCallLogToday">@string/call_log_header_today</string>
+
+ <!-- A format string used for displaying the date and time for a voicemail call log. For example: Jul 25, 2014 at 2:49 PM
+ The date will be replaced by 'Today' for voicemails created on the current day. For example: Today at 2:49 PM -->
+ <string name="voicemailCallLogDateTimeFormat"><xliff:g example="Jul 25, 2014" id="date">%1$s</xliff:g> at <xliff:g example="2:49 PM" id="time">%2$s</xliff:g></string>
+
+ <!-- Format for duration of voicemails which are displayed when viewing voicemail logs. For example "01:22" -->
+ <string name="voicemailDurationFormat"><xliff:g example="10" id="minutes">%1$02d</xliff:g>:<xliff:g example="20" id="seconds">%2$02d</xliff:g></string>
+
+ <!-- A format string used for displaying the date, time and duration for a voicemail call log. For example: Jul 25, 2014 at 2:49 PM • 00:34 -->
+ <string name="voicemailCallLogDateTimeFormatWithDuration"><xliff:g example="Jul 25, 2014 at 2:49PM" id="dateAndTime">%1$s</xliff:g> \u2022 <xliff:g example="01:22" id="duration">%2$s</xliff:g></string>
+
+ <!-- Dialog message which is shown when the user tries to make a phone call
+ to prohibited phone numbers [CHAR LIMIT=NONE] -->
+ <string msgid="4313552620858880999" name="dialog_phone_call_prohibited_message">Can\'t call this number</string>
+
+ <!-- Dialog message which is shown when the user tries to check voicemail
+ while the system isn't ready for the access. [CHAR LIMIT=NONE] -->
+ <string name="dialog_voicemail_not_ready_message">To set up voicemail, go to Menu > Settings.</string>
+
+ <!-- Dialog message which is shown when the user tries to check voicemail
+ while the system is in airplane mode. The user cannot access to
+ voicemail service in Airplane mode. [CHAR LIMI=NONE] -->
+ <string name="dialog_voicemail_airplane_mode_message">To call voicemail, first turn off Airplane mode.</string>
+
+ <!-- Message that appears in the favorites tab of the Phone app when the contact list has not fully loaded yet (below the favorite and frequent contacts) [CHAR LIMIT=20] -->
+ <string name="contact_list_loading">Loading\u2026</string>
+
+ <!-- The title of a dialog that displays the IMEI of the phone -->
+ <string name="imei">IMEI</string>
+
+ <!-- The title of a dialog that displays the MEID of the CDMA phone -->
+ <string name="meid">MEID</string>
+
+ <!-- Dialog text displayed when loading a phone number from the SIM card for speed dial -->
+ <string name="simContacts_emptyLoading">Loading from SIM card\u2026</string>
+
+ <!-- Dialog title displayed when loading a phone number from the SIM card for speed dial -->
+ <string name="simContacts_title">SIM card contacts</string>
+
+ <!-- Message displayed when there is no application available to handle the add contact menu option. [CHAR LIMIT=NONE] -->
+ <string name="add_contact_not_available">No contacts app available</string>
+
+ <!-- Message displayed when there is no application available to handle voice search. [CHAR LIMIT=NONE] -->
+ <string name="voice_search_not_available">Voice search not available</string>
+
+ <!-- Message displayed when the Phone application has been disabled and a phone call cannot
+ be made. [CHAR LIMIT=NONE] -->
+ <string name="call_not_available">Cannot make a phone call because the Phone application has been disabled.</string>
+
+ <!-- Hint displayed in dialer search box when there is no query that is currently typed.
+ [CHAR LIMIT=30] -->
+ <string name="dialer_hint_find_contact">Search contacts</string>
+
+ <!-- Hint displayed in add blocked number search box when there is no query typed.
+ [CHAR LIMIT=45] -->
+ <string name="block_number_search_hint">Add number or search contacts</string>
+
+ <!-- String resource for the font-family to use for the call log activity's title -->
+ <string name="call_log_activity_title_font_family" translatable="false">sans-serif-light</string>
+
+ <!-- String resource for the font-family to use for the full call history footer -->
+ <string name="view_full_call_history_font_family" translatable="false">sans-serif</string>
+
+ <!-- Text displayed when the call log is empty. -->
+ <string name="call_log_all_empty">Your call history is empty</string>
+
+ <!-- Label of the button displayed when the call history is empty. Allows the user to make a call. -->
+ <string name="call_log_all_empty_action">Make a call</string>
+
+ <!-- Text displayed when the list of missed calls is empty -->
+ <string name="call_log_missed_empty">You have no missed calls.</string>
+
+ <!-- Text displayed when the list of voicemails is empty -->
+ <string name="call_log_voicemail_empty">Your voicemail inbox is empty.</string>
+
+ <!-- Menu option to show favorite contacts only -->
+ <string name="show_favorites_only">Show favorites only</string>
+
+ <!-- Title of activity that displays a list of all calls -->
+ <string name="call_log_activity_title">Call History</string>
+
+ <!-- Title for the call log tab containing the list of all voicemails and calls
+ [CHAR LIMIT=30] -->
+ <string name="call_log_all_title">All</string>
+
+ <!-- Title for the call log tab containing the list of all missed calls only
+ [CHAR LIMIT=30] -->
+ <string name="call_log_missed_title">Missed</string>
+
+ <!-- Title for the call log tab containing the list of all voicemail calls only
+ [CHAR LIMIT=30] -->
+ <string name="call_log_voicemail_title">Voicemail</string>
+
+ <!-- Accessibility text for the tab showing recent and favorite contacts who can be called.
+ [CHAR LIMIT=40] -->
+ <string name="tab_speed_dial">Speed dial</string>
+
+ <!-- Accessibility text for the tab showing the call history. [CHAR LIMIT=40] -->
+ <string name="tab_history">Call History</string>
+
+ <!-- Accessibility text for the tab showing the user's contacts. [CHAR LIMIT=40] -->
+ <string name="tab_all_contacts">Contacts</string>
+
+ <!-- Accessibility text for the tab showing the user's voicemails. [CHAR LIMIT=40] -->
+ <string name="tab_voicemail">Voicemail</string>
+
+ <!-- Text displayed when user swipes out a favorite contact -->
+ <string name="favorite_hidden">Removed from favorites</string>
+ <!-- Text displayed for the undo button to undo removing a favorite contact -->
+ <string name="favorite_hidden_undo">Undo</string>
+
+ <!-- Shortcut item used to call a number directly from search -->
+ <string name="search_shortcut_call_number">Call
+ <xliff:g id="number">%s</xliff:g>
+ </string>
+
+ <!-- Shortcut item used to add a number directly to a new contact from search.
+ [CHAR LIMIT=25] -->
+ <string name="search_shortcut_create_new_contact">Create new contact</string>
+
+ <!-- Shortcut item used to add a number to an existing contact directly from search.
+ [CHAR LIMIT=25] -->
+ <string name="search_shortcut_add_to_contact">Add to a contact</string>
+
+ <!-- Shortcut item used to send a text message directly from search. [CHAR LIMIT=25] -->
+ <string name="search_shortcut_send_sms_message">Send SMS</string>
+
+ <!-- Shortcut item used to make a video call directly from search. [CHAR LIMIT=25] -->
+ <string name="search_shortcut_make_video_call">Make video call</string>
+
+ <!-- Shortcut item used to block a number directly from search. [CHAR LIMIT=25] -->
+ <string name="search_shortcut_block_number">Block number</string>
+
+ <!-- Number of missed calls shown on call card [CHAR LIMIT=40] -->
+ <string name="num_missed_calls"><xliff:g id="number">%s</xliff:g> new missed calls</string>
+
+ <!-- Shown when there are no speed dial favorites. -->
+ <string name="speed_dial_empty">No one is on your speed dial yet</string>
+
+ <!-- Shown as an action when there are no speed dial favorites -->
+ <string name="speed_dial_empty_add_favorite_action">Add a favorite</string>
+
+ <!-- Shown when there are no contacts in the all contacts list. -->
+ <string name="all_contacts_empty">You don\'t have any contacts yet</string>
+
+ <!-- Shown as an action when the all contacts list is empty -->
+ <string name="all_contacts_empty_add_contact_action">Add a contact</string>
+
+ <!-- Shows up as a tooltip to provide a hint to the user that the profile pic in a contact
+ card can be tapped to bring up a list of all numbers, or long pressed to start reordering
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="contact_tooltip">Touch image to see all numbers or touch & hold to reorder</string>
+
+ <!-- Remove button that shows up when contact is long-pressed. [CHAR LIMIT=NONE] -->
+ <string name="remove_contact">Remove</string>
+
+ <!-- Button text for the "video call" displayed underneath an entry in the call log.
+ Tapping causes a video call to be placed to the caller represented by the call log entry.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_video_call">Video call</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which opens up a
+ messaging app to send a SMS to the number represented by the call log entry.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_send_message">Send a message</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which opens up the
+ call compose UI for the number represented by the call log entry.
+ [CHAR LIMIT=30] -->
+ <string name="share_and_call">Share and call</string>
+
+ <!-- Button text for the button displayed underneath an entry in the call log.
+ Tapping navigates the user to the call details screen where the user can view details for
+ the call log entry. [CHAR LIMIT=30] -->
+ <string name="call_log_action_details">Call details</string>
+
+ <!-- Button text for the button displayed underneath an entry in the call log.
+ Tapping opens dialog to share voicemail archive with other apps. [CHAR LIMIT=30] -->
+ <string name="call_log_action_share_voicemail">Send to …</string>
+
+ <!-- Button text for the button displayed underneath an entry in the call log, which when
+ tapped triggers a return call to the named user. [CHAR LIMIT=30] -->
+ <string name="call_log_action_call">
+ Call <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing an incoming missed call entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_incoming_missed_call">Missed call from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing an incoming answered call entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_incoming_answered_call">Answered call from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing an "unread" voicemail entry in the voicemails tab.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_unread_voicemail">Unread voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing a "read" voicemail entry in the voicemails tab.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_read_voicemail">Voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing an outgoing call entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_outgoing_call">Call to <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing the phone account the call was made on or to. This string will be used
+ in description_incoming_missed_call, description_incoming_answered_call, and
+ description_outgoing_call.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string>
+
+ <!-- String describing the secondary line number the call was received via.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
+
+ <!-- TextView text item showing the secondary line number the call was received via.
+ [CHAR LIMIT=NONE]-->
+ <string name="call_log_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
+
+ <!-- String describing the PhoneAccount and via number that a call was received on, if both are
+ visible.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
+
+ <!-- The order of the PhoneAccount and via number that a call was received on,
+ if both are visible.
+ [CHAR LIMIT=NONE]-->
+ <string name="call_log_via_number_phone_account"><xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g> via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
+
+ <!-- String describing the phone icon on a call log list item. When tapped, it will place a
+ call to the number represented by that call log entry. [CHAR LIMIT=NONE]-->
+ <string name="description_call_log_call_action">Call</string>
+
+ <!-- String describing the "call" action for an entry in the call log. The call back
+ action triggers a return call to the named user.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_call_action">
+ Call <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the "video call" action for an entry in the call log. The video call
+ action triggers a return video call to the named person/number.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_video_call_action">
+ Video call <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>.
+ </string>
+
+ <!-- String describing the "listen" action for an entry in the call log. The listen
+ action is shown for call log entries representing a voicemail message and this button
+ triggers playing back the voicemail.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_action">
+ Listen to voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the "play voicemail" action for an entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_play">
+ Play voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the "pause voicemail" action for an entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_pause">
+ Pause voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+
+ <!-- String describing the "delete voicemail" action for an entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_delete">
+ Delete voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the number of new voicemails, displayed as a number badge on a tab.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <plurals name="description_voicemail_unread">
+ <item quantity="one"><xliff:g id="count">%d</xliff:g> new voicemail</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> new voicemails</item>
+ </plurals>
+
+ <!-- Description for the "create new contact" action for an entry in the call log. This action
+ opens a screen for creating a new contact for this name or number. [CHAR LIMIT=NONE] -->
+ <string name="description_create_new_contact_action">
+ Create contact for <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- Description for the "add to existing contact" action for an entry in the call log. This
+ action opens a screen for adding this name or number to an existing contact.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_add_to_existing_contact_action">
+ Add <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g> to existing contact
+ </string>
+
+ <!-- String describing the "details" action for an entry in the call log. The details action
+ displays the call details screen for an entry in the call log. This shows the calls to
+ and from the specified number associated with the call log entry.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_details_action">
+ Call details for <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- Toast message which appears when a call log entry is deleted.
+ [CHAR LIMIT=NONE] -->
+ <string name="toast_entry_removed">Deleted from call history</string>
+
+ <!-- String used as a header in the call log above calls which occurred today.
+ [CHAR LIMIT=65] -->
+ <string name="call_log_header_today">Today</string>
+
+ <!-- String used as a header in the call log above calls which occurred yesterday.
+ [CHAR LIMIT=65] -->
+ <string name="call_log_header_yesterday">Yesterday</string>
+
+ <!-- String used as a header in the call log above calls which occurred two days or more ago.
+ [CHAR LIMIT=65] -->
+ <string name="call_log_header_other">Older</string>
+
+ <!-- String a header on the call details screen. Appears above the list calls to or from a
+ particular number.
+ [CHAR LIMIT=65] -->
+ <string name="call_detail_list_header">Calls list</string>
+
+ <!-- String describing the "speaker on" button on the playback control used to listen to a
+ voicemail message. When speaker is on, playback of the voicemail will occur through the
+ phone speaker.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_speaker_on">Turn speaker on.</string>
+
+ <!-- String describing the "speaker off" button on the playback control used to listen to a
+ voicemail message. When speaker is off, playback of the voicemail will occur through the
+ phone earpiece.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_speaker_off">Turn speaker off.</string>
+
+ <!-- String describing the "play faster" button in the playback control used to listen to a
+ voicemail message. Speeds up playback of the voicemail message.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_play_faster">Play faster.</string>
+
+ <!-- String describing the "play slower" button in the playback control used to listen to a
+ voicemail message. Slows down playback of the voicemail message.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_play_slower">Play slower.</string>
+
+ <!-- String describing the "play/pause" button in the playback control used to listen to a
+ voicemail message. Starts playback or pauses ongoing playback.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_play_start_pause">Start or pause playback.</string>
+
+ <!-- Dialer settings related strings-->
+
+ <!-- Title for "Display options" category, which controls how contacts are shown.
+ [CHAR LIMIT=40] -->
+ <string name="display_options_title">Display options</string>
+
+ <!-- Title for the "Sounds and vibration" settings control settings related to ringtones,
+ dialpad tones, and vibration for incoming calls. [CHAR LIMIT=40] -->
+ <string name="sounds_and_vibration_title">Sounds and vibration</string>
+
+ <!-- Title for "Accessibility" category, which controls settings such as TTY mode and hearing
+ aid compatability. [CHAR LIMIT=40] -->
+ <string name="accessibility_settings_title">Accessibility</string>
+
+ <!-- Setting option name to pick ringtone (a list dialog comes up). [CHAR LIMIT=30] -->
+ <string name="ringtone_title">Phone ringtone</string>
+
+ <!-- Setting option name to enable or disable vibration when ringing the phone.
+ [CHAR LIMIT=30] -->
+ <string name="vibrate_on_ring_title">"Also vibrate for calls</string>
+
+ <!-- Setting option name to enable or disable DTMF tone sound [CHAR LIMIT=30] -->
+ <string name="dtmf_tone_enable_title">Keypad tones</string>
+ <!-- Label for setting to adjust the length of DTMF tone sounds. [CHAR LIMIT=40] -->
+ <string name="dtmf_tone_length_title">Keypad tone length</string>
+ <!-- Options displayed for the length of DTMF tone sounds. [CHAR LIMIT=40] -->
+ <string-array name="dtmf_tone_length_entries">
+ <item>Normal</item>
+ <item>Long</item>
+ </string-array>
+ <string-array name="dtmf_tone_length_entry_values" translatable="false">
+ <item>0</item>
+ <item>1</item>
+ </string-array>
+
+ <!-- Title of settings screen for managing the "Respond via SMS" feature. [CHAR LIMIT=30] -->
+ <string name="respond_via_sms_setting_title">Quick responses</string>
+
+ <!-- Label for the call settings section [CHAR LIMIT=30] -->
+ <string name="call_settings_label">Calls</string>
+
+ <!-- Label for the blocked numbers settings section [CHAR LIMIT=30] -->
+ <string name="manage_blocked_numbers_label">Call blocking</string>
+
+ <!-- Label for a section describing that call blocking is temporarily disabled because an
+ emergency call was made. [CHAR LIMIT=50] -->
+ <string name="blocked_numbers_disabled_emergency_header_label">
+ Call blocking temporarily off
+ </string>
+
+ <!-- Description that call blocking is temporarily disabled because the user called an
+ emergency number, and explains that call blocking will be re-enabled after a buffer
+ period has passed. [CHAR LIMIT=NONE] -->
+ <string name="blocked_numbers_disabled_emergency_desc">
+ Call blocking has been disabled because you contacted emergency services from this phone
+ within the last 48 hours. It will be automatically reenabled once the 48 hour period
+ expires.
+ </string>
+
+ <!-- Label for fragment to import numbers from contacts marked as send to voicemail.
+ [CHAR_LIMIT=30] -->
+ <string name="import_send_to_voicemail_numbers_label">Import numbers</string>
+
+ <!-- Text informing the user they have previously marked contacts to be sent to voicemail.
+ This will be followed by two buttons, 1) to view who is marked to be sent to voicemail
+ and 2) importing these settings to Dialer's block list. [CHAR LIMIT=NONE] -->
+ <string name="blocked_call_settings_import_description">
+ You previously marked some callers to be automatically sent to voicemail via other apps.
+ </string>
+
+ <!-- Label for button to view numbers of contacts previous marked to be sent to voicemail.
+ [CHAR_LIMIT=20] -->
+ <string name="blocked_call_settings_view_numbers_button">View Numbers</string>
+
+ <!-- Label for button to import settings for sending contacts to voicemail into Dialer's block
+ list. [CHAR_LIMIT=20] -->
+ <string name="blocked_call_settings_import_button">Import</string>
+
+ <!-- String describing the delete icon on a blocked number list item.
+ When tapped, it will show a dialog confirming the unblocking of the number.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_blocked_number_list_delete">Unblock number</string>
+
+ <!-- Button to bring up UI to add a number to the blocked call list. [CHAR LIMIT=40] -->
+ <string name="addBlockedNumber">Add number</string>
+
+ <!-- Footer message of number blocking screen with visual voicemail active.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_footer_message_vvm">
+ Calls from these numbers will be blocked and voicemails will be automatically deleted.
+ </string>
+
+ <!-- Footer message of number blocking screen with no visual voicemail.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_footer_message_no_vvm">
+ Calls from these numbers will be blocked, but they may still be able to leave you voicemails.
+ </string>
+
+ <!-- Heading for the block list in the "Spam and blocked cal)ls" settings. [CHAR LIMIT=64] -->
+ <string name="block_list">Blocked numbers</string>
+
+ <!-- Error message shown when user tries to add a number to the block list that was already
+ blocked. [CHAR LIMIT=64] -->
+ <string name="alreadyBlocked"><xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>
+ is already blocked.</string>
+
+ <!-- Label for the phone account settings [CHAR LIMIT=30] -->
+ <string name="phone_account_settings_label">Calling accounts</string>
+
+ <!-- Internal key for ringtone preference. -->
+ <string name="ringtone_preference_key" translatable="false">button_ringtone_key</string>
+ <!-- Internal key for vibrate when ringing preference. -->
+ <string name="vibrate_on_preference_key" translatable="false">button_vibrate_on_ring</string>
+ <!-- Internal key for vibrate when ringing preference. -->
+ <string name="play_dtmf_preference_key" translatable="false">button_play_dtmf_tone</string>
+ <!-- Internal key for DTMF tone length preference. -->
+ <string name="dtmf_tone_length_preference_key" translatable="false">button_dtmf_settings</string>
+
+ <!-- The label of the button used to turn on a single permission [CHAR LIMIT=30]-->
+ <string name="permission_single_turn_on">Turn on</string>
+
+ <!-- The label of the button used to turn on multiple permissions [CHAR LIMIT=30]-->
+ <string name="permission_multiple_turn_on">Set permissions</string>
+
+ <!-- Shown as a prompt to turn on the contacts permission to enable speed dial [CHAR LIMIT=NONE]-->
+ <string name="permission_no_speeddial">To enable speed dial, turn on the Contacts permission.</string>
+
+ <!-- Shown as a prompt to turn on the phone permission to enable the call log [CHAR LIMIT=NONE]-->
+ <string name="permission_no_calllog">To see your call log, turn on the Phone permission.</string>
+
+ <!-- Shown as a prompt to turn on the contacts permission to show all contacts [CHAR LIMIT=NONE]-->
+ <string name="permission_no_contacts">To see your contacts, turn on the Contacts permission.</string>
+
+ <!-- Shown as a prompt to turn on the phone permission to show voicemails [CHAR LIMIT=NONE]-->
+ <string name="permission_no_voicemail">To access your voicemail, turn on the Phone permission.</string>
+
+ <!-- Shown as a prompt to turn on contacts permissions to allow contact search [CHAR LIMIT=NONE]-->
+ <string name="permission_no_search">To search your contacts, turn on the Contacts permissions.</string>
+
+ <!-- Shown as a prompt to turn on the phone permission to allow a call to be placed [CHAR LIMIT=NONE]-->
+ <string name="permission_place_call">To place a call, turn on the Phone permission.</string>
+
+ <!-- Shown as a message that notifies the user that the Phone app cannot write to system settings, which is why the system settings app is being launched directly instead. [CHAR LIMIT=NONE]-->
+ <string name="toast_cannot_write_system_settings">Phone app does not have permission to write to system settings.</string>
+
+ <!-- Label under the name of a blocked number in the call log. [CHAR LIMIT=15] -->
+ <string name="blocked_number_call_log_label">Blocked</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which marks the
+ phone number represented by the call log entry as a Spam number.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_block_report_number">Block/report spam</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which marks the
+ phone number represented by the call log entry as a Spam number.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_block_number">Block number</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which removes the
+ phone number represented by the call log entry from the Spam numbers list.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_remove_spam">Not spam</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which removes the
+ phone number represented by the call log entry from the blacklisted numbers.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_unblock_number">Unblock number</string>
+
+ <!-- Label under the name of a spam number in the call log. [CHAR LIMIT=15] -->
+ <string name="spam_number_call_log_label">Spam</string>
+
+ <!-- Shown as a message that notifies the user enriched calling isn't working -->
+ <string name="call_composer_connection_failed"><xliff:g id="feature">%1$s</xliff:g> unavailable right now</string>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml
new file mode 100644
index 0000000..ac4422b
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/styles.xml
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <style name="DialtactsTheme" parent="DialerThemeBase">
+
+ <!-- Style for the overflow button in the actionbar. -->
+ <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflow</item>
+ <item name="actionOverflowButtonStyle">@style/DialtactsActionBarOverflow</item>
+
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:windowActionBarOverlay">true</item>
+ <item name="windowActionBarOverlay">true</item>
+ <item name="android:windowActionModeOverlay">true</item>
+ <item name="windowActionModeOverlay">true</item>
+ <item name="android:actionBarStyle">@style/DialtactsActionBarStyle</item>
+ <item name="actionBarStyle">@style/DialtactsActionBarStyle</item>
+
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:overlapAnchor">true</item>
+ <item name="android:homeAsUpIndicator">@drawable/ic_back_arrow</item>
+
+ <item name="android:listViewStyle">@style/ListViewStyle</item>
+ <item name="activated_background">@drawable/list_item_activated_background</item>
+ <item name="section_header_background">@drawable/list_title_holo</item>
+ <item name="list_section_header_height">32dip</item>
+ <item name="list_item_padding_top">7dp</item>
+ <item name="list_item_padding_right">24dp</item>
+ <item name="list_item_padding_bottom">7dp</item>
+ <item name="list_item_padding_left">16dp</item>
+ <item name="list_item_gap_between_image_and_text">
+ @dimen/contact_browser_list_item_gap_between_image_and_text
+ </item>
+ <item name="list_item_gap_between_label_and_data">8dip</item>
+ <item name="list_item_presence_icon_margin">4dip</item>
+ <item name="list_item_presence_icon_size">16dip</item>
+ <item name="list_item_photo_size">@dimen/contact_browser_list_item_photo_size</item>
+ <item name="list_item_profile_photo_size">70dip</item>
+ <item name="list_item_prefix_highlight_color">@color/people_app_theme_color</item>
+ <item name="list_item_background_color">@color/background_dialer_light</item>
+ <item name="list_item_header_text_indent">8dip</item>
+ <item name="list_item_header_text_color">@color/dialer_secondary_text_color</item>
+ <item name="list_item_header_text_size">14sp</item>
+ <item name="list_item_header_height">30dip</item>
+ <item name="list_item_data_width_weight">5</item>
+ <item name="list_item_label_width_weight">3</item>
+ <item name="contact_browser_list_padding_left">0dp</item>
+ <item name="contact_browser_list_padding_right">0dp</item>
+ <item name="contact_browser_background">@color/background_dialer_results</item>
+ <item name="list_item_name_text_color">@color/contact_list_name_text_color</item>
+ <item name="list_item_name_text_size">16sp</item>
+ <item name="list_item_text_indent">@dimen/contact_browser_list_item_text_indent</item>
+ <item name="list_item_text_offset_top">-2dp</item>
+ <!-- Favorites -->
+ <item name="favorites_padding_bottom">?android:attr/actionBarSize</item>
+ <item name="dialpad_key_button_touch_tint">@color/dialer_dialpad_touch_tint</item>
+ <item name="android:textAppearanceButton">@style/DialerButtonTextStyle</item>
+
+ <!-- Video call icon -->
+ <item name="list_item_video_call_icon_size">32dip</item>
+ <item name="list_item_video_call_icon_margin">8dip</item>
+
+ <item name="dialpad_style">@style/Dialpad.Light</item>
+ </style>
+
+ <!-- Action bar overflow menu icon. -->
+ <style name="DialtactsActionBarOverflow"
+ parent="@android:style/Widget.Material.Light.ActionButton.Overflow">
+ <item name="android:src">@drawable/ic_overflow_menu</item>
+ </style>
+
+ <!-- Action bar overflow menu icon. White with no shadow. -->
+ <style name="DialtactsActionBarOverflowWhite"
+ parent="@android:style/Widget.Material.Light.ActionButton.Overflow">
+ <item name="android:src">@drawable/overflow_menu</item>
+ </style>
+
+ <style name="DialpadTheme" parent="DialtactsTheme">
+ <item name="android:textColorPrimary">#FFFFFF</item>
+ </style>
+
+ <style name="DialtactsThemeWithoutActionBarOverlay" parent="DialtactsTheme">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:windowActionBarOverlay">false</item>
+ <item name="windowActionBarOverlay">false</item>
+ <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
+ <item name="actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
+ </style>
+
+ <!-- Hide the actionbar title during the activity preview -->
+ <style name="DialtactsActivityTheme" parent="DialtactsTheme">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:actionBarStyle">@style/DialtactsActionBarWithoutTitleStyle</item>
+ <item name="actionBarStyle">@style/DialtactsActionBarWithoutTitleStyle</item>
+
+ <item name="android:fastScrollThumbDrawable">@drawable/fastscroll_thumb</item>
+ <item name="android:fastScrollTrackDrawable">@null</item>
+ </style>
+
+ <style name="CallDetailActivityTheme" parent="DialtactsThemeWithoutActionBarOverlay">
+ <item name="android:windowBackground">@color/background_dialer_results</item>
+ <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
+ </style>
+
+ <style name="CallDetailActionItemStyle">
+ <item name="android:foreground">?android:attr/selectableItemBackground</item>
+ <item name="android:clickable">true</item>
+ <item name="android:drawablePadding">@dimen/call_detail_action_item_drawable_padding</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingStart">@dimen/call_detail_action_item_padding_horizontal</item>
+ <item name="android:paddingEnd">@dimen/call_detail_action_item_padding_horizontal</item>
+ <item name="android:paddingTop">@dimen/call_detail_action_item_padding_vertical</item>
+ <item name="android:paddingBottom">@dimen/call_detail_action_item_padding_vertical</item>
+ <item name="android:textColor">@color/call_detail_footer_text_color</item>
+ <item name="android:textSize">@dimen/call_detail_action_item_text_size</item>
+ </style>
+
+ <style name="DialtactsActionBarStyle" parent="DialerActionBarBaseStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:background">@color/actionbar_background_color</item>
+ <item name="background">@color/actionbar_background_color</item>
+ <item name="android:titleTextStyle">@style/DialtactsActionBarTitleText</item>
+ <item name="titleTextStyle">@style/DialtactsActionBarTitleText</item>
+ <item name="android:elevation">@dimen/action_bar_elevation</item>
+ <item name="elevation">@dimen/action_bar_elevation</item>
+ <!-- Empty icon -->
+ <item name="android:icon">@android:color/transparent</item>
+ <item name="icon">@android:color/transparent</item>
+ <!-- Shift the title text to the right -->
+ <item name="android:contentInsetStart">@dimen/actionbar_contentInsetStart</item>
+ <item name="contentInsetStart">@dimen/actionbar_contentInsetStart</item>
+ </style>
+
+ <style name="DialtactsActionBarWithoutTitleStyle" parent="DialtactsActionBarStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:displayOptions"></item>
+ <item name="displayOptions"></item>
+ <item name="android:height">@dimen/action_bar_height_large</item>
+ <item name="height">@dimen/action_bar_height_large</item>
+ <!-- Override ActionBar title offset to keep search box aligned left -->
+ <item name="android:contentInsetStart">0dp</item>
+ <item name="contentInsetStart">0dp</item>
+ <item name="android:contentInsetEnd">0dp</item>
+ <item name="contentInsetEnd">0dp</item>
+ </style>
+
+ <!-- Text in the action bar at the top of the screen -->
+ <style name="DialtactsActionBarTitleText"
+ parent="@android:style/TextAppearance.Material.Widget.ActionBar.Title">
+ <item name="android:textColor">@color/actionbar_text_color</item>
+ </style>
+
+ <!-- Text style for tabs. -->
+ <style name="DialtactsActionBarTabTextStyle"
+ parent="android:style/Widget.Material.Light.ActionBar.TabText">
+ <item name="android:textColor">@color/tab_text_color</item>
+ <item name="android:textSize">@dimen/tab_text_size</item>
+ <item name="android:fontFamily">"sans-serif-medium"</item>
+ </style>
+
+ <style name="CallLogActionStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/call_log_action_height</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:gravity">center_vertical</item>
+ </style>
+
+ <style name="CallLogActionTextStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingStart">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:textColor">@color/call_log_action_color</item>
+ <item name="android:textSize">@dimen/call_log_primary_text_size</item>
+ <item name="android:fontFamily">"sans-serif"</item>
+ <item name="android:focusable">true</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:importantForAccessibility">no</item>
+ </style>
+
+ <style name="CallLogActionSupportTextStyle" parent="@style/CallLogActionTextStyle">
+ <item name="android:textSize">@dimen/call_log_detail_text_size</item>
+ <item name="android:textColor">@color/call_log_detail_color</item>
+ </style>
+
+ <style name="CallLogActionIconStyle">
+ <item name="android:layout_width">@dimen/call_log_action_icon_dimen</item>
+ <item name="android:layout_height">@dimen/call_log_action_icon_dimen</item>
+ <item name="android:layout_marginStart">@dimen/call_log_action_icon_margin_start</item>
+ <item name="android:tint">?android:textColorSecondary</item>
+ <item name="android:importantForAccessibility">no</item>
+ </style>
+
+ <style name="DismissButtonStyle">
+ <item name="android:paddingLeft">@dimen/dismiss_button_padding_start</item>
+ <item name="android:paddingRight">@dimen/dismiss_button_padding_end</item>
+ </style>
+
+ <!-- Style applied to the "Settings" screen. Keep in sync with SettingsLight in Telephony. -->
+ <style name="SettingsStyle" parent="DialtactsThemeWithoutActionBarOverlay">
+ <!-- Setting text. -->
+ <item name="android:textColorPrimary">@color/settings_text_color_primary</item>
+ <!-- Setting description. -->
+ <item name="android:textColorSecondary">@color/settings_text_color_secondary</item>
+ <item name="android:windowBackground">@color/setting_background_color</item>
+ <item name="android:colorAccent">@color/dialtacts_theme_color</item>
+ <item name="android:textColorLink">@color/dialtacts_theme_color</item>
+ </style>
+
+ <style name="ManageBlockedNumbersStyle" parent="SettingsStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:windowActionBarOverlay">true</item>
+ <item name="windowActionBarOverlay">true</item>
+ <item name="android:actionBarStyle">@style/ManageBlockedNumbersActionBarStyle</item>
+ <item name="actionBarStyle">@style/ManageBlockedNumbersActionBarStyle</item>
+ <item name="android:fastScrollTrackDrawable">@null</item>
+ </style>
+
+ <style name="ManageBlockedNumbersActionBarStyle" parent="DialtactsActionBarWithoutTitleStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:height">@dimen/action_bar_height</item>
+ <item name="height">@dimen/action_bar_height</item>
+ </style>
+
+ <style name="VoicemailPlaybackLayoutTextStyle">
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="VoicemailPlaybackLayoutButtonStyle">
+ <item name="android:layout_width">56dp</item>
+ <item name="android:layout_height">56dp</item>
+ <item name="android:background">@drawable/oval_ripple</item>
+ <item name="android:padding">8dp</item>
+ </style>
+
+ <style name="DialerFlatButtonStyle" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:paddingEnd">@dimen/button_horizontal_padding</item>
+ <item name="android:paddingStart">@dimen/button_horizontal_padding</item>
+ <item name="android:textColor">@color/dialer_flat_button_text_color</item>
+ </style>
+
+ <!-- Style for the 'primary' button in a view. Unlike the DialerFlatButtonStyle, this button -->
+ <!-- is not colored white, to draw more attention to it. -->
+ <style name="DialerPrimaryFlatButtonStyle" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">@drawable/selectable_primary_flat_button</item>
+ <item name="android:paddingEnd">@dimen/button_horizontal_padding</item>
+ <item name="android:paddingStart">@dimen/button_horizontal_padding</item>
+ <item name="android:textColor">@android:color/white</item>
+ </style>
+
+ <style name="BlockedNumbersDescriptionTextStyle">
+ <item name="android:lineSpacingMultiplier">1.43</item>
+ <item name="android:paddingTop">8dp</item>
+ <item name="android:paddingBottom">8dp</item>
+ <item name="android:textSize">@dimen/blocked_number_settings_description_text_size</item>
+ </style>
+
+ <style name="FullWidthDivider">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">1dp</item>
+ <item name="android:background">?android:attr/listDivider</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/app/res/xml/display_options_settings.xml b/java/com/android/dialer/app/res/xml/display_options_settings.xml
new file mode 100644
index 0000000..0b4e11d
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/display_options_settings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <com.android.contacts.common.preference.SortOrderPreference
+ android:dialogTitle="@string/display_options_sort_list_by"
+ android:key="sortOrder"
+ android:title="@string/display_options_sort_list_by"/>
+
+ <com.android.contacts.common.preference.DisplayOrderPreference
+ android:dialogTitle="@string/display_options_view_names_as"
+ android:key="displayOrder"
+ android:title="@string/display_options_view_names_as"/>
+
+</PreferenceScreen>
diff --git a/java/com/android/dialer/app/res/xml/file_paths.xml b/java/com/android/dialer/app/res/xml/file_paths.xml
new file mode 100644
index 0000000..41522e4
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/file_paths.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<paths>
+ <!-- Offer access to files under Context.getCacheDir() -->
+ <cache-path name="my_cache"/>
+ <!-- Offer access to voicemail folder under Context.getFilesDir() -->
+ <files-path
+ name="voicemails"
+ path="voicemails/"/>
+</paths>
diff --git a/java/com/android/dialer/app/res/xml/searchable.xml b/java/com/android/dialer/app/res/xml/searchable.xml
new file mode 100644
index 0000000..0ea1685
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/searchable.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:hint="@string/dialer_hint_find_contact"
+ android:imeOptions="actionSearch"
+ android:inputType="textNoSuggestions"
+ android:label="@string/applicationLabel"
+ android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
+ />
\ No newline at end of file
diff --git a/java/com/android/dialer/app/res/xml/sound_settings.xml b/java/com/android/dialer/app/res/xml/sound_settings.xml
new file mode 100644
index 0000000..796ed2e
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/sound_settings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <com.android.dialer.app.settings.DefaultRingtonePreference
+ android:dialogTitle="@string/ringtone_title"
+ android:key="@string/ringtone_preference_key"
+ android:persistent="false"
+ android:ringtoneType="ringtone"
+ android:title="@string/ringtone_title"/>
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="@string/vibrate_on_preference_key"
+ android:persistent="false"
+ android:title="@string/vibrate_on_ring_title"/>
+
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="@string/play_dtmf_preference_key"
+ android:persistent="false"
+ android:title="@string/dtmf_tone_enable_title"/>
+
+ <ListPreference
+ android:entries="@array/dtmf_tone_length_entries"
+ android:entryValues="@array/dtmf_tone_length_entry_values"
+ android:key="@string/dtmf_tone_length_preference_key"
+ android:title="@string/dtmf_tone_length_title"/>
+
+</PreferenceScreen>
diff --git a/java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java b/java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java
new file mode 100644
index 0000000..2c46438
--- /dev/null
+++ b/java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2015 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.app.settings;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public class AppCompatPreferenceActivity extends PreferenceActivity {
+
+ private AppCompatDelegate mDelegate;
+
+ private boolean mIsSafeToCommitTransactions;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getDelegate().installViewFactory();
+ getDelegate().onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ getDelegate().onPostCreate(savedInstanceState);
+ }
+
+ public ActionBar getSupportActionBar() {
+ return getDelegate().getSupportActionBar();
+ }
+
+ public void setSupportActionBar(Toolbar toolbar) {
+ getDelegate().setSupportActionBar(toolbar);
+ }
+
+ @Override
+ public MenuInflater getMenuInflater() {
+ return getDelegate().getMenuInflater();
+ }
+
+ @Override
+ public void setContentView(int layoutResID) {
+ getDelegate().setContentView(layoutResID);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ getDelegate().setContentView(view);
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().setContentView(view, params);
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().addContentView(view, params);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ getDelegate().onPostResume();
+ }
+
+ @Override
+ protected void onTitleChanged(CharSequence title, int color) {
+ super.onTitleChanged(title, color);
+ getDelegate().setTitle(title);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ getDelegate().onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getDelegate().onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ getDelegate().onDestroy();
+ }
+
+ @Override
+ public void invalidateOptionsMenu() {
+ getDelegate().invalidateOptionsMenu();
+ }
+
+ private AppCompatDelegate getDelegate() {
+ if (mDelegate == null) {
+ mDelegate = AppCompatDelegate.create(this, null);
+ }
+ return mDelegate;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mIsSafeToCommitTransactions = false;
+ }
+
+ /**
+ * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+ * whether {@link Activity#onSaveInstanceState} has been called or not.
+ *
+ * <p>Make sure that the current activity calls into {@link super.onSaveInstanceState(Bundle
+ * outState)} (if that method is overridden), so the flag is properly set.
+ */
+ public boolean isSafeToCommitTransactions() {
+ return mIsSafeToCommitTransactions;
+ }
+}
diff --git a/java/com/android/dialer/app/settings/DefaultRingtonePreference.java b/java/com/android/dialer/app/settings/DefaultRingtonePreference.java
new file mode 100644
index 0000000..579584e
--- /dev/null
+++ b/java/com/android/dialer/app/settings/DefaultRingtonePreference.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.app.settings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.preference.RingtonePreference;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.widget.Toast;
+import com.android.dialer.app.R;
+
+/** RingtonePreference which doesn't show default ringtone setting. */
+public class DefaultRingtonePreference extends RingtonePreference {
+
+ public DefaultRingtonePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) {
+ super.onPrepareRingtonePickerIntent(ringtonePickerIntent);
+
+ /*
+ * Since this preference is for choosing the default ringtone, it
+ * doesn't make sense to show a 'Default' item.
+ */
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
+ }
+
+ @Override
+ protected void onSaveRingtone(Uri ringtoneUri) {
+ if (!Settings.System.canWrite(getContext())) {
+ Toast.makeText(
+ getContext(),
+ getContext().getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ RingtoneManager.setActualDefaultRingtoneUri(getContext(), getRingtoneType(), ringtoneUri);
+ }
+
+ @Override
+ protected Uri onRestoreRingtone() {
+ return RingtoneManager.getActualDefaultRingtoneUri(getContext(), getRingtoneType());
+ }
+}
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
new file mode 100644
index 0000000..b046740
--- /dev/null
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2013 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.app.settings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.view.MenuItem;
+import android.widget.Toast;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.proguard.UsedByReflection;
+import java.util.List;
+
+@UsedByReflection(value = "AndroidManifest-app.xml")
+public class DialerSettingsActivity extends AppCompatPreferenceActivity {
+
+ protected SharedPreferences mPreferences;
+ private boolean migrationStatusOnBuildHeaders;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ /*
+ * The blockedCallsHeader need to be recreated if the migration status changed because
+ * the intent needs to be updated.
+ */
+ if (migrationStatusOnBuildHeaders != FilteredNumberCompat.hasMigratedToNewBlocking(this)) {
+ invalidateHeaders();
+ }
+ }
+
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ if (showDisplayOptions()) {
+ Header displayOptionsHeader = new Header();
+ displayOptionsHeader.titleRes = R.string.display_options_title;
+ displayOptionsHeader.fragment = DisplayOptionsSettingsFragment.class.getName();
+ target.add(displayOptionsHeader);
+ }
+
+ Header soundSettingsHeader = new Header();
+ soundSettingsHeader.titleRes = R.string.sounds_and_vibration_title;
+ soundSettingsHeader.fragment = SoundSettingsFragment.class.getName();
+ soundSettingsHeader.id = R.id.settings_header_sounds_and_vibration;
+ target.add(soundSettingsHeader);
+
+ if (CompatUtils.isMarshmallowCompatible()) {
+ Header quickResponseSettingsHeader = new Header();
+ Intent quickResponseSettingsIntent =
+ new Intent(TelecomManager.ACTION_SHOW_RESPOND_VIA_SMS_SETTINGS);
+ quickResponseSettingsHeader.titleRes = R.string.respond_via_sms_setting_title;
+ quickResponseSettingsHeader.intent = quickResponseSettingsIntent;
+ target.add(quickResponseSettingsHeader);
+ }
+
+ TelephonyManager telephonyManager =
+ (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+
+ // "Call Settings" (full settings) is shown if the current user is primary user and there
+ // is only one SIM. Before N, "Calling accounts" setting is shown if the current user is
+ // primary user and there are multiple SIMs. In N+, "Calling accounts" is shown whenever
+ // "Call Settings" is not shown.
+ boolean isPrimaryUser = isPrimaryUser();
+ if (isPrimaryUser && TelephonyManagerCompat.getPhoneCount(telephonyManager) <= 1) {
+ Header callSettingsHeader = new Header();
+ Intent callSettingsIntent = new Intent(TelecomManager.ACTION_SHOW_CALL_SETTINGS);
+ callSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ callSettingsHeader.titleRes = R.string.call_settings_label;
+ callSettingsHeader.intent = callSettingsIntent;
+ target.add(callSettingsHeader);
+ } else if ((VERSION.SDK_INT >= VERSION_CODES.N) || isPrimaryUser) {
+ Header phoneAccountSettingsHeader = new Header();
+ Intent phoneAccountSettingsIntent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS);
+ phoneAccountSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ phoneAccountSettingsHeader.titleRes = R.string.phone_account_settings_label;
+ phoneAccountSettingsHeader.intent = phoneAccountSettingsIntent;
+ target.add(phoneAccountSettingsHeader);
+ }
+ if (FilteredNumberCompat.canCurrentUserOpenBlockSettings(this)) {
+ Header blockedCallsHeader = new Header();
+ blockedCallsHeader.titleRes = R.string.manage_blocked_numbers_label;
+ blockedCallsHeader.intent = FilteredNumberCompat.createManageBlockedNumbersIntent(this);
+ target.add(blockedCallsHeader);
+ migrationStatusOnBuildHeaders = FilteredNumberCompat.hasMigratedToNewBlocking(this);
+ }
+ if (isPrimaryUser
+ && (TelephonyManagerCompat.isTtyModeSupported(telephonyManager)
+ || TelephonyManagerCompat.isHearingAidCompatibilitySupported(telephonyManager))) {
+ Header accessibilitySettingsHeader = new Header();
+ Intent accessibilitySettingsIntent =
+ new Intent(TelecomManager.ACTION_SHOW_CALL_ACCESSIBILITY_SETTINGS);
+ accessibilitySettingsHeader.titleRes = R.string.accessibility_settings_title;
+ accessibilitySettingsHeader.intent = accessibilitySettingsIntent;
+ target.add(accessibilitySettingsHeader);
+ }
+ }
+
+ /**
+ * Returns {@code true} or {@code false} based on whether the display options setting should be
+ * shown. For languages such as Chinese, Japanese, or Korean, display options aren't useful since
+ * contacts are sorted and displayed family name first by default.
+ *
+ * @return {@code true} if the display options should be shown, {@code false} otherwise.
+ */
+ private boolean showDisplayOptions() {
+ return getResources().getBoolean(R.bool.config_display_order_user_changeable)
+ && getResources().getBoolean(R.bool.config_sort_order_user_changeable);
+ }
+
+ @Override
+ public void onHeaderClick(Header header, int position) {
+ if (header.id == R.id.settings_header_sounds_and_vibration) {
+ // If we don't have the permission to write to system settings, go to system sound
+ // settings instead. Otherwise, perform the super implementation (which launches our
+ // own preference fragment.
+ if (!Settings.System.canWrite(this)) {
+ Toast.makeText(
+ this,
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
+ return;
+ }
+ }
+ super.onHeaderClick(header, position);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!isSafeToCommitTransactions()) {
+ return;
+ }
+ super.onBackPressed();
+ }
+
+ @Override
+ protected boolean isValidFragment(String fragmentName) {
+ return true;
+ }
+
+ /** @return Whether the current user is the primary user. */
+ private boolean isPrimaryUser() {
+ return getSystemService(UserManager.class).isSystemUser();
+ }
+}
diff --git a/java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java b/java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java
new file mode 100644
index 0000000..bf1637f
--- /dev/null
+++ b/java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2015 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.app.settings;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import com.android.dialer.app.R;
+
+public class DisplayOptionsSettingsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.display_options_settings);
+ }
+}
diff --git a/java/com/android/dialer/app/settings/SoundSettingsFragment.java b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
new file mode 100644
index 0000000..83ce453
--- /dev/null
+++ b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2014 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.app.settings;
+
+import android.content.Context;
+import android.media.RingtoneManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.widget.Toast;
+import com.android.dialer.app.R;
+import com.android.dialer.compat.SdkVersionOverride;
+import com.android.dialer.util.SettingsUtil;
+
+public class SoundSettingsFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ private static final int NO_DTMF_TONE = 0;
+ private static final int PLAY_DTMF_TONE = 1;
+
+ private static final int NO_VIBRATION_FOR_CALLS = 0;
+ private static final int DO_VIBRATION_FOR_CALLS = 1;
+
+ private static final int DTMF_TONE_TYPE_NORMAL = 0;
+
+ private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1;
+
+ private Preference mRingtonePreference;
+ private final Handler mRingtoneLookupComplete =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_RINGTONE_SUMMARY:
+ mRingtonePreference.setSummary((CharSequence) msg.obj);
+ break;
+ }
+ }
+ };
+ private final Runnable mRingtoneLookupRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ updateRingtonePreferenceSummary();
+ }
+ };
+ private CheckBoxPreference mVibrateWhenRinging;
+ private CheckBoxPreference mPlayDtmfTone;
+ private ListPreference mDtmfToneLength;
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.sound_settings);
+
+ Context context = getActivity();
+
+ mRingtonePreference = findPreference(context.getString(R.string.ringtone_preference_key));
+ mVibrateWhenRinging =
+ (CheckBoxPreference) findPreference(context.getString(R.string.vibrate_on_preference_key));
+ mPlayDtmfTone =
+ (CheckBoxPreference) findPreference(context.getString(R.string.play_dtmf_preference_key));
+ mDtmfToneLength =
+ (ListPreference)
+ findPreference(context.getString(R.string.dtmf_tone_length_preference_key));
+
+ if (hasVibrator()) {
+ mVibrateWhenRinging.setOnPreferenceChangeListener(this);
+ } else {
+ getPreferenceScreen().removePreference(mVibrateWhenRinging);
+ mVibrateWhenRinging = null;
+ }
+
+ mPlayDtmfTone.setOnPreferenceChangeListener(this);
+ mPlayDtmfTone.setChecked(shouldPlayDtmfTone());
+
+ TelephonyManager telephonyManager =
+ (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+ if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M
+ && telephonyManager.canChangeDtmfToneLength()
+ && (telephonyManager.isWorldPhone() || !shouldHideCarrierSettings())) {
+ mDtmfToneLength.setOnPreferenceChangeListener(this);
+ mDtmfToneLength.setValueIndex(
+ Settings.System.getInt(
+ context.getContentResolver(),
+ Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
+ DTMF_TONE_TYPE_NORMAL));
+ } else {
+ getPreferenceScreen().removePreference(mDtmfToneLength);
+ mDtmfToneLength = null;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!Settings.System.canWrite(getContext())) {
+ // If the user launches this setting fragment, then toggles the WRITE_SYSTEM_SETTINGS
+ // AppOp, then close the fragment since there is nothing useful to do.
+ getActivity().onBackPressed();
+ return;
+ }
+
+ if (mVibrateWhenRinging != null) {
+ mVibrateWhenRinging.setChecked(shouldVibrateWhenRinging());
+ }
+
+ // Lookup the ringtone name asynchronously.
+ new Thread(mRingtoneLookupRunnable).start();
+ }
+
+ /**
+ * Supports onPreferenceChangeListener to look for preference changes.
+ *
+ * @param preference The preference to be changed
+ * @param objValue The value of the selection, NOT its localized display value.
+ */
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ if (!Settings.System.canWrite(getContext())) {
+ // A user shouldn't be able to get here, but this protects against monkey crashes.
+ Toast.makeText(
+ getContext(),
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return true;
+ }
+ if (preference == mVibrateWhenRinging) {
+ boolean doVibrate = (Boolean) objValue;
+ Settings.System.putInt(
+ getActivity().getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING,
+ doVibrate ? DO_VIBRATION_FOR_CALLS : NO_VIBRATION_FOR_CALLS);
+ } else if (preference == mDtmfToneLength) {
+ int index = mDtmfToneLength.findIndexOfValue((String) objValue);
+ Settings.System.putInt(
+ getActivity().getContentResolver(), Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, index);
+ }
+ return true;
+ }
+
+ /** Click listener for toggle events. */
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ if (!Settings.System.canWrite(getContext())) {
+ Toast.makeText(
+ getContext(),
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return true;
+ }
+ if (preference == mPlayDtmfTone) {
+ Settings.System.putInt(
+ getActivity().getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING,
+ mPlayDtmfTone.isChecked() ? PLAY_DTMF_TONE : NO_DTMF_TONE);
+ }
+ return true;
+ }
+
+ /** Updates the summary text on the ringtone preference with the name of the ringtone. */
+ private void updateRingtonePreferenceSummary() {
+ SettingsUtil.updateRingtoneName(
+ getActivity(),
+ mRingtoneLookupComplete,
+ RingtoneManager.TYPE_RINGTONE,
+ mRingtonePreference.getKey(),
+ MSG_UPDATE_RINGTONE_SUMMARY);
+ }
+
+ /**
+ * Obtain the value for "vibrate when ringing" setting. The default value is false.
+ *
+ * <p>Watch out: if the setting is missing in the device, this will try obtaining the old "vibrate
+ * on ring" setting from AudioManager, and save the previous setting to the new one.
+ */
+ private boolean shouldVibrateWhenRinging() {
+ int vibrateWhenRingingSetting =
+ Settings.System.getInt(
+ getActivity().getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING,
+ NO_VIBRATION_FOR_CALLS);
+ return hasVibrator() && (vibrateWhenRingingSetting == DO_VIBRATION_FOR_CALLS);
+ }
+
+ /** Obtains the value for dialpad/DTMF tones. The default value is true. */
+ private boolean shouldPlayDtmfTone() {
+ int dtmfToneSetting =
+ Settings.System.getInt(
+ getActivity().getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING,
+ PLAY_DTMF_TONE);
+ return dtmfToneSetting == PLAY_DTMF_TONE;
+ }
+
+ /** Whether the device hardware has a vibrator. */
+ private boolean hasVibrator() {
+ Vibrator vibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
+ return vibrator != null && vibrator.hasVibrator();
+ }
+
+ private boolean shouldHideCarrierSettings() {
+ CarrierConfigManager configManager =
+ (CarrierConfigManager) getActivity().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ return configManager
+ .getConfig()
+ .getBoolean(CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java b/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java
new file mode 100644
index 0000000..8d70cdb
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2015 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.app.voicemail;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.telecom.CallAudioState;
+import com.android.dialer.common.LogUtil;
+import java.util.concurrent.RejectedExecutionException;
+
+/** This class manages all audio changes for voicemail playback. */
+public final class VoicemailAudioManager
+ implements OnAudioFocusChangeListener, WiredHeadsetManager.Listener {
+
+ private static final String TAG = "VoicemailAudioManager";
+
+ public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
+
+ private AudioManager mAudioManager;
+ private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+ private WiredHeadsetManager mWiredHeadsetManager;
+ private boolean mWasSpeakerOn;
+ private CallAudioState mCallAudioState;
+ private boolean mBluetoothScoEnabled;
+
+ public VoicemailAudioManager(
+ Context context, VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+ mWiredHeadsetManager = new WiredHeadsetManager(context);
+ mWiredHeadsetManager.setListener(this);
+
+ mCallAudioState = getInitialAudioState();
+ LogUtil.i(
+ "VoicemailAudioManager.VoicemailAudioManager", "Initial audioState = " + mCallAudioState);
+ }
+
+ public void requestAudioFocus() {
+ int result =
+ mAudioManager.requestAudioFocus(
+ this, PLAYBACK_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ throw new RejectedExecutionException("Could not capture audio focus.");
+ }
+ updateBluetoothScoState(true);
+ }
+
+ public void abandonAudioFocus() {
+ updateBluetoothScoState(false);
+ mAudioManager.abandonAudioFocus(this);
+ }
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ LogUtil.d("VoicemailAudioManager.onAudioFocusChange", "focusChange=" + focusChange);
+ mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN);
+ }
+
+ @Override
+ public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ LogUtil.i(
+ "VoicemailAudioManager.onWiredHeadsetPluggedInChanged",
+ "wired headset was plugged in changed: " + oldIsPluggedIn + " -> " + newIsPluggedIn);
+
+ if (oldIsPluggedIn == newIsPluggedIn) {
+ return;
+ }
+
+ int newRoute = mCallAudioState.getRoute(); // start out with existing route
+ if (newIsPluggedIn) {
+ newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
+ } else {
+ if (mWasSpeakerOn) {
+ newRoute = CallAudioState.ROUTE_SPEAKER;
+ } else {
+ newRoute = CallAudioState.ROUTE_EARPIECE;
+ }
+ }
+
+ mVoicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER);
+
+ // We need to call this every time even if we do not change the route because the supported
+ // routes changed either to include or not include WIRED_HEADSET.
+ setSystemAudioState(
+ new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes()));
+ }
+
+ public void setSpeakerphoneOn(boolean on) {
+ setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE);
+ }
+
+ public boolean isWiredHeadsetPluggedIn() {
+ return mWiredHeadsetManager.isPluggedIn();
+ }
+
+ public void registerReceivers() {
+ // Receivers is plural because we expect to add bluetooth support.
+ mWiredHeadsetManager.registerReceiver();
+ }
+
+ public void unregisterReceivers() {
+ mWiredHeadsetManager.unregisterReceiver();
+ }
+
+ /**
+ * Bluetooth SCO (Synchronous Connection-Oriented) is the "phone" bluetooth audio. The system will
+ * route to the bluetooth headset automatically if A2DP ("media") is available, but if the headset
+ * only supports SCO then dialer must route it manually.
+ */
+ private void updateBluetoothScoState(boolean hasAudioFocus) {
+ if (hasAudioFocus) {
+ if (hasMediaAudioCapability()) {
+ mBluetoothScoEnabled = false;
+ } else {
+ mBluetoothScoEnabled = true;
+ LogUtil.i(
+ "VoicemailAudioManager.updateBluetoothScoState",
+ "bluetooth device doesn't support media, using SCO instead");
+ }
+ } else {
+ mBluetoothScoEnabled = false;
+ }
+ applyBluetoothScoState();
+ }
+
+ private void applyBluetoothScoState() {
+ if (mBluetoothScoEnabled) {
+ mAudioManager.startBluetoothSco();
+ // The doc for startBluetoothSco() states it could take seconds to establish the SCO
+ // connection, so we should probably resume the playback after we've acquired SCO.
+ // In practice the delay is unnoticeable so this is ignored for simplicity.
+ mAudioManager.setBluetoothScoOn(true);
+ } else {
+ mAudioManager.setBluetoothScoOn(false);
+ mAudioManager.stopBluetoothSco();
+ }
+ }
+
+ private boolean hasMediaAudioCapability() {
+ for (AudioDeviceInfo info : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
+ if (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Change the audio route, for example from earpiece to speakerphone.
+ *
+ * @param route The new audio route to use. See {@link CallAudioState}.
+ */
+ void setAudioRoute(int route) {
+ LogUtil.v(
+ "VoicemailAudioManager.setAudioRoute",
+ "route: " + CallAudioState.audioRouteToString(route));
+
+ // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+ int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());
+
+ // If route is unsupported, do nothing.
+ if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
+ LogUtil.w(
+ "VoicemailAudioManager.setAudioRoute",
+ "Asking to set to a route that is unsupported: " + newRoute);
+ return;
+ }
+
+ // Remember the new speaker state so it can be restored when the user plugs and unplugs
+ // a headset.
+ mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
+ setSystemAudioState(
+ new CallAudioState(false /* muted */, newRoute, mCallAudioState.getSupportedRouteMask()));
+ }
+
+ private CallAudioState getInitialAudioState() {
+ int supportedRouteMask = calculateSupportedRoutes();
+ int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
+ return new CallAudioState(false /* muted */, route, supportedRouteMask);
+ }
+
+ private int calculateSupportedRoutes() {
+ int routeMask = CallAudioState.ROUTE_SPEAKER;
+ if (mWiredHeadsetManager.isPluggedIn()) {
+ routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+ } else {
+ routeMask |= CallAudioState.ROUTE_EARPIECE;
+ }
+ return routeMask;
+ }
+
+ private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
+ // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+ // ROUTE_WIRED_OR_EARPIECE so that callers don't have to make a call to check which is
+ // supported before calling setAudioRoute.
+ if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
+ route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
+ if (route == 0) {
+ LogUtil.e(
+ "VoicemailAudioManager.selectWiredOrEarpiece",
+ "One of wired headset or earpiece should always be valid.");
+ // assume earpiece in this case.
+ route = CallAudioState.ROUTE_EARPIECE;
+ }
+ }
+ return route;
+ }
+
+ private void setSystemAudioState(CallAudioState callAudioState) {
+ CallAudioState oldAudioState = mCallAudioState;
+ mCallAudioState = callAudioState;
+
+ LogUtil.i(
+ "VoicemailAudioManager.setSystemAudioState",
+ "changing from " + oldAudioState + " to " + mCallAudioState);
+
+ // Audio route.
+ if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
+ turnOnSpeaker(true);
+ } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE
+ || mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
+ // Just handle turning off the speaker, the system will handle switching between wired
+ // headset and earpiece.
+ turnOnSpeaker(false);
+ // BluetoothSco is not handled by the system so it has to be reset.
+ applyBluetoothScoState();
+ }
+ }
+
+ private void turnOnSpeaker(boolean on) {
+ if (mAudioManager.isSpeakerphoneOn() != on) {
+ LogUtil.i("VoicemailAudioManager.turnOnSpeaker", "turning speaker phone on: " + on);
+ mAudioManager.setSpeakerphoneOn(on);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
new file mode 100644
index 0000000..939007a
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
@@ -0,0 +1,129 @@
+/*
+ * 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.app.voicemail;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Handler;
+import com.android.dialer.app.calllog.CallLogAlertManager;
+import com.android.dialer.app.calllog.CallLogModalAlertManager;
+import com.android.dialer.app.voicemail.error.VoicemailErrorAlert;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessageCreator;
+import com.android.dialer.app.voicemail.error.VoicemailStatus;
+import com.android.dialer.app.voicemail.error.VoicemailStatusReader;
+import com.android.dialer.database.CallLogQueryHandler;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fetches voicemail status and generate {@link VoicemailStatus} for {@link VoicemailErrorAlert} to
+ * show.
+ */
+public class VoicemailErrorManager implements CallLogQueryHandler.Listener, VoicemailStatusReader {
+
+ private final Context context;
+ private final CallLogQueryHandler callLogQueryHandler;
+ private final VoicemailErrorAlert alertItem;
+
+ private final ContentObserver statusObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ maybeFetchStatus();
+ }
+ };
+
+ private boolean isForeground;
+ private boolean statusInvalidated;
+
+ public VoicemailErrorManager(
+ Context context,
+ CallLogAlertManager alertManager,
+ CallLogModalAlertManager modalAlertManager) {
+ this.context = context;
+ alertItem =
+ new VoicemailErrorAlert(
+ context, alertManager, modalAlertManager, new VoicemailErrorMessageCreator());
+ callLogQueryHandler = new CallLogQueryHandler(context, context.getContentResolver(), this);
+ maybeFetchStatus();
+ }
+
+ public ContentObserver getContentObserver() {
+ return statusObserver;
+ }
+
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ List<VoicemailStatus> statuses = new ArrayList<>();
+ while (statusCursor.moveToNext()) {
+ VoicemailStatus status = new VoicemailStatus(context, statusCursor);
+ if (status.isActive()) {
+ statuses.add(status);
+ }
+ }
+ alertItem.updateStatus(statuses, this);
+ // TODO: b/30668323 support error from multiple sources.
+ return;
+ }
+
+ @Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public boolean onCallsFetched(Cursor combinedCursor) {
+ // Do nothing
+ return false;
+ }
+
+ public void onResume() {
+ isForeground = true;
+ if (statusInvalidated) {
+ maybeFetchStatus();
+ }
+ }
+
+ public void onPause() {
+ isForeground = false;
+ statusInvalidated = false;
+ }
+
+ @Override
+ public void refresh() {
+ maybeFetchStatus();
+ }
+
+ /**
+ * Fetch the status when the dialer is in foreground, or queue a fetch when the dialer resumes.
+ */
+ private void maybeFetchStatus() {
+ if (!isForeground) {
+ // Dialer is in the background, UI should not be updated. Reload the status when it resumes.
+ statusInvalidated = true;
+ return;
+ }
+ callLogQueryHandler.fetchVoicemailStatus();
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
new file mode 100644
index 0000000..fc6a376
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2011 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.app.voicemail;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.support.annotation.VisibleForTesting;
+import android.support.design.widget.Snackbar;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.app.calllog.CallLogListItemViewHolder;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for details on the
+ * voicemail playback implementation.
+ *
+ * <p>This class is not thread-safe, it is thread-confined. All calls to all public methods on this
+ * class are expected to come from the main ui thread.
+ */
+@NotThreadSafe
+public class VoicemailPlaybackLayout extends LinearLayout
+ implements VoicemailPlaybackPresenter.PlaybackView,
+ CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
+
+ private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
+ private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
+
+ private Context mContext;
+ private CallLogListItemViewHolder mViewHolder;
+ private VoicemailPlaybackPresenter mPresenter;
+ /** Click listener to toggle speakerphone. */
+ private final View.OnClickListener mSpeakerphoneListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPresenter != null) {
+ mPresenter.toggleSpeakerphone();
+ }
+ }
+ };
+
+ private Uri mVoicemailUri;
+ private final View.OnClickListener mDeleteButtonListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_DELETE_ENTRY);
+ if (mPresenter == null) {
+ return;
+ }
+
+ // When the undo button is pressed, the viewHolder we have is no longer valid because when
+ // we hide the view it is binded to something else, and the layout is not updated for
+ // hidden items. copy the adapter position so we can update the view upon undo.
+ // TODO: refactor this so the view holder will always be valid.
+ final int adapterPosition = mViewHolder.getAdapterPosition();
+
+ mPresenter.pausePlayback();
+ mPresenter.onVoicemailDeleted(mViewHolder);
+
+ final Uri deleteUri = mVoicemailUri;
+ final Runnable deleteCallback =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (Objects.equals(deleteUri, mVoicemailUri)) {
+ CallLogAsyncTaskUtil.deleteVoicemail(
+ mContext, deleteUri, VoicemailPlaybackLayout.this);
+ }
+ }
+ };
+
+ final Handler handler = new Handler();
+ // Add a little buffer time in case the user clicked "undo" at the end of the delay
+ // window.
+ handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
+
+ Snackbar.make(
+ VoicemailPlaybackLayout.this,
+ R.string.snackbar_voicemail_deleted,
+ Snackbar.LENGTH_LONG)
+ .setDuration(VOICEMAIL_DELETE_DELAY_MS)
+ .setAction(
+ R.string.snackbar_voicemail_deleted_undo,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mPresenter.onVoicemailDeleteUndo(adapterPosition);
+ handler.removeCallbacks(deleteCallback);
+ }
+ })
+ .setActionTextColor(
+ mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+ .show();
+ }
+ };
+ private boolean mIsPlaying = false;
+ /** Click listener to play or pause voicemail playback. */
+ private final View.OnClickListener mStartStopButtonListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mPresenter == null) {
+ return;
+ }
+
+ if (mIsPlaying) {
+ mPresenter.pausePlayback();
+ } else {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY);
+ mPresenter.resumePlayback();
+ }
+ }
+ };
+
+ private SeekBar mPlaybackSeek;
+ private ImageButton mStartStopButton;
+ private ImageButton mPlaybackSpeakerphone;
+ private ImageButton mDeleteButton;
+ private TextView mStateText;
+ private TextView mPositionText;
+ private TextView mTotalDurationText;
+ /** Handle state changes when the user manipulates the seek bar. */
+ private final OnSeekBarChangeListener mSeekBarChangeListener =
+ new OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (mPresenter != null) {
+ mPresenter.pausePlaybackForSeeking();
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mPresenter != null) {
+ mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress());
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ setClipPosition(progress, seekBar.getMax());
+ // Update the seek position if user manually changed it. This makes sure position gets
+ // updated when user use volume button to seek playback in talkback mode.
+ if (fromUser) {
+ mPresenter.seek(progress);
+ }
+ }
+ };
+
+ private PositionUpdater mPositionUpdater;
+ private Drawable mVoicemailSeekHandleEnabled;
+ private Drawable mVoicemailSeekHandleDisabled;
+
+ public VoicemailPlaybackLayout(Context context) {
+ this(context, null);
+ }
+
+ public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.voicemail_playback_layout, this);
+ }
+
+ public void setViewHolder(CallLogListItemViewHolder mViewHolder) {
+ this.mViewHolder = mViewHolder;
+ }
+
+ @Override
+ public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
+ mPresenter = presenter;
+ mVoicemailUri = voicemailUri;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
+ mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
+ mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
+ mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
+
+ mStateText = (TextView) findViewById(R.id.playback_state_text);
+ mStateText.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
+ mPositionText = (TextView) findViewById(R.id.playback_position_text);
+ mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
+
+ mPlaybackSeek.setOnSeekBarChangeListener(mSeekBarChangeListener);
+ mStartStopButton.setOnClickListener(mStartStopButtonListener);
+ mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
+ mDeleteButton.setOnClickListener(mDeleteButtonListener);
+
+ mPositionText.setText(formatAsMinutesAndSeconds(0));
+ mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
+
+ mVoicemailSeekHandleEnabled =
+ getResources().getDrawable(R.drawable.ic_voicemail_seek_handle, mContext.getTheme());
+ mVoicemailSeekHandleDisabled =
+ getResources()
+ .getDrawable(R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme());
+ }
+
+ @Override
+ public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
+ mIsPlaying = true;
+
+ mStartStopButton.setImageResource(R.drawable.ic_pause);
+
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ mPositionUpdater = null;
+ }
+ mPositionUpdater = new PositionUpdater(duration, executorService);
+ mPositionUpdater.startUpdating();
+ }
+
+ @Override
+ public void onPlaybackStopped() {
+ mIsPlaying = false;
+
+ mStartStopButton.setImageResource(R.drawable.ic_play_arrow);
+
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ mPositionUpdater = null;
+ }
+ }
+
+ @Override
+ public void onPlaybackError() {
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ }
+
+ disableUiElements();
+ mStateText.setText(getString(R.string.voicemail_playback_error));
+ }
+
+ @Override
+ public void onSpeakerphoneOn(boolean on) {
+ if (on) {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
+ // Speaker is now on, tapping button will turn it off.
+ mPlaybackSpeakerphone.setContentDescription(
+ mContext.getString(R.string.voicemail_speaker_off));
+ } else {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_down_24dp);
+ // Speaker is now off, tapping button will turn it on.
+ mPlaybackSpeakerphone.setContentDescription(
+ mContext.getString(R.string.voicemail_speaker_on));
+ }
+ }
+
+ @Override
+ public void setClipPosition(int positionMs, int durationMs) {
+ int seekBarPositionMs = Math.max(0, positionMs);
+ int seekBarMax = Math.max(seekBarPositionMs, durationMs);
+ if (mPlaybackSeek.getMax() != seekBarMax) {
+ mPlaybackSeek.setMax(seekBarMax);
+ }
+
+ mPlaybackSeek.setProgress(seekBarPositionMs);
+
+ mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
+ mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
+ }
+
+ @Override
+ public void setSuccess() {
+ mStateText.setText(null);
+ }
+
+ @Override
+ public void setIsFetchingContent() {
+ disableUiElements();
+ mStateText.setText(getString(R.string.voicemail_fetching_content));
+ }
+
+ @Override
+ public void setFetchContentTimeout() {
+ mStartStopButton.setEnabled(true);
+ mStateText.setText(getString(R.string.voicemail_fetching_timout));
+ }
+
+ @Override
+ public int getDesiredClipPosition() {
+ return mPlaybackSeek.getProgress();
+ }
+
+ @Override
+ public void disableUiElements() {
+ mStartStopButton.setEnabled(false);
+ resetSeekBar();
+ }
+
+ @Override
+ public void enableUiElements() {
+ mDeleteButton.setEnabled(true);
+ mStartStopButton.setEnabled(true);
+ mPlaybackSeek.setEnabled(true);
+ mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
+ }
+
+ @Override
+ public void resetSeekBar() {
+ mPlaybackSeek.setProgress(0);
+ mPlaybackSeek.setEnabled(false);
+ mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
+ }
+
+ @Override
+ public void onDeleteCall() {}
+
+ @Override
+ public void onDeleteVoicemail() {
+ mPresenter.onVoicemailDeletedInDatabase();
+ }
+
+ @Override
+ public void onGetCallDetails(PhoneCallDetails[] details) {}
+
+ private String getString(int resId) {
+ return mContext.getString(resId);
+ }
+
+ /**
+ * Formats a number of milliseconds as something that looks like {@code 00:05}.
+ *
+ * <p>We always use four digits, two for minutes two for seconds. In the very unlikely event that
+ * the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
+ */
+ private String formatAsMinutesAndSeconds(int millis) {
+ int seconds = millis / 1000;
+ int minutes = seconds / 60;
+ seconds -= minutes * 60;
+ if (minutes > 99) {
+ minutes = 99;
+ }
+ return String.format("%02d:%02d", minutes, seconds);
+ }
+
+ @VisibleForTesting
+ public String getStateText() {
+ return mStateText.getText().toString();
+ }
+
+ /** Controls the animation of the playback slider. */
+ @ThreadSafe
+ private final class PositionUpdater implements Runnable {
+
+ /** Update rate for the slider, 30fps. */
+ private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
+
+ private final ScheduledExecutorService mExecutorService;
+ private final Object mLock = new Object();
+ private int mDurationMs;
+
+ @GuardedBy("mLock")
+ private ScheduledFuture<?> mScheduledFuture;
+
+ private Runnable mUpdateClipPositionRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ int currentPositionMs = 0;
+ synchronized (mLock) {
+ if (mScheduledFuture == null || mPresenter == null) {
+ // This task has been canceled. Just stop now.
+ return;
+ }
+ currentPositionMs = mPresenter.getMediaPlayerPosition();
+ }
+ setClipPosition(currentPositionMs, mDurationMs);
+ }
+ };
+
+ public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
+ mDurationMs = durationMs;
+ mExecutorService = executorService;
+ }
+
+ @Override
+ public void run() {
+ post(mUpdateClipPositionRunnable);
+ }
+
+ public void startUpdating() {
+ synchronized (mLock) {
+ cancelPendingRunnables();
+ mScheduledFuture =
+ mExecutorService.scheduleAtFixedRate(
+ this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public void stopUpdating() {
+ synchronized (mLock) {
+ cancelPendingRunnables();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void cancelPendingRunnables() {
+ if (mScheduledFuture != null) {
+ mScheduledFuture.cancel(true);
+ mScheduledFuture = null;
+ }
+ removeCallbacks(mUpdateClipPositionRunnable);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
new file mode 100644
index 0000000..6570222
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
@@ -0,0 +1,1050 @@
+/*
+ * Copyright (C) 2011 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.app.voicemail;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.FileProvider;
+import android.text.TextUtils;
+import android.view.WindowManager.LayoutParams;
+import android.webkit.MimeTypeMap;
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogListItemViewHolder;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.constants.Constants;
+import com.android.dialer.phonenumbercache.CallLogQuery;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled to
+ * assumptions about the behaviors and lifecycle of the call log, in particular in the {@link
+ * CallLogFragment} and {@link CallLogAdapter}.
+ *
+ * <p>This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
+ * instance can be reused for different such layouts, using {@link #setPlaybackView}. This is to
+ * facilitate reuse across different voicemail call log entries.
+ *
+ * <p>This class is not thread safe. The thread policy for this class is thread-confinement, all
+ * calls into this class from outside must be done from the main UI thread.
+ */
+@NotThreadSafe
+@VisibleForTesting
+@TargetApi(VERSION_CODES.M)
+public class VoicemailPlaybackPresenter
+ implements MediaPlayer.OnPreparedListener,
+ MediaPlayer.OnCompletionListener,
+ MediaPlayer.OnErrorListener {
+
+ public static final int PLAYBACK_REQUEST = 0;
+ private static final int NUMBER_OF_THREADS_IN_POOL = 2;
+ // Time to wait for content to be fetched before timing out.
+ private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
+ private static final String VOICEMAIL_URI_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
+ private static final String IS_PREPARED_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
+ // If present in the saved instance bundle, we should not resume playback on create.
+ private static final String IS_PLAYING_STATE_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY";
+ // If present in the saved instance bundle, indicates where to set the playback slider.
+ private static final String CLIP_POSITION_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
+ private static final String IS_SPEAKERPHONE_ON_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
+ private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa";
+ private static VoicemailPlaybackPresenter sInstance;
+ private static ScheduledExecutorService mScheduledExecutorService;
+ /**
+ * The most recently cached duration. We cache this since we don't want to keep requesting it from
+ * the player, as this can easily lead to throwing {@link IllegalStateException} (any time the
+ * player is released, it's illegal to ask for the duration).
+ */
+ private final AtomicInteger mDuration = new AtomicInteger(0);
+
+ protected Context mContext;
+ private long mRowId;
+ protected Uri mVoicemailUri;
+ protected MediaPlayer mMediaPlayer;
+ // Used to run async tasks that need to interact with the UI.
+ protected AsyncTaskExecutor mAsyncTaskExecutor;
+ private Activity mActivity;
+ private PlaybackView mView;
+ private int mPosition;
+ private boolean mIsPlaying;
+ // MediaPlayer crashes on some method calls if not prepared but does not have a method which
+ // exposes its prepared state. Store this locally, so we can check and prevent crashes.
+ private boolean mIsPrepared;
+ private boolean mIsSpeakerphoneOn;
+
+ private boolean mShouldResumePlaybackAfterSeeking;
+ /**
+ * Used to handle the result of a successful or time-out fetch result.
+ *
+ * <p>This variable is thread-contained, accessed only on the ui thread.
+ */
+ private FetchResultHandler mFetchResultHandler;
+
+ private PowerManager.WakeLock mProximityWakeLock;
+ private VoicemailAudioManager mVoicemailAudioManager;
+ private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
+
+ /** Initialize variables which are activity-independent and state-independent. */
+ protected VoicemailPlaybackPresenter(Activity activity) {
+ Context context = activity.getApplicationContext();
+ mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
+ mVoicemailAudioManager = new VoicemailAudioManager(context, this);
+ PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ mProximityWakeLock =
+ powerManager.newWakeLock(
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "VoicemailPlaybackPresenter");
+ }
+ }
+
+ /**
+ * Obtain singleton instance of this class. Use a single instance to provide a consistent listener
+ * to the AudioManager when requesting and abandoning audio focus.
+ *
+ * <p>Otherwise, after rotation the previous listener will still be active but a new listener will
+ * be provided to calls to the AudioManager, which is bad. For example, abandoning audio focus
+ * with the new listeners results in an AUDIO_FOCUS_GAIN callback to the previous listener, which
+ * is the opposite of the intended behavior.
+ */
+ @MainThread
+ public static VoicemailPlaybackPresenter getInstance(
+ Activity activity, Bundle savedInstanceState) {
+ if (sInstance == null) {
+ sInstance = new VoicemailPlaybackPresenter(activity);
+ }
+
+ sInstance.init(activity, savedInstanceState);
+ return sInstance;
+ }
+
+ private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
+ if (mScheduledExecutorService == null) {
+ mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
+ }
+ return mScheduledExecutorService;
+ }
+
+ /** Update variables which are activity-dependent or state-dependent. */
+ @MainThread
+ protected void init(Activity activity, Bundle savedInstanceState) {
+ Assert.isMainThread();
+ mActivity = activity;
+ mContext = activity;
+
+ if (savedInstanceState != null) {
+ // Restores playback state when activity is recreated, such as after rotation.
+ mVoicemailUri = savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
+ mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
+ mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
+ mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
+ mIsSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false);
+ }
+
+ if (mMediaPlayer == null) {
+ mIsPrepared = false;
+ mIsPlaying = false;
+ }
+
+ if (mActivity != null) {
+ if (isPlaying()) {
+ mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+ }
+
+ /** Must be invoked when the parent Activity is saving it state. */
+ public void onSaveInstanceState(Bundle outState) {
+ if (mView != null) {
+ outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
+ outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
+ outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
+ outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
+ outState.putBoolean(IS_SPEAKERPHONE_ON_KEY, mIsSpeakerphoneOn);
+ }
+ }
+
+ /** Specify the view which this presenter controls and the voicemail to prepare to play. */
+ public void setPlaybackView(
+ PlaybackView view, long rowId, Uri voicemailUri, final boolean startPlayingImmediately) {
+ mRowId = rowId;
+ mView = view;
+ mView.setPresenter(this, voicemailUri);
+ mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
+
+ // Handles cases where the same entry is binded again when scrolling in list, or where
+ // the MediaPlayer was retained after an orientation change.
+ if (mMediaPlayer != null && mIsPrepared && voicemailUri.equals(mVoicemailUri)) {
+ // If the voicemail card was rebinded, we need to set the position to the appropriate
+ // point. Since we retain the media player, we can just set it to the position of the
+ // media player.
+ mPosition = mMediaPlayer.getCurrentPosition();
+ onPrepared(mMediaPlayer);
+ } else {
+ if (!voicemailUri.equals(mVoicemailUri)) {
+ mVoicemailUri = voicemailUri;
+ mPosition = 0;
+ }
+ /*
+ * Check to see if the content field in the DB is set. If set, we proceed to
+ * prepareContent() method. We get the duration of the voicemail from the query and set
+ * it if the content is not available.
+ */
+ checkForContent(
+ new OnContentCheckedListener() {
+ @Override
+ public void onContentChecked(boolean hasContent) {
+ if (hasContent) {
+ prepareContent();
+ } else {
+ if (startPlayingImmediately) {
+ requestContent(PLAYBACK_REQUEST);
+ }
+ if (mView != null) {
+ mView.resetSeekBar();
+ mView.setClipPosition(0, mDuration.get());
+ }
+ }
+ }
+ });
+
+ if (startPlayingImmediately) {
+ // Since setPlaybackView can get called during the view binding process, we don't
+ // want to reset mIsPlaying to false if the user is currently playing the
+ // voicemail and the view is rebound.
+ mIsPlaying = startPlayingImmediately;
+ }
+ }
+ }
+
+ /** Reset the presenter for playback back to its original state. */
+ public void resetAll() {
+ pausePresenter(true);
+
+ mView = null;
+ mVoicemailUri = null;
+ }
+
+ /**
+ * When navigating away from voicemail playback, we need to release the media player, pause the UI
+ * and save the position.
+ *
+ * @param reset {@code true} if we want to reset the position of the playback, {@code false} if we
+ * want to retain the current position (in case we return to the voicemail).
+ */
+ public void pausePresenter(boolean reset) {
+ pausePlayback();
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
+ disableProximitySensor(false /* waitForFarState */);
+
+ mIsPrepared = false;
+ mIsPlaying = false;
+
+ if (reset) {
+ // We want to reset the position whether or not the view is valid.
+ mPosition = 0;
+ }
+
+ if (mView != null) {
+ mView.onPlaybackStopped();
+ if (reset) {
+ mView.setClipPosition(0, mDuration.get());
+ } else {
+ mPosition = mView.getDesiredClipPosition();
+ }
+ }
+ }
+
+ /** Must be invoked when the parent activity is resumed. */
+ public void onResume() {
+ mVoicemailAudioManager.registerReceivers();
+ }
+
+ /** Must be invoked when the parent activity is paused. */
+ public void onPause() {
+ mVoicemailAudioManager.unregisterReceivers();
+
+ if (mActivity != null && mIsPrepared && mActivity.isChangingConfigurations()) {
+ // If an configuration change triggers the pause, retain the MediaPlayer.
+ LogUtil.d("VoicemailPlaybackPresenter.onPause", "configuration changed.");
+ return;
+ }
+
+ // Release the media player, otherwise there may be failures.
+ pausePresenter(false);
+ }
+
+ /** Must be invoked when the parent activity is destroyed. */
+ public void onDestroy() {
+ // Clear references to avoid leaks from the singleton instance.
+ mActivity = null;
+ mContext = null;
+
+ if (mScheduledExecutorService != null) {
+ mScheduledExecutorService.shutdown();
+ mScheduledExecutorService = null;
+ }
+
+ if (mFetchResultHandler != null) {
+ mFetchResultHandler.destroy();
+ mFetchResultHandler = null;
+ }
+ }
+
+ /** Checks to see if we have content available for this voicemail. */
+ protected void checkForContent(final OnContentCheckedListener callback) {
+ mAsyncTaskExecutor.submit(
+ Tasks.CHECK_FOR_CONTENT,
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Void... params) {
+ return queryHasContent(mVoicemailUri);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ callback.onContentChecked(hasContent);
+ }
+ });
+ }
+
+ private boolean queryHasContent(Uri voicemailUri) {
+ if (voicemailUri == null || mContext == null) {
+ return false;
+ }
+
+ ContentResolver contentResolver = mContext.getContentResolver();
+ Cursor cursor = contentResolver.query(voicemailUri, null, null, null, null);
+ try {
+ if (cursor != null && cursor.moveToNext()) {
+ int duration = cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.DURATION));
+ // Convert database duration (seconds) into mDuration (milliseconds)
+ mDuration.set(duration > 0 ? duration * 1000 : 0);
+ return cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
+ }
+ } finally {
+ MoreCloseables.closeQuietly(cursor);
+ }
+ return false;
+ }
+
+ /**
+ * Makes a broadcast request to ask that a voicemail source fetch this content.
+ *
+ * <p>This method <b>must be called on the ui thread</b>.
+ *
+ * <p>This method will be called when we realise that we don't have content for this voicemail. It
+ * will trigger a broadcast to request that the content be downloaded. It will add a listener to
+ * the content resolver so that it will be notified when the has_content field changes. It will
+ * also set a timer. If the has_content field changes to true within the allowed time, we will
+ * proceed to {@link #prepareContent()}. If the has_content field does not become true within the
+ * allowed time, we will update the ui to reflect the fact that content was not available.
+ *
+ * @return whether issued request to fetch content
+ */
+ protected boolean requestContent(int code) {
+ if (mContext == null || mVoicemailUri == null) {
+ return false;
+ }
+
+ FetchResultHandler tempFetchResultHandler =
+ new FetchResultHandler(new Handler(), mVoicemailUri, code);
+
+ switch (code) {
+ default:
+ if (mFetchResultHandler != null) {
+ mFetchResultHandler.destroy();
+ }
+ mView.setIsFetchingContent();
+ mFetchResultHandler = tempFetchResultHandler;
+ break;
+ }
+
+ mAsyncTaskExecutor.submit(
+ Tasks.SEND_FETCH_REQUEST,
+ new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ try (Cursor cursor =
+ mContext
+ .getContentResolver()
+ .query(
+ mVoicemailUri,
+ new String[] {Voicemails.SOURCE_PACKAGE},
+ null,
+ null,
+ null)) {
+ String sourcePackage;
+ if (!hasContent(cursor)) {
+ LogUtil.e(
+ "VoicemailPlaybackPresenter.requestContent",
+ "mVoicemailUri does not return a SOURCE_PACKAGE");
+ sourcePackage = null;
+ } else {
+ sourcePackage = cursor.getString(0);
+ }
+ // Send voicemail fetch request.
+ Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
+ intent.setPackage(sourcePackage);
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.requestContent",
+ "Sending ACTION_FETCH_VOICEMAIL to " + sourcePackage);
+ mContext.sendBroadcast(intent);
+ }
+ return null;
+ }
+ });
+ return true;
+ }
+
+ /**
+ * Prepares the voicemail content for playback.
+ *
+ * <p>This method will be called once we know that our voicemail has content (according to the
+ * content provider). this method asynchronously tries to prepare the data source through the
+ * media player. If preparation is successful, the media player will {@link #onPrepared()}, and it
+ * will call {@link #onError()} otherwise.
+ */
+ protected void prepareContent() {
+ if (mView == null) {
+ return;
+ }
+ LogUtil.d("VoicemailPlaybackPresenter.prepareContent", null);
+
+ // Release the previous media player, otherwise there may be failures.
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
+ mView.disableUiElements();
+ mIsPrepared = false;
+
+ try {
+ mMediaPlayer = new MediaPlayer();
+ mMediaPlayer.setOnPreparedListener(this);
+ mMediaPlayer.setOnErrorListener(this);
+ mMediaPlayer.setOnCompletionListener(this);
+
+ mMediaPlayer.reset();
+ mMediaPlayer.setDataSource(mContext, mVoicemailUri);
+ mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM);
+ mMediaPlayer.prepareAsync();
+ } catch (IOException e) {
+ handleError(e);
+ }
+ }
+
+ /**
+ * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
+ */
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ if (mView == null || mContext == null) {
+ return;
+ }
+ LogUtil.d("VoicemailPlaybackPresenter.onPrepared", null);
+ mIsPrepared = true;
+
+ mDuration.set(mMediaPlayer.getDuration());
+
+ LogUtil.d("VoicemailPlaybackPresenter.onPrepared", "mPosition=" + mPosition);
+ mView.setClipPosition(mPosition, mDuration.get());
+ mView.enableUiElements();
+ mView.setSuccess();
+ mMediaPlayer.seekTo(mPosition);
+
+ if (mIsPlaying) {
+ resumePlayback();
+ } else {
+ pausePlayback();
+ }
+ }
+
+ /**
+ * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
+ * is an unknown file format that can't be played.
+ */
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
+ return true;
+ }
+
+ protected void handleError(Exception e) {
+ LogUtil.e("VoicemailPlaybackPresenter.handlerError", "could not play voicemail", e);
+
+ if (mIsPrepared) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mIsPrepared = false;
+ }
+
+ if (mView != null) {
+ mView.onPlaybackError();
+ }
+
+ mPosition = 0;
+ mIsPlaying = false;
+ }
+
+ /** After done playing the voicemail clip, reset the clip position to the start. */
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ pausePlayback();
+
+ // Reset the seekbar position to the beginning.
+ mPosition = 0;
+ if (mView != null) {
+ mediaPlayer.seekTo(0);
+ mView.setClipPosition(0, mDuration.get());
+ }
+ }
+
+ /**
+ * Only play voicemail when audio focus is granted. When it is lost (usually by another
+ * application requesting focus), pause playback. Audio focus gain/lost only triggers the focus is
+ * requested. Audio focus is requested when the user pressed play and abandoned when the user
+ * pressed pause or the audio has finished. Losing focus should not abandon focus as the voicemail
+ * should resume once the focus is returned.
+ *
+ * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise.
+ */
+ public void onAudioFocusChange(boolean gainedFocus) {
+ if (mIsPlaying == gainedFocus) {
+ // Nothing new here, just exit.
+ return;
+ }
+
+ if (gainedFocus) {
+ resumePlayback();
+ } else {
+ pausePlayback(true);
+ }
+ }
+
+ /**
+ * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
+ * playing.
+ */
+ public void resumePlayback() {
+ if (mView == null) {
+ return;
+ }
+
+ if (!mIsPrepared) {
+ /*
+ * Check content before requesting content to avoid duplicated requests. It is possible
+ * that the UI doesn't know content has arrived if the fetch took too long causing a
+ * timeout, but succeeded.
+ */
+ checkForContent(
+ new OnContentCheckedListener() {
+ @Override
+ public void onContentChecked(boolean hasContent) {
+ if (!hasContent) {
+ // No local content, download from server. Queue playing if the request was
+ // issued,
+ mIsPlaying = requestContent(PLAYBACK_REQUEST);
+ } else {
+ // Queue playing once the media play loaded the content.
+ mIsPlaying = true;
+ prepareContent();
+ }
+ }
+ });
+ return;
+ }
+
+ mIsPlaying = true;
+
+ mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
+ // Clamp the start position between 0 and the duration.
+ mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
+
+ mMediaPlayer.seekTo(mPosition);
+
+ try {
+ // Grab audio focus.
+ // Can throw RejectedExecutionException.
+ mVoicemailAudioManager.requestAudioFocus();
+ mMediaPlayer.start();
+ setSpeakerphoneOn(mIsSpeakerphoneOn);
+ mVoicemailAudioManager.setSpeakerphoneOn(mIsSpeakerphoneOn);
+ } catch (RejectedExecutionException e) {
+ handleError(e);
+ }
+ }
+
+ LogUtil.d("VoicemailPlaybackPresenter.resumePlayback", "resumed playback at %d.", mPosition);
+ mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
+ }
+
+ /** Pauses voicemail playback at the current position. Null-op if already paused. */
+ public void pausePlayback() {
+ pausePlayback(false);
+ }
+
+ private void pausePlayback(boolean keepFocus) {
+ if (!mIsPrepared) {
+ return;
+ }
+
+ mIsPlaying = false;
+
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ }
+
+ mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
+
+ LogUtil.d("VoicemailPlaybackPresenter.pausePlayback", "paused playback at %d.", mPosition);
+
+ if (mView != null) {
+ mView.onPlaybackStopped();
+ }
+
+ if (!keepFocus) {
+ mVoicemailAudioManager.abandonAudioFocus();
+ }
+ if (mActivity != null) {
+ mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ disableProximitySensor(true /* waitForFarState */);
+ }
+
+ /**
+ * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
+ * playing to know whether to resume playback once the user selects a new position.
+ */
+ public void pausePlaybackForSeeking() {
+ if (mMediaPlayer != null) {
+ mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
+ }
+ pausePlayback(true);
+ }
+
+ public void resumePlaybackAfterSeeking(int desiredPosition) {
+ mPosition = desiredPosition;
+ if (mShouldResumePlaybackAfterSeeking) {
+ mShouldResumePlaybackAfterSeeking = false;
+ resumePlayback();
+ }
+ }
+
+ /**
+ * Seek to position. This is called when user manually seek the playback. It could be either by
+ * touch or volume button while in talkback mode.
+ */
+ public void seek(int position) {
+ mPosition = position;
+ mMediaPlayer.seekTo(mPosition);
+ }
+
+ private void enableProximitySensor() {
+ if (mProximityWakeLock == null
+ || mIsSpeakerphoneOn
+ || !mIsPrepared
+ || mMediaPlayer == null
+ || !mMediaPlayer.isPlaying()) {
+ return;
+ }
+
+ if (!mProximityWakeLock.isHeld()) {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.enableProximitySensor", "acquiring proximity wake lock");
+ mProximityWakeLock.acquire();
+ } else {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.enableProximitySensor",
+ "proximity wake lock already acquired");
+ }
+ }
+
+ private void disableProximitySensor(boolean waitForFarState) {
+ if (mProximityWakeLock == null) {
+ return;
+ }
+ if (mProximityWakeLock.isHeld()) {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.disableProximitySensor", "releasing proximity wake lock");
+ int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
+ mProximityWakeLock.release(flags);
+ } else {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.disableProximitySensor",
+ "proximity wake lock already released");
+ }
+ }
+
+ /** This is for use by UI interactions only. It simplifies UI logic. */
+ public void toggleSpeakerphone() {
+ mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
+ setSpeakerphoneOn(!mIsSpeakerphoneOn);
+ }
+
+ public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
+ mOnVoicemailDeletedListener = listener;
+ }
+
+ public int getMediaPlayerPosition() {
+ return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
+ }
+
+ void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeleted(viewHolder, mVoicemailUri);
+ }
+ }
+
+ void onVoicemailDeleteUndo(int adapterPosition) {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeleteUndo(mRowId, adapterPosition, mVoicemailUri);
+ }
+ }
+
+ void onVoicemailDeletedInDatabase() {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase(mRowId, mVoicemailUri);
+ }
+ }
+
+ @VisibleForTesting
+ public boolean isPlaying() {
+ return mIsPlaying;
+ }
+
+ @VisibleForTesting
+ public boolean isSpeakerphoneOn() {
+ return mIsSpeakerphoneOn;
+ }
+
+ /**
+ * This method only handles app-level changes to the speakerphone. Audio layer changes should be
+ * handled separately. This is so that the VoicemailAudioManager can trigger changes to the
+ * presenter without the presenter triggering the audio manager and duplicating actions.
+ */
+ public void setSpeakerphoneOn(boolean on) {
+ if (mView == null) {
+ return;
+ }
+
+ mView.onSpeakerphoneOn(on);
+
+ mIsSpeakerphoneOn = on;
+
+ // This should run even if speakerphone is not being toggled because we may be switching
+ // from earpiece to headphone and vise versa. Also upon initial setup the default audio
+ // source is the earpiece, so we want to trigger the proximity sensor.
+ if (mIsPlaying) {
+ if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) {
+ disableProximitySensor(false /* waitForFarState */);
+ } else {
+ enableProximitySensor();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void clearInstance() {
+ sInstance = null;
+ }
+
+ /**
+ * Share voicemail to be opened by user selected apps. This method will collect information, copy
+ * voicemail to a temporary file in background and launch a chooser intent to share it.
+ */
+ @TargetApi(VERSION_CODES.M)
+ public void shareVoicemail() {
+ mAsyncTaskExecutor.submit(
+ Tasks.SHARE_VOICEMAIL,
+ new AsyncTask<Void, Void, Uri>() {
+ @Nullable
+ @Override
+ protected Uri doInBackground(Void... params) {
+ ContentResolver contentResolver = mContext.getContentResolver();
+ try (Cursor callLogInfo = getCallLogInfoCursor(contentResolver, mVoicemailUri);
+ Cursor contentInfo = getContentInfoCursor(contentResolver, mVoicemailUri)) {
+
+ if (hasContent(callLogInfo) && hasContent(contentInfo)) {
+ String cachedName = callLogInfo.getString(CallLogQuery.CACHED_NAME);
+ String number =
+ contentInfo.getString(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.NUMBER));
+ long date =
+ contentInfo.getLong(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.DATE));
+ String mimeType =
+ contentInfo.getString(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.MIME_TYPE));
+
+ // Copy voicemail content to a new file.
+ // Please see reference in third_party/java_src/android_app/dialer/java/com/android/
+ // dialer/app/res/xml/file_paths.xml for correct cache directory name.
+ File parentDir = new File(mContext.getCacheDir(), "my_cache");
+ if (!parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ File temporaryVoicemailFile =
+ new File(parentDir, getFileName(cachedName, number, mimeType, date));
+
+ try (InputStream inputStream = contentResolver.openInputStream(mVoicemailUri);
+ OutputStream outputStream =
+ contentResolver.openOutputStream(Uri.fromFile(temporaryVoicemailFile))) {
+ if (inputStream != null && outputStream != null) {
+ ByteStreams.copy(inputStream, outputStream);
+ return FileProvider.getUriForFile(
+ mContext,
+ Constants.get().getFileProviderAuthority(),
+ temporaryVoicemailFile);
+ }
+ } catch (IOException e) {
+ LogUtil.e(
+ "VoicemailAsyncTaskUtil.shareVoicemail",
+ "failed to copy voicemail content to new file: ",
+ e);
+ }
+ return null;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ if (uri == null) {
+ LogUtil.e("VoicemailAsyncTaskUtil.shareVoicemail", "failed to get voicemail");
+ } else {
+ mContext.startActivity(
+ Intent.createChooser(
+ getShareIntent(mContext, uri),
+ mContext.getResources().getText(R.string.call_log_action_share_voicemail)));
+ }
+ }
+ });
+ }
+
+ private static String getFileName(String cachedName, String number, String mimeType, long date) {
+ String callerName = TextUtils.isEmpty(cachedName) ? number : cachedName;
+ SimpleDateFormat simpleDateFormat =
+ new SimpleDateFormat(VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT, Locale.getDefault());
+
+ String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+
+ return callerName
+ + "_"
+ + simpleDateFormat.format(new Date(date))
+ + (TextUtils.isEmpty(fileExtension) ? "" : "." + fileExtension);
+ }
+
+ private static Intent getShareIntent(Context context, Uri voicemailFileUri) {
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
+ shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ shareIntent.setType(context.getContentResolver().getType(voicemailFileUri));
+ return shareIntent;
+ }
+
+ private static boolean hasContent(@Nullable Cursor cursor) {
+ return cursor != null && cursor.moveToFirst();
+ }
+
+ @Nullable
+ private static Cursor getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
+ return contentResolver.query(
+ ContentUris.withAppendedId(
+ CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(voicemailUri)),
+ CallLogQuery.getProjection(),
+ null,
+ null,
+ null);
+ }
+
+ @Nullable
+ private static Cursor getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
+ return contentResolver.query(
+ voicemailUri,
+ new String[] {
+ VoicemailContract.Voicemails._ID,
+ VoicemailContract.Voicemails.NUMBER,
+ VoicemailContract.Voicemails.DATE,
+ VoicemailContract.Voicemails.MIME_TYPE,
+ },
+ null,
+ null,
+ null);
+ }
+
+ /** The enumeration of {@link AsyncTask} objects we use in this class. */
+ public enum Tasks {
+ CHECK_FOR_CONTENT,
+ CHECK_CONTENT_AFTER_CHANGE,
+ SHARE_VOICEMAIL,
+ SEND_FETCH_REQUEST
+ }
+
+ /** Contract describing the behaviour we need from the ui we are controlling. */
+ public interface PlaybackView {
+
+ int getDesiredClipPosition();
+
+ void disableUiElements();
+
+ void enableUiElements();
+
+ void onPlaybackError();
+
+ void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
+
+ void onPlaybackStopped();
+
+ void onSpeakerphoneOn(boolean on);
+
+ void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
+
+ void setSuccess();
+
+ void setFetchContentTimeout();
+
+ void setIsFetchingContent();
+
+ void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
+
+ void resetSeekBar();
+ }
+
+ public interface OnVoicemailDeletedListener {
+
+ void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri);
+
+ void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri);
+
+ void onVoicemailDeletedInDatabase(long rowId, Uri uri);
+ }
+
+ protected interface OnContentCheckedListener {
+
+ void onContentChecked(boolean hasContent);
+ }
+
+ @ThreadSafe
+ private class FetchResultHandler extends ContentObserver implements Runnable {
+
+ private final Handler mFetchResultHandler;
+ private final Uri mVoicemailUri;
+ private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
+
+ public FetchResultHandler(Handler handler, Uri uri, int code) {
+ super(handler);
+ mFetchResultHandler = handler;
+ mVoicemailUri = uri;
+ if (mContext != null) {
+ mContext.getContentResolver().registerContentObserver(mVoicemailUri, false, this);
+ mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
+ }
+ }
+
+ /** Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */
+ @Override
+ public void run() {
+ if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ if (mView != null) {
+ mView.setFetchContentTimeout();
+ }
+ }
+ }
+
+ public void destroy() {
+ if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ mFetchResultHandler.removeCallbacks(this);
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mAsyncTaskExecutor.submit(
+ Tasks.CHECK_CONTENT_AFTER_CHANGE,
+ new AsyncTask<Void, Void, Boolean>() {
+
+ @Override
+ public Boolean doInBackground(Void... params) {
+ return queryHasContent(mVoicemailUri);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
+ mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
+ prepareContent();
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java b/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java
new file mode 100644
index 0000000..24d4c6f
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 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.app.voicemail;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.util.Log;
+
+/** Listens for and caches headset state. */
+class WiredHeadsetManager {
+
+ private static final String TAG = WiredHeadsetManager.class.getSimpleName();
+ private final WiredHeadsetBroadcastReceiver mReceiver;
+ private boolean mIsPluggedIn;
+ private Listener mListener;
+ private Context mContext;
+
+ WiredHeadsetManager(Context context) {
+ mContext = context;
+ mReceiver = new WiredHeadsetBroadcastReceiver();
+
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mIsPluggedIn = audioManager.isWiredHeadsetOn();
+ }
+
+ void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ boolean isPluggedIn() {
+ return mIsPluggedIn;
+ }
+
+ void registerReceiver() {
+ // Register for misc other intent broadcasts.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+ mContext.registerReceiver(mReceiver, intentFilter);
+ }
+
+ void unregisterReceiver() {
+ mContext.unregisterReceiver(mReceiver);
+ }
+
+ private void onHeadsetPluggedInChanged(boolean isPluggedIn) {
+ if (mIsPluggedIn != isPluggedIn) {
+ Log.v(TAG, "onHeadsetPluggedInChanged, mIsPluggedIn: " + mIsPluggedIn + " -> " + isPluggedIn);
+ boolean oldIsPluggedIn = mIsPluggedIn;
+ mIsPluggedIn = isPluggedIn;
+ if (mListener != null) {
+ mListener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+ }
+ }
+ }
+
+ interface Listener {
+
+ void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
+ }
+
+ /** Receiver for wired headset plugged and unplugged events. */
+ private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AudioManager.ACTION_HEADSET_PLUG.equals(intent.getAction())) {
+ boolean isPluggedIn = intent.getIntExtra("state", 0) == 1;
+ Log.v(TAG, "ACTION_HEADSET_PLUG event, plugged in: " + isPluggedIn);
+ onHeadsetPluggedInChanged(isPluggedIn);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
new file mode 100644
index 0000000..65d0430
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.app.voicemail.error">
+
+ <uses-permission android:name="android.permission.CALL_PHONE"/>
+</manifest>
diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
new file mode 100644
index 0000000..e36406d
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
@@ -0,0 +1,177 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Create error message from {@link VoicemailStatus} for OMTP visual voicemail. This is also the
+ * default behavior if other message creator does not handle the status.
+ */
+public class OmtpVoicemailMessageCreator {
+
+ private static final float QUOTA_NEAR_FULL_THRESHOLD = 0.9f;
+ private static final float QUOTA_FULL_THRESHOLD = 0.99f;
+
+ @Nullable
+ public static VoicemailErrorMessage create(Context context, VoicemailStatus status) {
+ if (Status.CONFIGURATION_STATE_OK == status.configurationState
+ && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
+ && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
+
+ return checkQuota(context, status);
+ }
+ // Initial state when the source is activating. Other error might be written into data and
+ // notification channel during activation.
+ if (Status.CONFIGURATION_STATE_CONFIGURING == status.configurationState
+ && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
+ && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_activating_title),
+ context.getString(R.string.voicemail_error_activating_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context));
+ }
+
+ if (Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION == status.notificationChannelState) {
+ return createNoSignalMessage(context, status);
+ }
+
+ if (Status.CONFIGURATION_STATE_FAILED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_activation_failed_title),
+ context.getString(R.string.voicemail_error_activation_failed_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_no_data_title),
+ context.getString(R.string.voicemail_error_no_data_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_no_data_title),
+ context.getString(R.string.voicemail_error_no_data_cellular_required_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_bad_config_title),
+ context.getString(R.string.voicemail_error_bad_config_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_communication_title),
+ context.getString(R.string.voicemail_error_communication_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_SERVER_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_server_title),
+ context.getString(R.string.voicemail_error_server_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_server_connection_title),
+ context.getString(R.string.voicemail_error_server_connection_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ // This should be an assertion error, but there's a bug in NYC-DR (b/31069259) that will
+ // sometimes give status mixed from multiple SIMs. There's no meaningful message to be displayed
+ // from it, so just suppress the message.
+ LogUtil.e("OmtpVoicemailMessageCreator.create", "Unhandled status: " + status);
+ return null;
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage checkQuota(Context context, VoicemailStatus status) {
+ if (status.quotaOccupied != Status.QUOTA_UNAVAILABLE
+ && status.quotaTotal != Status.QUOTA_UNAVAILABLE) {
+ if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_FULL_THRESHOLD) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_inbox_full_title),
+ context.getString(R.string.voicemail_error_inbox_full_message));
+ }
+
+ if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_NEAR_FULL_THRESHOLD) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_inbox_near_full_title),
+ context.getString(R.string.voicemail_error_inbox_near_full_message));
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage createNoSignalMessage(
+ Context context, VoicemailStatus status) {
+ CharSequence title;
+ CharSequence description;
+ List<Action> actions = new ArrayList<>();
+ if (Status.CONFIGURATION_STATE_OK == status.configurationState) {
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED == status.dataChannelState) {
+ title = context.getString(R.string.voicemail_error_no_signal_title);
+ description =
+ context.getString(R.string.voicemail_error_no_signal_cellular_required_message);
+ } else {
+ title = context.getString(R.string.voicemail_error_no_signal_title);
+ if (status.isAirplaneMode) {
+ description = context.getString(R.string.voicemail_error_no_signal_airplane_mode_message);
+ } else {
+ description = context.getString(R.string.voicemail_error_no_signal_message);
+ }
+ actions.add(VoicemailErrorMessage.createSyncAction(context, status));
+ }
+ } else {
+ title = context.getString(R.string.voicemail_error_not_activate_no_signal_title);
+ if (status.isAirplaneMode) {
+ description =
+ context.getString(
+ R.string.voicemail_error_not_activate_no_signal_airplane_mode_message);
+ } else {
+ description = context.getString(R.string.voicemail_error_not_activate_no_signal_message);
+ actions.add(VoicemailErrorMessage.createRetryAction(context, status));
+ }
+ }
+ if (status.isAirplaneMode) {
+ actions.add(VoicemailErrorMessage.createChangeAirplaneModeAction(context));
+ }
+ return new VoicemailErrorMessage(title, description, actions);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java
new file mode 100644
index 0000000..d34a0f3
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java
@@ -0,0 +1,165 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.view.View;
+import android.widget.TextView;
+import com.android.dialer.app.alert.AlertManager;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.List;
+
+/**
+ * UI for the voicemail error message, which will be inserted to the top of the voicemail tab if any
+ * occurred.
+ */
+public class VoicemailErrorAlert {
+
+ private final Context context;
+ private final AlertManager alertManager;
+ private final VoicemailErrorMessageCreator messageCreator;
+
+ private final View view;
+ private final TextView header;
+ private final TextView details;
+ private final TextView primaryAction;
+ private final TextView secondaryAction;
+ private final TextView primaryActionRaised;
+ private final TextView secondaryActionRaised;
+ private final AlertManager modalAlertManager;
+ private View modalView;
+
+ public VoicemailErrorAlert(
+ Context context,
+ AlertManager alertManager,
+ AlertManager modalAlertManager,
+ VoicemailErrorMessageCreator messageCreator) {
+ this.context = context;
+ this.alertManager = alertManager;
+ this.modalAlertManager = modalAlertManager;
+ this.messageCreator = messageCreator;
+
+ view = alertManager.inflate(R.layout.voicemai_error_message_fragment);
+ header = (TextView) view.findViewById(R.id.error_card_header);
+ details = (TextView) view.findViewById(R.id.error_card_details);
+ primaryAction = (TextView) view.findViewById(R.id.primary_action);
+ secondaryAction = (TextView) view.findViewById(R.id.secondary_action);
+ primaryActionRaised = (TextView) view.findViewById(R.id.primary_action_raised);
+ secondaryActionRaised = (TextView) view.findViewById(R.id.secondary_action_raised);
+ }
+
+ public void updateStatus(List<VoicemailStatus> statuses, VoicemailStatusReader statusReader) {
+ LogUtil.i("VoicemailErrorAlert.updateStatus", "%d status", statuses.size());
+ VoicemailErrorMessage message = null;
+ view.setVisibility(View.VISIBLE);
+ for (VoicemailStatus status : statuses) {
+ message = messageCreator.create(context, status, statusReader);
+ if (message != null) {
+ break;
+ }
+ }
+
+ alertManager.clear();
+ modalAlertManager.clear();
+ if (message != null) {
+ LogUtil.i(
+ "VoicemailErrorAlert.updateStatus",
+ "isModal: %b, %s",
+ message.isModal(),
+ message.getTitle());
+ if (message.isModal()) {
+ if (message instanceof VoicemailTosMessage) {
+ modalView = getTosView(modalAlertManager, (VoicemailTosMessage) message);
+ } else {
+ throw new IllegalArgumentException("Modal message type is undefined!");
+ }
+ modalAlertManager.add(modalView);
+ } else {
+ loadMessage(message);
+ alertManager.add(view);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public View getView() {
+ return view;
+ }
+
+ @VisibleForTesting
+ public View getModalView() {
+ return modalView;
+ }
+
+ void loadMessage(VoicemailErrorMessage message) {
+ header.setText(message.getTitle());
+ details.setText(message.getDescription());
+ bindActions(message);
+ }
+
+ private View getTosView(AlertManager alertManager, VoicemailTosMessage message) {
+ View view = alertManager.inflate(R.layout.voicemail_tos_fragment);
+ TextView tosTitle = (TextView) view.findViewById(R.id.tos_message_title);
+ tosTitle.setText(message.getTitle());
+ TextView tosDetails = (TextView) view.findViewById(R.id.tos_message_details);
+ tosDetails.setText(message.getDescription());
+
+ Assert.checkArgument(message.getActions().size() == 2);
+ Action primaryAction = message.getActions().get(0);
+ TextView primaryButton = (TextView) view.findViewById(R.id.voicemail_tos_button_decline);
+ primaryButton.setText(primaryAction.getText());
+ primaryButton.setOnClickListener(primaryAction.getListener());
+ Action secondaryAction = message.getActions().get(1);
+ TextView secondaryButton = (TextView) view.findViewById(R.id.voicemail_tos_button_accept);
+ secondaryButton.setText(secondaryAction.getText());
+ secondaryButton.setOnClickListener(secondaryAction.getListener());
+ return view;
+ }
+
+ /**
+ * Attach actions to buttons until all buttons are assigned. If there are not enough actions the
+ * rest of the buttons will be removed. If there are more actions then buttons the extra actions
+ * will be dropped. {@link VoicemailErrorMessage#getActions()} will specify what actions should be
+ * shown and in what order.
+ */
+ private void bindActions(VoicemailErrorMessage message) {
+ TextView[] buttons = new TextView[] {primaryAction, secondaryAction};
+ TextView[] raisedButtons = new TextView[] {primaryActionRaised, secondaryActionRaised};
+ for (int i = 0; i < buttons.length; i++) {
+ if (message.getActions() != null && i < message.getActions().size()) {
+ VoicemailErrorMessage.Action action = message.getActions().get(i);
+ TextView button;
+ if (action.isRaised()) {
+ button = raisedButtons[i];
+ buttons[i].setVisibility(View.GONE);
+ } else {
+ button = buttons[i];
+ raisedButtons[i].setVisibility(View.GONE);
+ }
+ button.setText(action.getText());
+ button.setOnClickListener(action.getListener());
+ button.setVisibility(View.VISIBLE);
+ } else {
+ buttons[i].setVisibility(View.GONE);
+ raisedButtons[i].setVisibility(View.GONE);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
new file mode 100644
index 0000000..6157200
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
@@ -0,0 +1,178 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.provider.VoicemailContract;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.CallUtil;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents an error determined from the current {@link
+ * android.provider.VoicemailContract.Status}. The message will contain a title, a description, and
+ * a list of actions that can be performed.
+ */
+public class VoicemailErrorMessage {
+
+ private final CharSequence title;
+ private final CharSequence description;
+ private final List<Action> actions;
+
+ private boolean modal;
+
+ /** Something the user can click on to resolve an error, such as retrying or calling voicemail */
+ public static class Action {
+
+ private final CharSequence text;
+ private final View.OnClickListener listener;
+ private final boolean raised;
+
+ public Action(CharSequence text, View.OnClickListener listener) {
+ this(text, listener, false);
+ }
+
+ public Action(CharSequence text, View.OnClickListener listener, boolean raised) {
+ this.text = text;
+ this.listener = listener;
+ this.raised = raised;
+ }
+
+ public CharSequence getText() {
+ return text;
+ }
+
+ public View.OnClickListener getListener() {
+ return listener;
+ }
+
+ public boolean isRaised() {
+ return raised;
+ }
+ }
+
+ public CharSequence getTitle() {
+ return title;
+ }
+
+ public CharSequence getDescription() {
+ return description;
+ }
+
+ @Nullable
+ public List<Action> getActions() {
+ return actions;
+ }
+
+ public boolean isModal() {
+ return modal;
+ }
+
+ public VoicemailErrorMessage setModal(boolean value) {
+ modal = value;
+ return this;
+ }
+
+ public VoicemailErrorMessage(CharSequence title, CharSequence description, Action... actions) {
+ this(title, description, Arrays.asList(actions));
+ }
+
+ public VoicemailErrorMessage(
+ CharSequence title, CharSequence description, @Nullable List<Action> actions) {
+ this.title = title;
+ this.description = description;
+ this.actions = actions;
+ }
+
+ @NonNull
+ public static Action createChangeAirplaneModeAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_turn_off_airplane_mode),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Settings.ACTION_AIRPLANE_MODE_SETTINGS);
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createSetPinAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_set_pin),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_ALERT_SET_PIN_CLICKED);
+ Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createCallVoicemailAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_call_voicemail),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_CALL, CallUtil.getVoicemailUri());
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createSyncAction(final Context context, final VoicemailStatus status) {
+ return new Action(
+ context.getString(R.string.voicemail_action_sync),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+ intent.setPackage(status.sourcePackage);
+ context.sendBroadcast(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createRetryAction(final Context context, final VoicemailStatus status) {
+ return new Action(
+ context.getString(R.string.voicemail_action_retry),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+ intent.setPackage(status.sourcePackage);
+ context.sendBroadcast(intent);
+ }
+ });
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
new file mode 100644
index 0000000..5ebef80
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+
+/**
+ * Given a VoicemailStatus, {@link VoicemailErrorMessageCreator#create(Context, VoicemailStatus)}
+ * will return a {@link VoicemailErrorMessage} representing the message to be shown to the user, or
+ * <code>null</code> if no message should be shown.
+ */
+public class VoicemailErrorMessageCreator {
+
+ @Nullable
+ public VoicemailErrorMessage create(
+ Context context, VoicemailStatus status, VoicemailStatusReader statusReader) {
+ // Never return error message before NMR1. Voicemail status is not supported on those.
+ if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
+ return null;
+ }
+ switch (status.type) {
+ case Vvm3VoicemailMessageCreator.VVM_TYPE_VVM3:
+ return Vvm3VoicemailMessageCreator.create(context, status, statusReader);
+ default:
+ return OmtpVoicemailMessageCreator.create(context, status);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
new file mode 100644
index 0000000..a09941d
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
@@ -0,0 +1,260 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import com.android.dialer.database.VoicemailStatusQuery;
+
+/** Structured data from {@link android.provider.VoicemailContract.Status} */
+public class VoicemailStatus {
+
+ public final String sourcePackage;
+ public final String type;
+
+ public final String phoneAccountComponentName;
+ public final String phoneAccountId;
+
+ @Nullable public final Uri settingsUri;
+ @Nullable public final Uri voicemailAccessUri;
+
+ public final int configurationState;
+ public final int dataChannelState;
+ public final int notificationChannelState;
+
+ public final int quotaOccupied;
+ public final int quotaTotal;
+
+ // System status
+
+ public final boolean isAirplaneMode;
+
+ /** Wraps the row currently pointed by <code>statusCursor</code> */
+ public VoicemailStatus(Context context, Cursor statusCursor) {
+ sourcePackage = getString(statusCursor, VoicemailStatusQuery.SOURCE_PACKAGE_INDEX, "");
+
+ settingsUri = getUri(statusCursor, VoicemailStatusQuery.SETTINGS_URI_INDEX);
+ voicemailAccessUri = getUri(statusCursor, VoicemailStatusQuery.VOICEMAIL_ACCESS_URI_INDEX);
+
+ configurationState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.CONFIGURATION_STATE_INDEX,
+ Status.CONFIGURATION_STATE_NOT_CONFIGURED);
+ dataChannelState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.DATA_CHANNEL_STATE_INDEX,
+ Status.DATA_CHANNEL_STATE_NO_CONNECTION);
+ notificationChannelState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.NOTIFICATION_CHANNEL_STATE_INDEX,
+ Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+
+ isAirplaneMode =
+ Settings.System.getInt(context.getContentResolver(), Global.AIRPLANE_MODE_ON, 0) != 0;
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ quotaOccupied =
+ getInt(statusCursor, VoicemailStatusQuery.QUOTA_OCCUPIED_INDEX, Status.QUOTA_UNAVAILABLE);
+ quotaTotal =
+ getInt(statusCursor, VoicemailStatusQuery.QUOTA_TOTAL_INDEX, Status.QUOTA_UNAVAILABLE);
+ } else {
+ quotaOccupied = Status.QUOTA_UNAVAILABLE;
+ quotaTotal = Status.QUOTA_UNAVAILABLE;
+ }
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ type =
+ getString(
+ statusCursor, VoicemailStatusQuery.SOURCE_TYPE_INDEX, TelephonyManager.VVM_TYPE_OMTP);
+ phoneAccountComponentName =
+ getString(statusCursor, VoicemailStatusQuery.PHONE_ACCOUNT_COMPONENT_NAME, "");
+ phoneAccountId = getString(statusCursor, VoicemailStatusQuery.PHONE_ACCOUNT_ID, "");
+ } else {
+ type = TelephonyManager.VVM_TYPE_OMTP;
+ phoneAccountComponentName = "";
+ phoneAccountId = "";
+ }
+ }
+
+ private VoicemailStatus(Builder builder) {
+ sourcePackage = builder.sourcePackage;
+ phoneAccountComponentName = builder.phoneAccountComponentName;
+ phoneAccountId = builder.phoneAccountId;
+ type = builder.type;
+ settingsUri = builder.settingsUri;
+ voicemailAccessUri = builder.voicemailAccessUri;
+ configurationState = builder.configurationState;
+ dataChannelState = builder.dataChannelState;
+ notificationChannelState = builder.notificationChannelState;
+ quotaOccupied = builder.quotaOccupied;
+ quotaTotal = builder.quotaTotal;
+ isAirplaneMode = builder.isAirplaneMode;
+ }
+
+ static class Builder {
+
+ private String sourcePackage = "";
+ private String type = TelephonyManager.VVM_TYPE_OMTP;
+ private String phoneAccountComponentName = "";
+ private String phoneAccountId = "";
+
+ @Nullable private Uri settingsUri;
+ @Nullable private Uri voicemailAccessUri;
+
+ private int configurationState = Status.CONFIGURATION_STATE_NOT_CONFIGURED;
+ private int dataChannelState = Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+ private int notificationChannelState = Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+
+ private int quotaOccupied = Status.QUOTA_UNAVAILABLE;
+ private int quotaTotal = Status.QUOTA_UNAVAILABLE;
+
+ private boolean isAirplaneMode;
+
+ public VoicemailStatus build() {
+ return new VoicemailStatus(this);
+ }
+
+ public Builder setSourcePackage(String sourcePackage) {
+ this.sourcePackage = sourcePackage;
+ return this;
+ }
+
+ public Builder setType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder setPhoneAccountComponentName(String name) {
+ this.phoneAccountComponentName = name;
+ return this;
+ }
+
+ public Builder setPhoneAccountId(String id) {
+ this.phoneAccountId = id;
+ return this;
+ }
+
+ public Builder setSettingsUri(Uri settingsUri) {
+ this.settingsUri = settingsUri;
+ return this;
+ }
+
+ public Builder setVoicemailAccessUri(Uri voicemailAccessUri) {
+ this.voicemailAccessUri = voicemailAccessUri;
+ return this;
+ }
+
+ public Builder setConfigurationState(int configurationState) {
+ this.configurationState = configurationState;
+ return this;
+ }
+
+ public Builder setDataChannelState(int dataChannelState) {
+ this.dataChannelState = dataChannelState;
+ return this;
+ }
+
+ public Builder setNotificationChannelState(int notificationChannelState) {
+ this.notificationChannelState = notificationChannelState;
+ return this;
+ }
+
+ public Builder setQuotaOccupied(int quotaOccupied) {
+ this.quotaOccupied = quotaOccupied;
+ return this;
+ }
+
+ public Builder setQuotaTotal(int quotaTotal) {
+ this.quotaTotal = quotaTotal;
+ return this;
+ }
+
+ public Builder setAirplaneMode(boolean isAirplaneMode) {
+ this.isAirplaneMode = isAirplaneMode;
+ return this;
+ }
+ }
+
+ public boolean isActive() {
+ switch (configurationState) {
+ case Status.CONFIGURATION_STATE_NOT_CONFIGURED:
+ case Status.CONFIGURATION_STATE_DISABLED:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "VoicemailStatus["
+ + "sourcePackage: "
+ + sourcePackage
+ + ", type:"
+ + type
+ + ", settingsUri: "
+ + settingsUri
+ + ", voicemailAccessUri: "
+ + voicemailAccessUri
+ + ", configurationState: "
+ + configurationState
+ + ", dataChannelState: "
+ + dataChannelState
+ + ", notificationChannelState: "
+ + notificationChannelState
+ + ", quotaOccupied: "
+ + quotaOccupied
+ + ", quotaTotal: "
+ + quotaTotal
+ + ", isAirplaneMode: "
+ + isAirplaneMode
+ + "]";
+ }
+
+ @Nullable
+ private static Uri getUri(Cursor cursor, int index) {
+ if (cursor.getString(index) != null) {
+ return Uri.parse(cursor.getString(index));
+ }
+ return null;
+ }
+
+ private static int getInt(Cursor cursor, int index, int defaultValue) {
+ if (cursor.isNull(index)) {
+ return defaultValue;
+ }
+ return cursor.getInt(index);
+ }
+
+ private static String getString(Cursor cursor, int index, String defaultValue) {
+ if (cursor.isNull(index)) {
+ return defaultValue;
+ }
+ return cursor.getString(index);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
new file mode 100644
index 0000000..6f41121
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
@@ -0,0 +1,114 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Status;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+
+/**
+ * This class will detect the corruption in the voicemail status and log it so we can track how many
+ * users are affected.
+ */
+public class VoicemailStatusCorruptionHandler {
+
+ /** Where the check is made so logging can be done. */
+ public enum Source {
+ Activity,
+ Notification
+ }
+
+ private static final String CONFIG_VVM_STATUS_FIX_DISABLED = "vvm_status_fix_disabled";
+
+ public static void maybeFixVoicemailStatus(Context context, Cursor statusCursor, Source source) {
+
+ if (ConfigProviderBindings.get(context).getBoolean(CONFIG_VVM_STATUS_FIX_DISABLED, false)) {
+ return;
+ }
+
+ if (VERSION.SDK_INT != VERSION_CODES.N_MR1) {
+ // This issue is specific to N MR1, it is fixed in future SDK.
+ return;
+ }
+
+ if (statusCursor.getCount() == 0) {
+ return;
+ }
+
+ statusCursor.moveToFirst();
+ VoicemailStatus status = new VoicemailStatus(context, statusCursor);
+ PhoneAccountHandle phoneAccountHandle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(status.phoneAccountComponentName),
+ status.phoneAccountId);
+
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+
+ boolean visualVoicemailEnabled =
+ TelephonyManagerCompat.isVisualVoicemailEnabled(telephonyManager, phoneAccountHandle);
+ LogUtil.i(
+ "VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus",
+ "Source="
+ + source
+ + ", CONFIGURATION_STAIE="
+ + status.configurationState
+ + ", visualVoicemailEnabled="
+ + visualVoicemailEnabled);
+
+ // If visual voicemail is enabled, the CONFIGURATION_STATE should be either OK, PIN_NOT_SET,
+ // or other failure code. CONFIGURATION_STATE_NOT_CONFIGURED means that the client has been
+ // shut down improperly (b/32371710). The client should be reset or the VVM tab will be
+ // missing.
+ if (Status.CONFIGURATION_STATE_NOT_CONFIGURED == status.configurationState
+ && visualVoicemailEnabled) {
+ LogUtil.e(
+ "VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus",
+ "VVM3 voicemail status corrupted");
+
+ switch (source) {
+ case Activity:
+ Logger.get(context)
+ .logImpression(
+ DialerImpression.Type
+ .VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY);
+ break;
+ case Notification:
+ Logger.get(context)
+ .logImpression(
+ DialerImpression.Type
+ .VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION);
+ break;
+ default:
+ Assert.fail("this should never happen");
+ break;
+ }
+ // At this point we could attempt to work around the issue by disabling and re-enabling
+ // voicemail. Unfortunately this work around is buggy so we'll do nothing for now.
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java
new file mode 100644
index 0000000..fd9e7ef
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java
@@ -0,0 +1,25 @@
+/*
+ * 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.app.voicemail.error;
+
+/**
+ * A source that is generating the voicemail status to show error messages, used by {@link
+ * VoicemailErrorMessageCreator} to inform the source that the status should be updated
+ */
+public interface VoicemailStatusReader {
+ void refresh();
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java
new file mode 100644
index 0000000..86b1244
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java
@@ -0,0 +1,25 @@
+/*
+ * 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.app.voicemail.error;
+
+/** Voicemail TOS message. */
+public class VoicemailTosMessage extends VoicemailErrorMessage {
+
+ public VoicemailTosMessage(CharSequence title, CharSequence description, Action... actions) {
+ super(title, description, actions);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
new file mode 100644
index 0000000..6e9405c
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
@@ -0,0 +1,428 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import java.util.Locale;
+
+/**
+ * Create error message from {@link VoicemailStatus} for VVM3 visual voicemail. VVM3 is used only by
+ * Verizon Wireless.
+ */
+@RequiresApi(VERSION_CODES.N_MR1)
+public class Vvm3VoicemailMessageCreator {
+
+ public static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
+
+ // Copied from com.android.phone.vvm.omtp.protocol.Vvm3EventHandler
+ // TODO(b/28380841): unbundle VVM client so we can access these values directly
+ public static final int VMS_DNS_FAILURE = -9001;
+ public static final int VMG_DNS_FAILURE = -9002;
+ public static final int SPG_DNS_FAILURE = -9003;
+ public static final int VMS_NO_CELLULAR = -9004;
+ public static final int VMG_NO_CELLULAR = -9005;
+ public static final int SPG_NO_CELLULAR = -9006;
+ public static final int VMS_TIMEOUT = -9007;
+ public static final int VMG_TIMEOUT = -9008;
+ public static final int STATUS_SMS_TIMEOUT = -9009;
+
+ public static final int SUBSCRIBER_BLOCKED = -9990;
+ public static final int UNKNOWN_USER = -9991;
+ public static final int UNKNOWN_DEVICE = -9992;
+ public static final int INVALID_PASSWORD = -9993;
+ public static final int MAILBOX_NOT_INITIALIZED = -9994;
+ public static final int SERVICE_NOT_PROVISIONED = -9995;
+ public static final int SERVICE_NOT_ACTIVATED = -9996;
+ public static final int USER_BLOCKED = -9998;
+ public static final int IMAP_GETQUOTA_ERROR = -9997;
+ public static final int IMAP_SELECT_ERROR = -9989;
+ public static final int IMAP_ERROR = -9999;
+
+ public static final int VMG_INTERNAL_ERROR = -101;
+ public static final int VMG_DB_ERROR = -102;
+ public static final int VMG_COMMUNICATION_ERROR = -103;
+ public static final int SPG_URL_NOT_FOUND = -301;
+
+ // Non VVM3 codes:
+ public static final int VMG_UNKNOWN_ERROR = -1;
+ public static final int PIN_NOT_SET = -100;
+ public static final int SUBSCRIBER_UNKNOWN = -99;
+
+ private static final String ISO639_SPANISH = "es";
+ @VisibleForTesting static final String VVM3_TOS_ACCEPTANCE_FLAG_KEY = "vvm3_tos_acceptance_flag";
+
+ @Nullable
+ public static VoicemailErrorMessage create(
+ final Context context,
+ final VoicemailStatus status,
+ final VoicemailStatusReader statusReader) {
+ VoicemailErrorMessage tosMessage = maybeShowTosMessage(context, status, statusReader);
+ if (tosMessage != null) {
+ return tosMessage;
+ }
+
+ if (VMS_DNS_FAILURE == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_DNS_FAILURE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SPG_DNS_FAILURE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_spg_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_spg_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMS_NO_CELLULAR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_NO_CELLULAR == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SPG_NO_CELLULAR == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_spg_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_spg_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMS_TIMEOUT == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_TIMEOUT == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (STATUS_SMS_TIMEOUT == status.notificationChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_status_sms_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_status_sms_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SUBSCRIBER_BLOCKED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_subscriber_blocked_title),
+ getCustomerSupportString(context, R.string.vvm3_error_subscriber_blocked_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (UNKNOWN_USER == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_unknown_user_title),
+ getCustomerSupportString(context, R.string.vvm3_error_unknown_user_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (UNKNOWN_DEVICE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_unknown_device_title),
+ getCustomerSupportString(context, R.string.vvm3_error_unknown_device_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (INVALID_PASSWORD == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_invalid_password_title),
+ getCustomerSupportString(context, R.string.vvm3_error_invalid_password_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (MAILBOX_NOT_INITIALIZED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_mailbox_not_initialized_title),
+ getCustomerSupportString(context, R.string.vvm3_error_mailbox_not_initialized_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SERVICE_NOT_PROVISIONED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_service_not_provisioned_title),
+ getCustomerSupportString(context, R.string.vvm3_error_service_not_provisioned_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SERVICE_NOT_ACTIVATED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_service_not_activated_title),
+ getCustomerSupportString(context, R.string.vvm3_error_service_not_activated_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (USER_BLOCKED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_user_blocked_title),
+ getCustomerSupportString(context, R.string.vvm3_error_user_blocked_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SUBSCRIBER_UNKNOWN == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_subscriber_unknown_title),
+ getCustomerSupportString(context, R.string.vvm3_error_subscriber_unknown_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_GETQUOTA_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_getquota_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_getquota_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_SELECT_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_select_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_select_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (PIN_NOT_SET == status.configurationState) {
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_ALERT_SET_PIN_SHOWN);
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_pin_not_set_title),
+ getCustomerSupportString(context, R.string.voicemail_error_pin_not_set_message),
+ VoicemailErrorMessage.createSetPinAction(context));
+ }
+
+ return OmtpVoicemailMessageCreator.create(context, status);
+ }
+
+ @NonNull
+ private static CharSequence getCustomerSupportString(Context context, int id) {
+ // TODO: get number based on the country the user is currently in.
+ return ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ context.getResources(),
+ id,
+ context.getString(R.string.verizon_domestic_customer_support_display_number));
+ }
+
+ @NonNull
+ private static Action createCallCustomerSupportAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_call_customer_support),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent =
+ new Intent(
+ Intent.ACTION_CALL,
+ Uri.parse(
+ "tel:"
+ + context.getString(
+ R.string.verizon_domestic_customer_support_number)));
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage maybeShowTosMessage(
+ final Context context,
+ final VoicemailStatus status,
+ final VoicemailStatusReader statusReader) {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (preferences.getBoolean(VVM3_TOS_ACCEPTANCE_FLAG_KEY, false)) {
+ return null;
+ }
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_SHOWN);
+
+ CharSequence termsAndConditions;
+ CharSequence acceptText;
+ CharSequence declineText;
+ // TODO(b/29082671): use LocaleList
+ if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_SPANISH).getLanguage())) {
+ // Spanish
+ termsAndConditions = context.getString(R.string.verizon_terms_and_conditions_1_1_spanish);
+ acceptText = context.getString(R.string.verizon_terms_and_conditions_accept_spanish);
+ declineText = context.getString(R.string.verizon_terms_and_conditions_decline_spanish);
+ } else {
+ termsAndConditions = context.getString(R.string.verizon_terms_and_conditions_1_1_english);
+ acceptText = context.getString(R.string.verizon_terms_and_conditions_accept_english);
+ declineText = context.getString(R.string.verizon_terms_and_conditions_decline_english);
+ }
+
+ return new VoicemailTosMessage(
+ context.getString(R.string.verizon_terms_and_conditions_title),
+ context.getString(R.string.verizon_terms_and_conditions_message, termsAndConditions),
+ new Action(
+ declineText,
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtil.i("Vvm3VoicemailMessageCreator.maybeShowTosMessage", "decline clicked");
+ PhoneAccountHandle handle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(status.phoneAccountComponentName),
+ status.phoneAccountId);
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINE_CLICKED);
+ showDeclineTosDialog(context, handle, status);
+ }
+ }),
+ new Action(
+ acceptText,
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtil.i("Vvm3VoicemailMessageCreator.maybeShowTosMessage", "accept clicked");
+ preferences.edit().putBoolean(VVM3_TOS_ACCEPTANCE_FLAG_KEY, true).apply();
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_ACCEPTED);
+ statusReader.refresh();
+ }
+ },
+ true /* raised */))
+ .setModal(true);
+ }
+
+ private static void showDeclineTosDialog(
+ final Context context, final PhoneAccountHandle handle, VoicemailStatus status) {
+ if (PIN_NOT_SET == status.configurationState) {
+ LogUtil.i(
+ "Vvm3VoicemailMessageCreator.showDeclineTosDialog",
+ "PIN_NOT_SET, showing set PIN dialog");
+ showSetPinBeforeDeclineDialog(context);
+ return;
+ }
+ LogUtil.i(
+ "Vvm3VoicemailMessageCreator.showDeclineTosDialog",
+ "showing decline ToS dialog, status=" + status);
+ final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.verizon_terms_and_conditions_decline_dialog_message);
+ builder.setPositiveButton(
+ R.string.verizon_terms_and_conditions_decline_dialog_downgrade,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINED);
+ TelephonyManagerCompat.setVisualVoicemailEnabled(telephonyManager, handle, false);
+ }
+ });
+
+ builder.setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ builder.setCancelable(true);
+ builder.show();
+ }
+
+ private static void showSetPinBeforeDeclineDialog(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.verizon_terms_and_conditions_decline_set_pin_dialog_message);
+ builder.setPositiveButton(
+ R.string.verizon_terms_and_conditions_decline_set_pin_dialog_set_pin,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINE_CHANGE_PIN_SHOWN);
+ Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+ context.startActivity(intent);
+ }
+ });
+
+ builder.setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ builder.setCancelable(true);
+ builder.show();
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
new file mode 100644
index 0000000..0dfb1c2
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
@@ -0,0 +1,114 @@
+<?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.
+-->
+<android.support.v7.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/error_card"
+ style="@style/CallLogCardStyle"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/error_card_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/alert_main_padding"
+ android:layout_marginStart="@dimen/alert_main_padding"
+ android:layout_marginEnd="@dimen/alert_main_padding"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/error_card_header"
+ android:textStyle="bold"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/alert_title_padding"
+ android:layout_gravity="center_vertical"
+ android:singleLine="false"
+ android:textColor="@color/primary_text_color"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+
+ <TextView
+ android:id="@+id/error_card_details"
+ android:autoLink="web"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lineSpacingExtra="@dimen/alert_line_spacing"
+ android:singleLine="false"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/error_actions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:paddingTop="@dimen/alert_action_vertical_padding"
+ android:paddingBottom="@dimen/alert_action_vertical_padding"
+ android:paddingStart="@dimen/alert_action_horizontal_padding"
+ android:paddingEnd="@dimen/alert_action_horizontal_padding"
+ android:gravity="start"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/primary_action_raised"
+ style="@style/RaisedErrorActionStyle"
+ android:nextFocusLeft="@+id/promo_card"
+ android:nextFocusRight="@+id/primary_action"
+ android:clickable="true"
+ />
+
+ <TextView
+ android:id="@+id/primary_action"
+ style="@style/ErrorActionStyle"
+ android:background="?android:attr/selectableItemBackground"
+ android:nextFocusLeft="@+id/promo_card"
+ android:nextFocusRight="@+id/secondary_action"
+ android:clickable="true"
+ />
+
+ <TextView
+ android:id="@+id/secondary_action"
+ style="@style/ErrorActionStyle"
+ android:paddingEnd="@dimen/alert_action_between_padding"
+ android:background="?android:attr/selectableItemBackground"
+ android:nextFocusLeft="@+id/primary_action"
+ android:nextFocusRight="@+id/promo_card"
+ android:clickable="true"/>
+
+ <android.support.v4.widget.Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <TextView
+ android:id="@+id/secondary_action_raised"
+ style="@style/RaisedErrorActionStyle"
+ android:paddingEnd="@dimen/alert_action_between_padding"
+ android:layout_marginEnd="8dp"
+ android:nextFocusLeft="@+id/primary_action"
+ android:nextFocusRight="@+id/promo_card"
+ android:clickable="true"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</android.support.v7.widget.CardView>
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
new file mode 100644
index 0000000..2b9d173
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <ScrollView
+ android:id="@+id/voicemail_tos_message"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/tos_message_title"
+ android:textStyle="bold"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="24dp"
+ android:paddingBottom="12dp"
+ android:text="@string/verizon_terms_and_conditions_title"
+ android:textColor="@color/primary_text_color"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+ <TextView
+ android:id="@+id/tos_message_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:autoLink="web"
+ android:text="@string/verizon_terms_and_conditions_1.1_english"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ </LinearLayout>
+ </ScrollView>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#D2D2D2"/>
+
+ <LinearLayout
+ android:id="@+id/voicemail_tos_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/voicemail_tos_button_decline"
+ style="@style/ErrorActionStyle"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/verizon_terms_and_conditions_decline_english"/>
+ <android.support.v4.widget.Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+ <TextView
+ android:id="@+id/voicemail_tos_button_accept"
+ style="@style/RaisedErrorActionStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/verizon_terms_and_conditions_accept_english"/>
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml b/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml
new file mode 100644
index 0000000..20dd40a
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="alert_icon_size">24dp</dimen>
+ <dimen name="alert_start_padding">16dp</dimen>
+ <dimen name="alert_top_padding">21dp</dimen>
+ <dimen name="alert_main_padding">24dp</dimen>
+ <dimen name="alert_title_padding">12dp</dimen>
+ <dimen name="alert_action_vertical_padding">4dp</dimen>
+ <dimen name="alert_action_horizontal_padding">4dp</dimen>
+ <dimen name="alert_action_between_padding">11dp</dimen>
+ <dimen name="alert_line_spacing">4dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
new file mode 100644
index 0000000..1d39b9d
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
@@ -0,0 +1,176 @@
+<!--
+ ~ 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="voicemail_error_turn_off_airplane_mode_title">Turn off airplane mode</string>
+
+ <string name="voicemail_error_activating_title">Activating visual voicemail</string>
+ <string name="voicemail_error_activating_message">You might not receive voicemail notifications until visual voicemail is fully activated. Call voicemail to retrieve new messages until voicemail is fully activated.</string>
+
+ <string name="voicemail_error_not_activate_no_signal_title">Can\'t activate visual voicemail</string>
+ <string name="voicemail_error_not_activate_no_signal_message">Make sure your phone has cellular connection and try again.</string>
+ <string name="voicemail_error_not_activate_no_signal_airplane_mode_message">Turn off airplane mode and try again.</string>
+
+ <string name="voicemail_error_no_signal_title">No connection</string>
+ <string name="voicemail_error_no_signal_message">You won\'t be notified for new voicemails. If you\'re on Wi-Fi, you can check for voicemail by syncing now.</string>
+ <string name="voicemail_error_no_signal_airplane_mode_message">You won\'t be notified for new voicemails. Turn off airplane mode to sync your voicemail.</string>
+ <string name="voicemail_error_no_signal_cellular_required_message">Your phone needs a cellular data connection to check voicemail.</string>
+
+ <string name="voicemail_error_activation_failed_title">Can\'t activate visual voicemail</string>
+ <string name="voicemail_error_activation_failed_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_no_data_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_no_data_message">Try again when your Wi-Fi or cellular connection is better. You can still call to check voicemail.</string>
+ <string name="voicemail_error_no_data_cellular_required_message">Try again when your cellular data connection is better. You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_bad_config_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_bad_config_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_communication_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_communication_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_server_connection_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_server_connection_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_server_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_server_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_inbox_near_full_title">Inbox almost full</string>
+ <string name="voicemail_error_inbox_near_full_message">You won\'t be able to receive new voicemail if your inbox is full.</string>
+
+ <string name="voicemail_error_inbox_full_title">Can\'t receive new voicemails</string>
+ <string name="voicemail_error_inbox_full_message">Your inbox is full. Try deleting some messages to receive new voicemail.</string>
+
+
+ <string name="voicemail_error_pin_not_set_title">Set your voicemail PIN</string>
+ <string name="voicemail_error_pin_not_set_message">You\'ll need a voicemail PIN anytime you call to access your voicemail.</string>
+
+ <string name="voicemail_error_unknown_title">Unknown error</string>
+
+ <string name="voicemail_action_turn_off_airplane_mode">Airplane Mode Settings</string>
+ <string name="voicemail_action_set_pin">Set PIN</string>
+ <string name="voicemail_action_retry">Try Again</string>
+ <string name="voicemail_action_sync">Sync</string>
+ <string name="voicemail_action_call_voicemail">Call Voicemail</string>
+ <string name="voicemail_action_call_customer_support">Call Customer Support</string>
+
+ <string name="vvm3_error_vms_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_vms_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9001.</string>
+
+ <string name="vvm3_error_vmg_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_vmg_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9002.</string>
+
+ <string name="vvm3_error_spg_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_spg_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9003.</string>
+
+ <string name="vvm3_error_vms_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_vms_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9004.</string>
+
+ <string name="vvm3_error_vmg_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_vmg_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9005.</string>
+
+ <string name="vvm3_error_spg_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_spg_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9006.</string>
+
+ <string name="vvm3_error_vms_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_vms_timeout_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9007.</string>
+
+ <string name="vvm3_error_vmg_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_vmg_timeout_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9008.</string>
+
+ <string name="vvm3_error_status_sms_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_status_sms_timeout_message">Sorry, we\'re having trouble setting up your service. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9009.</string>
+
+ <string name="vvm3_error_subscriber_blocked_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_subscriber_blocked_message">Sorry, we\'re not able to connect to your voice mailbox at this time. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9990."</string>
+
+ <string name="vvm3_error_unknown_user_title">Set Up Voice Mail</string>
+ <string name="vvm3_error_unknown_user_message">Voicemail is not set up on your account. Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9991.</string>
+
+ <string name="vvm3_error_unknown_device_title">Voice Mail</string>
+ <string name="vvm3_error_unknown_device_message">Visual Voicemail cannot be used on this device. Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9992.</string>
+
+ <string name="vvm3_error_invalid_password_title">Something Went Wrong</string>
+ <string name="vvm3_error_invalid_password_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9993.</string>
+
+ <string name="vvm3_error_mailbox_not_initialized_title">Visual Voice Mail</string>
+ <string name="vvm3_error_mailbox_not_initialized_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9994.</string>
+
+ <string name="vvm3_error_service_not_provisioned_title">Visual Voice Mail</string>
+ <string name="vvm3_error_service_not_provisioned_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9995.</string>
+
+ <string name="vvm3_error_service_not_activated_title">Visual Voice Mail</string>
+ <string name="vvm3_error_service_not_activated_message">To activate Visual Voice Mail, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9996.</string>
+
+ <string name="vvm3_error_user_blocked_title">Something Went Wrong</string>
+ <string name="vvm3_error_user_blocked_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9998.</string>
+
+ <string name="vvm3_error_subscriber_unknown_title">Visual Voicemail is Disabled</string>
+ <string name="vvm3_error_subscriber_unknown_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> to activate visual voicemail.</string>
+
+ <string name="vvm3_error_imap_getquota_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_getquota_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9997.</string>
+
+ <string name="vvm3_error_imap_select_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_select_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9989.</string>
+
+ <string name="vvm3_error_imap_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9999.</string>
+
+ <string translatable="false" name="verizon_domestic_customer_support_number">+18009220204</string>
+ <string translatable="false" name="verizon_domestic_customer_support_display_number">(800) 922–0204</string>
+
+ <string name="verizon_terms_and_conditions_title">Visual Voicemail Terms and Conditions</string>
+ <string name="verizon_terms_and_conditions_message">You must accept Verizon Wireless\'s terms and conditions to use visual voicemail:\n\n%s</string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_1.1_english">
+Visual Voice Mail (VVM) is a service that provides access to voice mail messages directly on the device, without the need to call *86. This service requires traditional Voice Mail but does not support all traditional Voice Mail features, which you can access by dialing *86 from your handset. Use of this feature will be billed on a per-megabyte basis, or according to any data package you have. Mobile to mobile minutes do not apply. Standard rates apply to any calls, emails or messages initiated from Visual Voice Mail.\n
+\n
+You may disable VVM in settings. This will revert you to basic voice mail. In some cases you may need to call customer care to cancel and if you cancel Visual Voice Mail you may lose all stored voice mails and information.\n
+\n
+For the Premium Visual Voice Mail service, some voice messages may not be completely transcribed; incomplete messages will end with [...]. Only the first 45 seconds of each voice message will be transcribed, so for longer messages, you will need to listen to the voice message itself. Any profane or offensive language also will not be transcribed and will appear as [...] in the transcription.\n
+\n
+Speech recordings may be collected and stored for a period of 30 days, solely for the purpose of testing and improving transcription technology and performance, subject to the Verizon Wireless Privacy Policy, which can be found at http://www.verizon.com/about/privacy/policy/\n
+\n
+You understand that by selecting ACCEPT, your messages will be stored and anyone in possession of this device will have access to your voice mail. You further understand that your voice mail messages may be stored in electronic format on this device. To limit unauthorized access to your voice mail, you should consider locking your phone when not in use. Not available in all areas or over Wi-Fi.\n
+\n
+If you do not accept all of these terms and conditions, do not use Visual Voice Mail. </string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_1.1_spanish">
+El buzón de voz visual (VVM) es un servicio que permite acceder a los mensajes del buzón de voz directamente en el dispositivo, sin necesidad de llamar al *86. Este servicio requiere el buzón de voz tradicional, pero no admite todas las funciones del buzón de voz tradicional, a las que se puede acceder marcando *86 en el teléfono. El uso de esta función se factura por megabyte o conforme a cualquier paquete de datos que tenga. No se aplican los minutos de un dispositivo móvil a otro. Se aplican tarifas estándar a todos los correos electrónicos, las llamadas o los mensajes originados en el buzón de voz visual.\n
+\n
+Puede inhabilitar el VVM en la configuración. Esto le permite volver al buzón de voz básico. En algunos casos, es posible que deba llamar al servicio de atención al cliente para cancelar el buzón de voz visual. Si lo cancela, puede perder la información y los mensajes de voz almacenados.\n
+\n
+En el caso del servicio de buzón de voz visual premium, es posible que algunos mensajes no se transcriban totalmente; los mensajes incompletos finalizan con "[…]". Solo se transcriben los primeros 45 segundos de cada mensaje de voz, por lo que debe escuchar los mensajes de voz más largos. Tampoco se transcribe ninguna palabra ofensiva o profana; aparece como "[…]" en la transcripción.\n
+\n
+Es posible que reunamos y almacenemos grabaciones de voz durante 30 días, con el único fin de probar y mejorar el rendimiento y la tecnología de la transcripción, sujeto a la Política de privacidad de Verizon Wireless, disponible en http://www.verizon.com/about/privacy/policy/.\n
+\n
+Entiende que, al seleccionar ACEPTAR, sus mensajes se almacenarán, y cualquier persona que disponga de este dispositivo tendrá acceso al buzón de voz. Entiende, además, que los mensajes de voz pueden almacenarse en formato electrónico en este dispositivo. Para limitar el acceso no autorizado al buzón de voz, debe considerar el bloqueo del teléfono cuando no está en uso. No está disponible en todas las áreas ni mediante Wi-Fi.\n
+\n
+Si no acepta todos estos términos y condiciones, no use el buzón de voz visual.
+ </string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_accept_english">Accept</string>
+ <string translatable="false" name="verizon_terms_and_conditions_accept_spanish">Aceptar</string>
+ <string translatable="false" name="verizon_terms_and_conditions_decline_english">Decline</string>
+ <string translatable="false" name="verizon_terms_and_conditions_decline_spanish">Rechazar</string>
+
+ <string name="verizon_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string>
+ <string name="verizon_terms_and_conditions_decline_dialog_downgrade">Disable visual voicemail</string>
+
+ <string name="verizon_terms_and_conditions_decline_set_pin_dialog_message">Voicemail will only be accessible by calling *86. Set a new voicemail PIN to proceed.</string>
+ <string name="verizon_terms_and_conditions_decline_set_pin_dialog_set_pin">Set PIN</string>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/styles.xml b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
new file mode 100644
index 0000000..c4a8542
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="ErrorActionStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:gravity">end|center_vertical</item>
+ <item name="android:paddingStart">8dp</item>
+ <item name="android:paddingEnd">8dp</item>
+ <item name="android:layout_marginStart">8dp</item>
+ <item name="android:layout_marginEnd">8dp</item>
+ <item name="android:textColor">@color/dialtacts_theme_color</item>
+ <item name="android:fontFamily">"sans-serif-medium"</item>
+ <item name="android:focusable">true</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="RaisedErrorActionStyle" parent="Widget.AppCompat.Button.Colored">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:colorButtonNormal">@color/dialer_theme_color</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:layout_height">@dimen/call_log_action_height</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/app/widget/ActionBarController.java b/java/com/android/dialer/app/widget/ActionBarController.java
new file mode 100644
index 0000000..7fe056c
--- /dev/null
+++ b/java/com/android/dialer/app/widget/ActionBarController.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2013 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.app.widget;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import com.android.dialer.animation.AnimUtils.AnimationCallback;
+import com.android.dialer.app.DialtactsActivity;
+
+/**
+ * Controls the various animated properties of the actionBar: showing/hiding, fading/revealing, and
+ * collapsing/expanding, and assigns suitable properties to the actionBar based on the current state
+ * of the UI.
+ */
+public class ActionBarController {
+
+ public static final boolean DEBUG = DialtactsActivity.DEBUG;
+ public static final String TAG = "ActionBarController";
+ private static final String KEY_IS_SLID_UP = "key_actionbar_is_slid_up";
+ private static final String KEY_IS_FADED_OUT = "key_actionbar_is_faded_out";
+ private static final String KEY_IS_EXPANDED = "key_actionbar_is_expanded";
+
+ private ActivityUi mActivityUi;
+ private SearchEditTextLayout mSearchBox;
+
+ private boolean mIsActionBarSlidUp;
+
+ private final AnimationCallback mFadeOutCallback =
+ new AnimationCallback() {
+ @Override
+ public void onAnimationEnd() {
+ slideActionBar(true /* slideUp */, false /* animate */);
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ slideActionBar(true /* slideUp */, false /* animate */);
+ }
+ };
+
+ public ActionBarController(ActivityUi activityUi, SearchEditTextLayout searchBox) {
+ mActivityUi = activityUi;
+ mSearchBox = searchBox;
+ }
+
+ /** @return Whether or not the action bar is currently showing (both slid down and visible) */
+ public boolean isActionBarShowing() {
+ return !mIsActionBarSlidUp && !mSearchBox.isFadedOut();
+ }
+
+ /** Called when the user has tapped on the collapsed search box, to start a new search query. */
+ public void onSearchBoxTapped() {
+ if (DEBUG) {
+ Log.d(TAG, "OnSearchBoxTapped: isInSearchUi " + mActivityUi.isInSearchUi());
+ }
+ if (!mActivityUi.isInSearchUi()) {
+ mSearchBox.expand(true /* animate */, true /* requestFocus */);
+ }
+ }
+
+ /** Called when search UI has been exited for some reason. */
+ public void onSearchUiExited() {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "OnSearchUIExited: isExpanded "
+ + mSearchBox.isExpanded()
+ + " isFadedOut: "
+ + mSearchBox.isFadedOut()
+ + " shouldShowActionBar: "
+ + mActivityUi.shouldShowActionBar());
+ }
+ if (mSearchBox.isExpanded()) {
+ mSearchBox.collapse(true /* animate */);
+ }
+ if (mSearchBox.isFadedOut()) {
+ mSearchBox.fadeIn();
+ }
+
+ if (mActivityUi.shouldShowActionBar()) {
+ slideActionBar(false /* slideUp */, false /* animate */);
+ } else {
+ slideActionBar(true /* slideUp */, false /* animate */);
+ }
+ }
+
+ /**
+ * Called to indicate that the user is trying to hide the dialpad. Should be called before any
+ * state changes have actually occurred.
+ */
+ public void onDialpadDown() {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "OnDialpadDown: isInSearchUi "
+ + mActivityUi.isInSearchUi()
+ + " hasSearchQuery: "
+ + mActivityUi.hasSearchQuery()
+ + " isFadedOut: "
+ + mSearchBox.isFadedOut()
+ + " isExpanded: "
+ + mSearchBox.isExpanded());
+ }
+ if (mActivityUi.isInSearchUi()) {
+ if (mActivityUi.hasSearchQuery()) {
+ if (mSearchBox.isFadedOut()) {
+ mSearchBox.setVisible(true);
+ }
+ if (!mSearchBox.isExpanded()) {
+ mSearchBox.expand(false /* animate */, false /* requestFocus */);
+ }
+ slideActionBar(false /* slideUp */, true /* animate */);
+ } else {
+ mSearchBox.fadeIn();
+ }
+ }
+ }
+
+ /**
+ * Called to indicate that the user is trying to show the dialpad. Should be called before any
+ * state changes have actually occurred.
+ */
+ public void onDialpadUp() {
+ if (DEBUG) {
+ Log.d(TAG, "OnDialpadUp: isInSearchUi " + mActivityUi.isInSearchUi());
+ }
+ if (mActivityUi.isInSearchUi()) {
+ slideActionBar(true /* slideUp */, true /* animate */);
+ } else {
+ // From the lists fragment
+ mSearchBox.fadeOut(mFadeOutCallback);
+ }
+ }
+
+ public void slideActionBar(boolean slideUp, boolean animate) {
+ if (DEBUG) {
+ Log.d(TAG, "Sliding actionBar - up: " + slideUp + " animate: " + animate);
+ }
+ if (animate) {
+ ValueAnimator animator = slideUp ? ValueAnimator.ofFloat(0, 1) : ValueAnimator.ofFloat(1, 0);
+ animator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float value = (float) animation.getAnimatedValue();
+ setHideOffset((int) (mActivityUi.getActionBarHeight() * value));
+ }
+ });
+ animator.start();
+ } else {
+ setHideOffset(slideUp ? mActivityUi.getActionBarHeight() : 0);
+ }
+ mIsActionBarSlidUp = slideUp;
+ }
+
+ public void setAlpha(float alphaValue) {
+ mSearchBox.animate().alpha(alphaValue).start();
+ }
+
+ /** @return The offset the action bar is being translated upwards by */
+ public int getHideOffset() {
+ return mActivityUi.getActionBarHideOffset();
+ }
+
+ public void setHideOffset(int offset) {
+ mIsActionBarSlidUp = offset >= mActivityUi.getActionBarHeight();
+ mActivityUi.setActionBarHideOffset(offset);
+ }
+
+ public int getActionBarHeight() {
+ return mActivityUi.getActionBarHeight();
+ }
+
+ /** Saves the current state of the action bar into a provided {@link Bundle} */
+ public void saveInstanceState(Bundle outState) {
+ outState.putBoolean(KEY_IS_SLID_UP, mIsActionBarSlidUp);
+ outState.putBoolean(KEY_IS_FADED_OUT, mSearchBox.isFadedOut());
+ outState.putBoolean(KEY_IS_EXPANDED, mSearchBox.isExpanded());
+ }
+
+ /** Restores the action bar state from a provided {@link Bundle}. */
+ public void restoreInstanceState(Bundle inState) {
+ mIsActionBarSlidUp = inState.getBoolean(KEY_IS_SLID_UP);
+
+ final boolean isSearchBoxFadedOut = inState.getBoolean(KEY_IS_FADED_OUT);
+ if (isSearchBoxFadedOut) {
+ if (!mSearchBox.isFadedOut()) {
+ mSearchBox.setVisible(false);
+ }
+ } else if (mSearchBox.isFadedOut()) {
+ mSearchBox.setVisible(true);
+ }
+
+ final boolean isSearchBoxExpanded = inState.getBoolean(KEY_IS_EXPANDED);
+ if (isSearchBoxExpanded) {
+ if (!mSearchBox.isExpanded()) {
+ mSearchBox.expand(false, false);
+ }
+ } else if (mSearchBox.isExpanded()) {
+ mSearchBox.collapse(false);
+ }
+ }
+
+ /**
+ * This should be called after onCreateOptionsMenu has been called, when the actionbar has been
+ * laid out and actually has a height.
+ */
+ public void restoreActionBarOffset() {
+ slideActionBar(mIsActionBarSlidUp /* slideUp */, false /* animate */);
+ }
+
+ @VisibleForTesting
+ public boolean getIsActionBarSlidUp() {
+ return mIsActionBarSlidUp;
+ }
+
+ public interface ActivityUi {
+
+ boolean isInSearchUi();
+
+ boolean hasSearchQuery();
+
+ boolean shouldShowActionBar();
+
+ int getActionBarHeight();
+
+ int getActionBarHideOffset();
+
+ void setActionBarHideOffset(int offset);
+ }
+}
diff --git a/java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java b/java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java
new file mode 100644
index 0000000..85fd5ec
--- /dev/null
+++ b/java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java
@@ -0,0 +1,43 @@
+/*
+ * 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.app.widget;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+import com.android.dialer.app.R;
+import com.android.dialer.util.OrientationUtil;
+
+/** Empty content view to be shown when dialpad is visible. */
+public class DialpadSearchEmptyContentView extends EmptyContentView {
+
+ public DialpadSearchEmptyContentView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void inflateLayout() {
+ int orientation =
+ OrientationUtil.isLandscape(getContext()) ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL;
+
+ setOrientation(orientation);
+
+ final LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.empty_content_view_dialpad_search, this);
+ }
+}
diff --git a/java/com/android/dialer/app/widget/EmptyContentView.java b/java/com/android/dialer/app/widget/EmptyContentView.java
new file mode 100644
index 0000000..cfc8665
--- /dev/null
+++ b/java/com/android/dialer/app/widget/EmptyContentView.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 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.app.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.app.R;
+
+public class EmptyContentView extends LinearLayout implements View.OnClickListener {
+
+ /** Listener to call when action button is clicked. */
+ public interface OnEmptyViewActionButtonClickedListener {
+ void onEmptyViewActionButtonClicked();
+ }
+
+ public static final int NO_LABEL = 0;
+ public static final int NO_IMAGE = 0;
+
+ private ImageView mImageView;
+ private TextView mDescriptionView;
+ private TextView mActionView;
+ private OnEmptyViewActionButtonClickedListener mOnActionButtonClickedListener;
+
+ public EmptyContentView(Context context) {
+ this(context, null);
+ }
+
+ public EmptyContentView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ inflateLayout();
+
+ // Don't let touches fall through the empty view.
+ setClickable(true);
+ mImageView = (ImageView) findViewById(R.id.emptyListViewImage);
+ mDescriptionView = (TextView) findViewById(R.id.emptyListViewMessage);
+ mActionView = (TextView) findViewById(R.id.emptyListViewAction);
+ mActionView.setOnClickListener(this);
+ }
+
+ public void setDescription(int resourceId) {
+ if (resourceId == NO_LABEL) {
+ mDescriptionView.setText(null);
+ mDescriptionView.setVisibility(View.GONE);
+ } else {
+ mDescriptionView.setText(resourceId);
+ mDescriptionView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setImage(int resourceId) {
+ if (resourceId == NO_LABEL) {
+ mImageView.setImageDrawable(null);
+ mImageView.setVisibility(View.GONE);
+ } else {
+ mImageView.setImageResource(resourceId);
+ mImageView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setActionLabel(int resourceId) {
+ if (resourceId == NO_LABEL) {
+ mActionView.setText(null);
+ mActionView.setVisibility(View.GONE);
+ } else {
+ mActionView.setText(resourceId);
+ mActionView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public boolean isShowingContent() {
+ return mImageView.getVisibility() == View.VISIBLE
+ || mDescriptionView.getVisibility() == View.VISIBLE
+ || mActionView.getVisibility() == View.VISIBLE;
+ }
+
+ public void setActionClickedListener(OnEmptyViewActionButtonClickedListener listener) {
+ mOnActionButtonClickedListener = listener;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mOnActionButtonClickedListener != null) {
+ mOnActionButtonClickedListener.onEmptyViewActionButtonClicked();
+ }
+ }
+
+ protected void inflateLayout() {
+ setOrientation(LinearLayout.VERTICAL);
+ final LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.empty_content_view, this);
+ }
+
+}
diff --git a/java/com/android/dialer/app/widget/SearchEditTextLayout.java b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
new file mode 100644
index 0000000..be850f9
--- /dev/null
+++ b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2014 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.app.widget;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.app.R;
+import com.android.dialer.util.DialerUtils;
+
+public class SearchEditTextLayout extends FrameLayout {
+
+ private static final float EXPAND_MARGIN_FRACTION_START = 0.8f;
+ private static final int ANIMATION_DURATION = 200;
+ /* Subclass-visible for testing */
+ protected boolean mIsExpanded = false;
+ protected boolean mIsFadedOut = false;
+ private OnKeyListener mPreImeKeyListener;
+ private int mTopMargin;
+ private int mBottomMargin;
+ private int mLeftMargin;
+ private int mRightMargin;
+ private float mCollapsedElevation;
+ private View mCollapsed;
+ private View mExpanded;
+ private EditText mSearchView;
+ private View mSearchIcon;
+ private View mCollapsedSearchBox;
+ private View mVoiceSearchButtonView;
+ private View mOverflowButtonView;
+ private View mBackButtonView;
+ private View mExpandedSearchBox;
+ private View mClearButtonView;
+
+ private ValueAnimator mAnimator;
+
+ private Callback mCallback;
+
+ public SearchEditTextLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setPreImeKeyListener(OnKeyListener listener) {
+ mPreImeKeyListener = listener;
+ }
+
+ public void setCallback(Callback listener) {
+ mCallback = listener;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
+ mTopMargin = params.topMargin;
+ mBottomMargin = params.bottomMargin;
+ mLeftMargin = params.leftMargin;
+ mRightMargin = params.rightMargin;
+
+ mCollapsedElevation = getElevation();
+
+ mCollapsed = findViewById(R.id.search_box_collapsed);
+ mExpanded = findViewById(R.id.search_box_expanded);
+ mSearchView = (EditText) mExpanded.findViewById(R.id.search_view);
+
+ mSearchIcon = findViewById(R.id.search_magnifying_glass);
+ mCollapsedSearchBox = findViewById(R.id.search_box_start_search);
+ mVoiceSearchButtonView = findViewById(R.id.voice_search_button);
+ mOverflowButtonView = findViewById(R.id.dialtacts_options_menu_button);
+ mBackButtonView = findViewById(R.id.search_back_button);
+ mExpandedSearchBox = findViewById(R.id.search_box_expanded);
+ mClearButtonView = findViewById(R.id.search_close_button);
+
+ // Convert a long click into a click to expand the search box, and then long click on the
+ // search view. This accelerates the long-press scenario for copy/paste.
+ mCollapsedSearchBox.setOnLongClickListener(
+ new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ mCollapsedSearchBox.performClick();
+ mSearchView.performLongClick();
+ return false;
+ }
+ });
+
+ mSearchView.setOnFocusChangeListener(
+ new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ DialerUtils.showInputMethod(v);
+ } else {
+ DialerUtils.hideInputMethod(v);
+ }
+ }
+ });
+
+ mSearchView.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mCallback != null) {
+ mCallback.onSearchViewClicked();
+ }
+ }
+ });
+
+ mSearchView.addTextChangedListener(
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ mClearButtonView.setVisibility(TextUtils.isEmpty(s) ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+
+ findViewById(R.id.search_close_button)
+ .setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mSearchView.setText(null);
+ }
+ });
+
+ findViewById(R.id.search_back_button)
+ .setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mCallback != null) {
+ mCallback.onBackButtonClicked();
+ }
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ if (mPreImeKeyListener != null) {
+ if (mPreImeKeyListener.onKey(this, event.getKeyCode(), event)) {
+ return true;
+ }
+ }
+ return super.dispatchKeyEventPreIme(event);
+ }
+
+ public void fadeOut() {
+ fadeOut(null);
+ }
+
+ public void fadeOut(AnimUtils.AnimationCallback callback) {
+ AnimUtils.fadeOut(this, ANIMATION_DURATION, callback);
+ mIsFadedOut = true;
+ }
+
+ public void fadeIn() {
+ AnimUtils.fadeIn(this, ANIMATION_DURATION);
+ mIsFadedOut = false;
+ }
+
+ public void setVisible(boolean visible) {
+ if (visible) {
+ setAlpha(1);
+ setVisibility(View.VISIBLE);
+ mIsFadedOut = false;
+ } else {
+ setAlpha(0);
+ setVisibility(View.GONE);
+ mIsFadedOut = true;
+ }
+ }
+
+ public void expand(boolean animate, boolean requestFocus) {
+ updateVisibility(true /* isExpand */);
+
+ if (animate) {
+ AnimUtils.crossFadeViews(mExpanded, mCollapsed, ANIMATION_DURATION);
+ mAnimator = ValueAnimator.ofFloat(EXPAND_MARGIN_FRACTION_START, 0f);
+ setMargins(EXPAND_MARGIN_FRACTION_START);
+ prepareAnimator(true);
+ } else {
+ mExpanded.setVisibility(View.VISIBLE);
+ mExpanded.setAlpha(1);
+ setMargins(0f);
+ mCollapsed.setVisibility(View.GONE);
+ }
+
+ // Set 9-patch background. This owns the padding, so we need to restore the original values.
+ int paddingTop = this.getPaddingTop();
+ int paddingStart = this.getPaddingStart();
+ int paddingBottom = this.getPaddingBottom();
+ int paddingEnd = this.getPaddingEnd();
+ setBackgroundResource(R.drawable.search_shadow);
+ setElevation(0);
+ setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom);
+
+ if (requestFocus) {
+ mSearchView.requestFocus();
+ }
+ mIsExpanded = true;
+ }
+
+ public void collapse(boolean animate) {
+ updateVisibility(false /* isExpand */);
+
+ if (animate) {
+ AnimUtils.crossFadeViews(mCollapsed, mExpanded, ANIMATION_DURATION);
+ mAnimator = ValueAnimator.ofFloat(0f, 1f);
+ prepareAnimator(false);
+ } else {
+ mCollapsed.setVisibility(View.VISIBLE);
+ mCollapsed.setAlpha(1);
+ setMargins(1f);
+ mExpanded.setVisibility(View.GONE);
+ }
+
+ mIsExpanded = false;
+ setElevation(mCollapsedElevation);
+ setBackgroundResource(R.drawable.rounded_corner);
+ }
+
+ /**
+ * Updates the visibility of views depending on whether we will show the expanded or collapsed
+ * search view. This helps prevent some jank with the crossfading if we are animating.
+ *
+ * @param isExpand Whether we are about to show the expanded search box.
+ */
+ private void updateVisibility(boolean isExpand) {
+ int collapsedViewVisibility = isExpand ? View.GONE : View.VISIBLE;
+ int expandedViewVisibility = isExpand ? View.VISIBLE : View.GONE;
+
+ mSearchIcon.setVisibility(collapsedViewVisibility);
+ mCollapsedSearchBox.setVisibility(collapsedViewVisibility);
+ mVoiceSearchButtonView.setVisibility(collapsedViewVisibility);
+ mOverflowButtonView.setVisibility(collapsedViewVisibility);
+ mBackButtonView.setVisibility(expandedViewVisibility);
+ // TODO: Prevents keyboard from jumping up in landscape mode after exiting the
+ // SearchFragment when the query string is empty. More elegant fix?
+ //mExpandedSearchBox.setVisibility(expandedViewVisibility);
+ if (TextUtils.isEmpty(mSearchView.getText())) {
+ mClearButtonView.setVisibility(View.GONE);
+ } else {
+ mClearButtonView.setVisibility(expandedViewVisibility);
+ }
+ }
+
+ private void prepareAnimator(final boolean expand) {
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+
+ mAnimator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final Float fraction = (Float) animation.getAnimatedValue();
+ setMargins(fraction);
+ }
+ });
+
+ mAnimator.setDuration(ANIMATION_DURATION);
+ mAnimator.start();
+ }
+
+ public boolean isExpanded() {
+ return mIsExpanded;
+ }
+
+ public boolean isFadedOut() {
+ return mIsFadedOut;
+ }
+
+ /**
+ * Assigns margins to the search box as a fraction of its maximum margin size
+ *
+ * @param fraction How large the margins should be as a fraction of their full size
+ */
+ private void setMargins(float fraction) {
+ MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
+ params.topMargin = (int) (mTopMargin * fraction);
+ params.bottomMargin = (int) (mBottomMargin * fraction);
+ params.leftMargin = (int) (mLeftMargin * fraction);
+ params.rightMargin = (int) (mRightMargin * fraction);
+ requestLayout();
+ }
+
+ /** Listener for the back button next to the search view being pressed */
+ public interface Callback {
+
+ void onBackButtonClicked();
+
+ void onSearchViewClicked();
+ }
+}
diff --git a/java/com/android/dialer/backup/AndroidManifest.xml b/java/com/android/dialer/backup/AndroidManifest.xml
new file mode 100644
index 0000000..cfdb3d9
--- /dev/null
+++ b/java/com/android/dialer/backup/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ 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.backup">
+
+ <application
+ android:backupAgent="com.android.dialer.backup.DialerBackupAgent"
+ android:fullBackupOnly="true"
+ android:restoreAnyVersion="true"
+ android:name="com.android.dialer.app.DialerApplication"
+ />
+
+</manifest>
\ No newline at end of file
diff --git a/java/com/android/dialer/backup/DialerBackupAgent.java b/java/com/android/dialer/backup/DialerBackupAgent.java
new file mode 100644
index 0000000..391a93f
--- /dev/null
+++ b/java/com/android/dialer/backup/DialerBackupAgent.java
@@ -0,0 +1,276 @@
+/*
+ * 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.backup;
+
+import android.annotation.TargetApi;
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FullBackupDataOutput;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.ParcelFileDescriptor;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.util.Pair;
+import com.android.dialer.backup.nano.VoicemailInfo;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.telecom.TelecomUtil;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Locale;
+
+/**
+ * The Dialer backup agent to backup voicemails, and files under files, shared prefs and databases
+ */
+public class DialerBackupAgent extends BackupAgent {
+ // File names suffix for backup/restore.
+ private static final String VOICEMAIL_BACKUP_FILE_SUFFIX = "_voicemail_backup.proto";
+ // File name formats for backup. It looks like 000000_voicemail_backup.proto, 0000001...
+ private static final String VOICEMAIL_BACKUP_FILE_FORMAT = "%06d" + VOICEMAIL_BACKUP_FILE_SUFFIX;
+ // Order by Date entries from database. We start backup from the newest.
+ private static final String ORDER_BY_DATE = "date DESC";
+ // Voicemail Uri Column
+ public static final String VOICEMAIL_URI = "voicemail_uri";
+ // Voicemail packages to backup
+ public static final String VOICEMAIL_SOURCE_PACKAGE = "com.android.phone";
+
+ private long voicemailsBackedupSoFar = 0;
+ private long sizeOfVoicemailsBackedupSoFar = 0;
+ private boolean maxVoicemailBackupReached = false;
+
+ /**
+ * onBackup is used for Key/Value backup. Since we are using Dolly/Android Auto backup, we do not
+ * need to implement this method and Dolly should not be calling this. Instead Dolly will be
+ * calling onFullBackup.
+ */
+ @Override
+ public void onBackup(
+ ParcelFileDescriptor parcelFileDescriptor,
+ BackupDataOutput backupDataOutput,
+ ParcelFileDescriptor parcelFileDescriptor1)
+ throws IOException {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_BACKUP);
+ Assert.fail("Android Backup should not call DialerBackupAgent.onBackup");
+ }
+
+ /**
+ * onRestore is used for Key/Value restore. Since we are using Dolly/Android Auto backup/restore,
+ * we do not need to implement this method as Dolly should not be calling this method. Instead
+ * onFileRestore will be called by Dolly.
+ */
+ @Override
+ public void onRestore(
+ BackupDataInput backupDataInput, int i, ParcelFileDescriptor parcelFileDescriptor)
+ throws IOException {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE);
+ Assert.fail("Android Backup should not call DialerBackupAgent.onRestore");
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ @Override
+ public void onFullBackup(FullBackupDataOutput data) throws IOException {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_FULL_BACKUP);
+ LogUtil.i("DialerBackupAgent.onFullBackup", "performing dialer backup");
+ boolean autoBackupEnabled =
+ ConfigProviderBindings.get(this).getBoolean("enable_autobackup", true);
+ boolean vmBackupEnabled =
+ ConfigProviderBindings.get(this).getBoolean("enable_vm_backup", false);
+
+ if (autoBackupEnabled) {
+ if (!maxVoicemailBackupReached && vmBackupEnabled) {
+ voicemailsBackedupSoFar = 0;
+ sizeOfVoicemailsBackedupSoFar = 0;
+
+ LogUtil.i("DialerBackupAgent.onFullBackup", "autoBackup is enabled");
+ ContentResolver contentResolver = getContentResolver();
+ int limit = 1000;
+
+ Uri uri =
+ TelecomUtil.getCallLogUri(this)
+ .buildUpon()
+ .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
+ .build();
+
+ LogUtil.i("DialerBackupAgent.onFullBackup", "backing up from: " + uri);
+
+ try (Cursor cursor =
+ contentResolver.query(
+ uri,
+ null,
+ String.format(
+ "(%s = ? AND deleted = 0 AND %s = ?)", Calls.TYPE, Voicemails.SOURCE_PACKAGE),
+ new String[] {
+ Integer.toString(CallLog.Calls.VOICEMAIL_TYPE), VOICEMAIL_SOURCE_PACKAGE
+ },
+ ORDER_BY_DATE,
+ null)) {
+
+ if (cursor == null) {
+ LogUtil.i("DialerBackupAgent.onFullBackup", "cursor was null");
+ return;
+ }
+
+ LogUtil.i("DialerBackupAgent.onFullBackup", "cursor count: " + cursor.getCount());
+ if (cursor.moveToFirst()) {
+ int fileNum = 0;
+ do {
+ backupRow(
+ data, cursor, String.format(Locale.US, VOICEMAIL_BACKUP_FILE_FORMAT, fileNum++));
+ } while (cursor.moveToNext() && !maxVoicemailBackupReached);
+ } else {
+ LogUtil.i("DialerBackupAgent.onFullBackup", "cursor.moveToFirst failed");
+ }
+ }
+ }
+ LogUtil.i(
+ "DialerBackupAgent.onFullBackup",
+ "vm files backed up: %d, vm size backed up:%d, "
+ + "max vm backup reached:%b, vm backup enabled:%b",
+ voicemailsBackedupSoFar,
+ sizeOfVoicemailsBackedupSoFar,
+ maxVoicemailBackupReached,
+ vmBackupEnabled);
+ super.onFullBackup(data);
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_FULL_BACKED_UP);
+ } else {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_BACKUP_DISABLED);
+ LogUtil.i("DialerBackupAgent.onFullBackup", "autoBackup is disabled");
+ }
+ }
+
+ private void backupRow(FullBackupDataOutput data, Cursor cursor, String fileName)
+ throws IOException {
+
+ VoicemailInfo cursorRowInProto =
+ DialerBackupUtils.convertVoicemailCursorRowToProto(cursor, getContentResolver());
+
+ File file = new File(getFilesDir(), fileName);
+ DialerBackupUtils.writeProtoToFile(file, cursorRowInProto);
+
+ if (sizeOfVoicemailsBackedupSoFar + file.length()
+ > DialerBackupUtils.maxVoicemailSizeToBackup) {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_MAX_VM_BACKUP_REACHED);
+ maxVoicemailBackupReached = true;
+ file.delete();
+ return;
+ }
+
+ backupFile(file, data);
+ }
+
+ // TODO: Write to FullBackupDataOutput directly (b/33849960)
+ private void backupFile(File file, FullBackupDataOutput data) throws IOException {
+ try {
+ super.fullBackupFile(file, data);
+ sizeOfVoicemailsBackedupSoFar = sizeOfVoicemailsBackedupSoFar + file.length();
+ voicemailsBackedupSoFar++;
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_VOICEMAIL_BACKED_UP);
+ LogUtil.i("DialerBackupAgent.backupFile", "file backed up:" + file.getAbsolutePath());
+ } finally {
+ file.delete();
+ }
+ }
+
+ // Being tracked in b/33839952
+ @Override
+ public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_QUOTA_EXCEEDED);
+ LogUtil.i("DialerBackupAgent.onQuotaExceeded", "does nothing");
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ @Override
+ public void onRestoreFile(
+ ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime)
+ throws IOException {
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "size:" + size + " destination: " + destination);
+
+ String fileName = destination.getName();
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "file name: " + fileName);
+
+ if (ConfigProviderBindings.get(this).getBoolean("enable_autobackup", true)) {
+ if (fileName.endsWith(VOICEMAIL_BACKUP_FILE_SUFFIX)
+ && ConfigProviderBindings.get(this).getBoolean("enable_vm_restore", true)) {
+ if (DialerBackupUtils.canRestoreVoicemails(getContentResolver(), this)) {
+ try {
+ super.onRestoreFile(data, size, destination, type, mode, mtime);
+ restoreVoicemail(destination);
+ destination.delete();
+ } catch (IOException e) {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_IO_EXCEPTION);
+ LogUtil.e(
+ "DialerBackupAgent.onRestoreFile",
+ "could not restore voicemail - IOException: ",
+ e);
+ }
+ } else {
+ LogUtil.i(
+ "DialerBackupAgent.onRestoreFile", "build does not support restoring voicemails");
+ }
+
+ } else {
+ super.onRestoreFile(data, size, destination, type, mode, mtime);
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "restored: " + fileName);
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_RESTORED_FILE);
+ }
+ } else {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_DISABLED);
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "autoBackup is disabled");
+ }
+ }
+
+ @Override
+ public void onRestoreFinished() {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_FINISHED);
+ LogUtil.i("DialerBackupAgent.onRestoreFinished", "do nothing");
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ private void restoreVoicemail(File file) throws IOException {
+ Pair<ContentValues, byte[]> pair =
+ DialerBackupUtils.convertVoicemailProtoFileToContentValueAndAudioBytes(
+ file, getApplicationContext());
+
+ if (pair == null) {
+ LogUtil.i("DialerBackupAgent.restoreVoicemail", "not restoring VM due to duplicate");
+ Logger.get(this)
+ .logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_VM_DUPLICATE_NOT_RESTORING);
+ return;
+ }
+
+ // TODO: Uniquely identify backup agent as the creator of this voicemail b/34084298
+ try (OutputStream restoreStream =
+ getContentResolver()
+ .openOutputStream(
+ getContentResolver()
+ .insert(VoicemailContract.Voicemails.CONTENT_URI, pair.first))) {
+ DialerBackupUtils.copyAudioBytesToContentUri(pair.second, restoreStream);
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_RESTORED_VOICEMAIL);
+ }
+ }
+}
diff --git a/java/com/android/dialer/backup/DialerBackupUtils.java b/java/com/android/dialer/backup/DialerBackupUtils.java
new file mode 100644
index 0000000..ff0cb4f
--- /dev/null
+++ b/java/com/android/dialer/backup/DialerBackupUtils.java
@@ -0,0 +1,320 @@
+/*
+ * 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.backup;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+import com.android.dialer.backup.nano.VoicemailInfo;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import com.google.protobuf.nano.MessageNano;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Helper functions for DialerBackupAgent */
+public class DialerBackupUtils {
+ // Backup voicemails up to 20MB
+ static long maxVoicemailSizeToBackup = 20000000L;
+ static final String RESTORED_COLUMN = "restored";
+
+ private DialerBackupUtils() {}
+
+ public static void copyAudioBytesToContentUri(
+ @NonNull byte[] audioBytesArray, @NonNull OutputStream restoreStream) throws IOException {
+ LogUtil.i("DialerBackupUtils.copyStream", "audioByteArray length: " + audioBytesArray.length);
+
+ ByteArrayInputStream decodedStream = new ByteArrayInputStream(audioBytesArray);
+ LogUtil.i(
+ "DialerBackupUtils.copyStream", "decodedStream.available: " + decodedStream.available());
+
+ ByteStreams.copy(decodedStream, restoreStream);
+ }
+
+ public static @Nullable byte[] audioStreamToByteArray(@NonNull InputStream stream)
+ throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ if (stream.available() > 0) {
+ ByteStreams.copy(stream, buffer);
+ } else {
+ LogUtil.i("DialerBackupUtils.audioStreamToByteArray", "no audio stream to backup");
+ }
+ return buffer.toByteArray();
+ }
+
+ public static void writeProtoToFile(@NonNull File file, @NonNull VoicemailInfo voicemailInfo)
+ throws IOException {
+ LogUtil.i(
+ "DialerBackupUtils.writeProtoToFile",
+ "backup " + voicemailInfo + " to " + file.getAbsolutePath());
+
+ byte[] bytes = MessageNano.toByteArray(voicemailInfo);
+ Files.write(bytes, file);
+ }
+
+ /** Only restore voicemails that have the restored column in calllog (NMR2+ builds) */
+ @TargetApi(VERSION_CODES.M)
+ public static boolean canRestoreVoicemails(ContentResolver contentResolver, Context context) {
+ try (Cursor cursor = contentResolver.query(Voicemails.CONTENT_URI, null, null, null, null)) {
+ // Restored column only exists in NMR2 and above builds.
+ if (cursor.getColumnIndex(RESTORED_COLUMN) != -1) {
+ LogUtil.i("DialerBackupUtils.canRestoreVoicemails", "Build supports restore");
+ return true;
+ } else {
+ LogUtil.i("DialerBackupUtils.canRestoreVoicemails", "Build does not support restore");
+ return false;
+ }
+ }
+ }
+
+ public static VoicemailInfo protoFileToVoicemailInfo(@NonNull File file) throws IOException {
+ byte[] byteArray = Files.toByteArray(file);
+ return VoicemailInfo.parseFrom(byteArray);
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ public static VoicemailInfo convertVoicemailCursorRowToProto(
+ @NonNull Cursor cursor, @NonNull ContentResolver contentResolver) throws IOException {
+
+ VoicemailInfo voicemailInfo = new VoicemailInfo();
+
+ for (int i = 0; i < cursor.getColumnCount(); ++i) {
+ String name = cursor.getColumnName(i);
+ String value = cursor.getString(i);
+
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailCursorRowToProto",
+ "column index: %d, column name: %s, column value: %s",
+ i,
+ name,
+ value);
+
+ switch (name) {
+ case Voicemails.DATE:
+ voicemailInfo.date = value;
+ break;
+ case Voicemails.DELETED:
+ voicemailInfo.deleted = value;
+ break;
+ case Voicemails.DIRTY:
+ voicemailInfo.dirty = value;
+ break;
+ case Voicemails.DIR_TYPE:
+ voicemailInfo.dirType = value;
+ break;
+ case Voicemails.DURATION:
+ voicemailInfo.duration = value;
+ break;
+ case Voicemails.HAS_CONTENT:
+ voicemailInfo.hasContent = value;
+ break;
+ case Voicemails.IS_READ:
+ voicemailInfo.isRead = value;
+ break;
+ case Voicemails.ITEM_TYPE:
+ voicemailInfo.itemType = value;
+ break;
+ case Voicemails.LAST_MODIFIED:
+ voicemailInfo.lastModified = value;
+ break;
+ case Voicemails.MIME_TYPE:
+ voicemailInfo.mimeType = value;
+ break;
+ case Voicemails.NUMBER:
+ voicemailInfo.number = value;
+ break;
+ case Voicemails.PHONE_ACCOUNT_COMPONENT_NAME:
+ voicemailInfo.phoneAccountComponentName = value;
+ break;
+ case Voicemails.PHONE_ACCOUNT_ID:
+ voicemailInfo.phoneAccountId = value;
+ break;
+ case Voicemails.SOURCE_DATA:
+ voicemailInfo.sourceData = value;
+ break;
+ case Voicemails.SOURCE_PACKAGE:
+ voicemailInfo.sourcePackage = value;
+ break;
+ case Voicemails.TRANSCRIPTION:
+ voicemailInfo.transcription = value;
+ break;
+ case DialerBackupAgent.VOICEMAIL_URI:
+ try (InputStream audioStream = contentResolver.openInputStream(Uri.parse(value))) {
+ voicemailInfo.encodedVoicemailKey = audioStreamToByteArray(audioStream);
+ }
+ break;
+ default:
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailCursorRowToProto",
+ "Not backing up column: %s, with value: %s",
+ name,
+ value);
+ break;
+ }
+ }
+ return voicemailInfo;
+ }
+
+ public static Pair<ContentValues, byte[]> convertVoicemailProtoFileToContentValueAndAudioBytes(
+ @NonNull File file, Context context) throws IOException {
+
+ VoicemailInfo voicemailInfo = DialerBackupUtils.protoFileToVoicemailInfo(file);
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
+ "file name: "
+ + file.getName()
+ + " voicemailInfo size: "
+ + voicemailInfo.getSerializedSize());
+
+ if (isDuplicate(context, voicemailInfo)) {
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
+ "voicemail already exists");
+ return null;
+ } else {
+ ContentValues contentValues = new ContentValues();
+
+ if (!voicemailInfo.date.isEmpty()) {
+ contentValues.put(Voicemails.DATE, voicemailInfo.date);
+ }
+ if (!voicemailInfo.deleted.isEmpty()) {
+ contentValues.put(Voicemails.DELETED, voicemailInfo.deleted);
+ }
+ if (!voicemailInfo.dirty.isEmpty()) {
+ contentValues.put(Voicemails.DIRTY, voicemailInfo.dirty);
+ }
+ if (!voicemailInfo.duration.isEmpty()) {
+ contentValues.put(Voicemails.DURATION, voicemailInfo.duration);
+ }
+ if (!voicemailInfo.isRead.isEmpty()) {
+ contentValues.put(Voicemails.IS_READ, voicemailInfo.isRead);
+ }
+ if (!voicemailInfo.lastModified.isEmpty()) {
+ contentValues.put(Voicemails.LAST_MODIFIED, voicemailInfo.lastModified);
+ }
+ if (!voicemailInfo.mimeType.isEmpty()) {
+ contentValues.put(Voicemails.MIME_TYPE, voicemailInfo.mimeType);
+ }
+ if (!voicemailInfo.number.isEmpty()) {
+ contentValues.put(Voicemails.NUMBER, voicemailInfo.number);
+ }
+ if (!voicemailInfo.phoneAccountComponentName.isEmpty()) {
+ contentValues.put(
+ Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, voicemailInfo.phoneAccountComponentName);
+ }
+ if (!voicemailInfo.phoneAccountId.isEmpty()) {
+ contentValues.put(Voicemails.PHONE_ACCOUNT_ID, voicemailInfo.phoneAccountId);
+ }
+ if (!voicemailInfo.sourceData.isEmpty()) {
+ contentValues.put(Voicemails.SOURCE_DATA, voicemailInfo.sourceData);
+ }
+ if (!voicemailInfo.sourcePackage.isEmpty()) {
+ contentValues.put(Voicemails.SOURCE_PACKAGE, voicemailInfo.sourcePackage);
+ }
+ if (!voicemailInfo.transcription.isEmpty()) {
+ contentValues.put(Voicemails.TRANSCRIPTION, voicemailInfo.transcription);
+ }
+ contentValues.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+ contentValues.put(RESTORED_COLUMN, "1");
+ contentValues.put(Voicemails.SOURCE_PACKAGE, getSourcePackage(context, voicemailInfo));
+
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
+ "cv: " + contentValues);
+
+ return Pair.create(contentValues, voicemailInfo.encodedVoicemailKey);
+ }
+ }
+
+ /**
+ * We should be using the system package name as the source package if there is no endless VM/VM
+ * archive present on the device. This is to separate pre-O (no endless VM) and O+ (endless VM)
+ * devices. This ensures that the source of truth for VMs is the VM server when endless VM is not
+ * enabled, and when endless VM/archived VMs is present, the source of truth for VMs is the device
+ * itself.
+ */
+ private static String getSourcePackage(Context context, VoicemailInfo voicemailInfo) {
+ if (ConfigProviderBindings.get(context)
+ .getBoolean("voicemail_restore_force_system_source_package", false)) {
+ LogUtil.i("DialerBackupUtils.getSourcePackage", "forcing system source package");
+ return "com.android.phone";
+ }
+ if (ConfigProviderBindings.get(context)
+ .getBoolean("voicemail_restore_check_archive_for_source_package", true)) {
+ if ("1".equals(voicemailInfo.archived)) {
+ LogUtil.i(
+ "DialerBackupUtils.getSourcePackage",
+ "voicemail was archived, using app source package");
+ // Using our app's source package will prevent the archived voicemail from being deleted by
+ // the system when it syncs with the voicemail server. In most cases the user will not see
+ // duplicate voicemails because this voicemail was archived and likely deleted from the
+ // voicemail server.
+ return context.getPackageName();
+ } else {
+ // Use the system source package. This means that if the voicemail is not present on the
+ // voicemail server then the system will delete it when it syncs.
+ LogUtil.i(
+ "DialerBackupUtils.getSourcePackage",
+ "voicemail was not archived, using system source package");
+ return "com.android.phone";
+ }
+ }
+ // Use our app's source package. This means that if the system syncs voicemail from the server
+ // the user could potentially get duplicate voicemails.
+ LogUtil.i("DialerBackupUtils.getSourcePackage", "defaulting to using app source package");
+ return context.getPackageName();
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ private static boolean isDuplicate(Context context, VoicemailInfo voicemailInfo) {
+ // This checks for VM that might already exist, and doesn't restore them
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ VoicemailContract.Voicemails.CONTENT_URI,
+ null,
+ String.format(
+ "(%s = ? AND %s = ? AND %s = ?)",
+ Voicemails.NUMBER, Voicemails.DATE, Voicemails.DURATION),
+ new String[] {voicemailInfo.number, voicemailInfo.date, voicemailInfo.duration},
+ null,
+ null)) {
+ if (cursor.moveToFirst()
+ && ConfigProviderBindings.get(context)
+ .getBoolean("enable_vm_restore_no_duplicate", true)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/backup/proto/VoicemailInfo.java b/java/com/android/dialer/backup/proto/VoicemailInfo.java
new file mode 100644
index 0000000..9ff8423
--- /dev/null
+++ b/java/com/android/dialer/backup/proto/VoicemailInfo.java
@@ -0,0 +1,377 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.backup.nano;
+
+@SuppressWarnings("hiding")
+public final class VoicemailInfo extends
+ com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> {
+
+ private static volatile VoicemailInfo[] _emptyArray;
+ public static VoicemailInfo[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new VoicemailInfo[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional string date = 1;
+ public java.lang.String date;
+
+ // optional string deleted = 2;
+ public java.lang.String deleted;
+
+ // optional string dirty = 3;
+ public java.lang.String dirty;
+
+ // optional string dir_type = 4;
+ public java.lang.String dirType;
+
+ // optional string duration = 5;
+ public java.lang.String duration;
+
+ // optional string has_content = 6;
+ public java.lang.String hasContent;
+
+ // optional string is_read = 7;
+ public java.lang.String isRead;
+
+ // optional string item_type = 8;
+ public java.lang.String itemType;
+
+ // optional string last_modified = 9;
+ public java.lang.String lastModified;
+
+ // optional string mime_type = 10;
+ public java.lang.String mimeType;
+
+ // optional string number = 11;
+ public java.lang.String number;
+
+ // optional string phone_account_component_name = 12;
+ public java.lang.String phoneAccountComponentName;
+
+ // optional string phone_account_id = 13;
+ public java.lang.String phoneAccountId;
+
+ // optional string source_data = 14;
+ public java.lang.String sourceData;
+
+ // optional string source_package = 15;
+ public java.lang.String sourcePackage;
+
+ // optional string transcription = 16;
+ public java.lang.String transcription;
+
+ // optional string voicemail_uri = 17;
+ public java.lang.String voicemailUri;
+
+ // optional bytes encoded_voicemail_key = 18;
+ public byte[] encodedVoicemailKey;
+
+ // optional string archived = 19;
+ public java.lang.String archived;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.backup.VoicemailInfo)
+
+ public VoicemailInfo() {
+ clear();
+ }
+
+ public VoicemailInfo clear() {
+ date = "";
+ deleted = "";
+ dirty = "";
+ dirType = "";
+ duration = "";
+ hasContent = "";
+ isRead = "";
+ itemType = "";
+ lastModified = "";
+ mimeType = "";
+ number = "";
+ phoneAccountComponentName = "";
+ phoneAccountId = "";
+ sourceData = "";
+ sourcePackage = "";
+ transcription = "";
+ voicemailUri = "";
+ encodedVoicemailKey = com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES;
+ archived = "";
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.date != null && !this.date.equals("")) {
+ output.writeString(1, this.date);
+ }
+ if (this.deleted != null && !this.deleted.equals("")) {
+ output.writeString(2, this.deleted);
+ }
+ if (this.dirty != null && !this.dirty.equals("")) {
+ output.writeString(3, this.dirty);
+ }
+ if (this.dirType != null && !this.dirType.equals("")) {
+ output.writeString(4, this.dirType);
+ }
+ if (this.duration != null && !this.duration.equals("")) {
+ output.writeString(5, this.duration);
+ }
+ if (this.hasContent != null && !this.hasContent.equals("")) {
+ output.writeString(6, this.hasContent);
+ }
+ if (this.isRead != null && !this.isRead.equals("")) {
+ output.writeString(7, this.isRead);
+ }
+ if (this.itemType != null && !this.itemType.equals("")) {
+ output.writeString(8, this.itemType);
+ }
+ if (this.lastModified != null && !this.lastModified.equals("")) {
+ output.writeString(9, this.lastModified);
+ }
+ if (this.mimeType != null && !this.mimeType.equals("")) {
+ output.writeString(10, this.mimeType);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ output.writeString(11, this.number);
+ }
+ if (this.phoneAccountComponentName != null && !this.phoneAccountComponentName.equals("")) {
+ output.writeString(12, this.phoneAccountComponentName);
+ }
+ if (this.phoneAccountId != null && !this.phoneAccountId.equals("")) {
+ output.writeString(13, this.phoneAccountId);
+ }
+ if (this.sourceData != null && !this.sourceData.equals("")) {
+ output.writeString(14, this.sourceData);
+ }
+ if (this.sourcePackage != null && !this.sourcePackage.equals("")) {
+ output.writeString(15, this.sourcePackage);
+ }
+ if (this.transcription != null && !this.transcription.equals("")) {
+ output.writeString(16, this.transcription);
+ }
+ if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
+ output.writeString(17, this.voicemailUri);
+ }
+ if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+ output.writeBytes(18, this.encodedVoicemailKey);
+ }
+ if (this.archived != null && !this.archived.equals("")) {
+ output.writeString(19, this.archived);
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.date != null && !this.date.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(1, this.date);
+ }
+ if (this.deleted != null && !this.deleted.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(2, this.deleted);
+ }
+ if (this.dirty != null && !this.dirty.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(3, this.dirty);
+ }
+ if (this.dirType != null && !this.dirType.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(4, this.dirType);
+ }
+ if (this.duration != null && !this.duration.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(5, this.duration);
+ }
+ if (this.hasContent != null && !this.hasContent.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(6, this.hasContent);
+ }
+ if (this.isRead != null && !this.isRead.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(7, this.isRead);
+ }
+ if (this.itemType != null && !this.itemType.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(8, this.itemType);
+ }
+ if (this.lastModified != null && !this.lastModified.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(9, this.lastModified);
+ }
+ if (this.mimeType != null && !this.mimeType.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(10, this.mimeType);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(11, this.number);
+ }
+ if (this.phoneAccountComponentName != null && !this.phoneAccountComponentName.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(12, this.phoneAccountComponentName);
+ }
+ if (this.phoneAccountId != null && !this.phoneAccountId.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(13, this.phoneAccountId);
+ }
+ if (this.sourceData != null && !this.sourceData.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(14, this.sourceData);
+ }
+ if (this.sourcePackage != null && !this.sourcePackage.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(15, this.sourcePackage);
+ }
+ if (this.transcription != null && !this.transcription.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(16, this.transcription);
+ }
+ if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(17, this.voicemailUri);
+ }
+ if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeBytesSize(18, this.encodedVoicemailKey);
+ }
+ if (this.archived != null && !this.archived.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(19, this.archived);
+ }
+ return size;
+ }
+
+ @Override
+ public VoicemailInfo mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 10: {
+ this.date = input.readString();
+ break;
+ }
+ case 18: {
+ this.deleted = input.readString();
+ break;
+ }
+ case 26: {
+ this.dirty = input.readString();
+ break;
+ }
+ case 34: {
+ this.dirType = input.readString();
+ break;
+ }
+ case 42: {
+ this.duration = input.readString();
+ break;
+ }
+ case 50: {
+ this.hasContent = input.readString();
+ break;
+ }
+ case 58: {
+ this.isRead = input.readString();
+ break;
+ }
+ case 66: {
+ this.itemType = input.readString();
+ break;
+ }
+ case 74: {
+ this.lastModified = input.readString();
+ break;
+ }
+ case 82: {
+ this.mimeType = input.readString();
+ break;
+ }
+ case 90: {
+ this.number = input.readString();
+ break;
+ }
+ case 98: {
+ this.phoneAccountComponentName = input.readString();
+ break;
+ }
+ case 106: {
+ this.phoneAccountId = input.readString();
+ break;
+ }
+ case 114: {
+ this.sourceData = input.readString();
+ break;
+ }
+ case 122: {
+ this.sourcePackage = input.readString();
+ break;
+ }
+ case 130: {
+ this.transcription = input.readString();
+ break;
+ }
+ case 138: {
+ this.voicemailUri = input.readString();
+ break;
+ }
+ case 146: {
+ this.encodedVoicemailKey = input.readBytes();
+ break;
+ }
+ case 154: {
+ this.archived = input.readString();
+ break;
+ }
+ }
+ }
+ }
+
+ public static VoicemailInfo parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new VoicemailInfo(), data);
+ }
+
+ public static VoicemailInfo parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new VoicemailInfo().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/blocking/AndroidManifest.xml b/java/com/android/dialer/blocking/AndroidManifest.xml
new file mode 100644
index 0000000..08d2439
--- /dev/null
+++ b/java/com/android/dialer/blocking/AndroidManifest.xml
@@ -0,0 +1,13 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.blocking">
+
+ <application android:theme="@style/Theme.AppCompat">
+
+ <provider
+ android:authorities="com.android.dialer.blocking.filterednumberprovider"
+ android:exported="false"
+ android:multiprocess="false"
+ android:name="com.android.dialer.blocking.FilteredNumberProvider"/>
+
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/blocking/BlockNumberDialogFragment.java b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java
new file mode 100644
index 0000000..c405b2f
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2015 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.blocking;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Toast;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnBlockNumberListener;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnUnblockNumberListener;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker;
+
+/**
+ * Fragment for confirming and enacting blocking/unblocking a number. Also invokes snackbar
+ * providing undo functionality.
+ */
+public class BlockNumberDialogFragment extends DialogFragment {
+
+ private static final String BLOCK_DIALOG_FRAGMENT = "BlockNumberDialog";
+ private static final String ARG_BLOCK_ID = "argBlockId";
+ private static final String ARG_NUMBER = "argNumber";
+ private static final String ARG_COUNTRY_ISO = "argCountryIso";
+ private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber";
+ private static final String ARG_PARENT_VIEW_ID = "parentViewId";
+ private String mNumber;
+ private String mDisplayNumber;
+ private String mCountryIso;
+ private FilteredNumberAsyncQueryHandler mHandler;
+ private View mParentView;
+ private VisualVoicemailEnabledChecker mVoicemailEnabledChecker;
+ private Callback mCallback;
+
+ public static BlockNumberDialogFragment show(
+ Integer blockId,
+ String number,
+ String countryIso,
+ String displayNumber,
+ Integer parentViewId,
+ FragmentManager fragmentManager,
+ Callback callback) {
+ final BlockNumberDialogFragment newFragment =
+ BlockNumberDialogFragment.newInstance(
+ blockId, number, countryIso, displayNumber, parentViewId);
+
+ newFragment.setCallback(callback);
+ newFragment.show(fragmentManager, BlockNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
+ return newFragment;
+ }
+
+ private static BlockNumberDialogFragment newInstance(
+ Integer blockId,
+ String number,
+ String countryIso,
+ String displayNumber,
+ Integer parentViewId) {
+ final BlockNumberDialogFragment fragment = new BlockNumberDialogFragment();
+ final Bundle args = new Bundle();
+ if (blockId != null) {
+ args.putInt(ARG_BLOCK_ID, blockId.intValue());
+ }
+ if (parentViewId != null) {
+ args.putInt(ARG_PARENT_VIEW_ID, parentViewId.intValue());
+ }
+ args.putString(ARG_NUMBER, number);
+ args.putString(ARG_COUNTRY_ISO, countryIso);
+ args.putString(ARG_DISPLAY_NUMBER, displayNumber);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public void setFilteredNumberAsyncQueryHandlerForTesting(
+ FilteredNumberAsyncQueryHandler handler) {
+ mHandler = handler;
+ }
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ final boolean isBlocked = getArguments().containsKey(ARG_BLOCK_ID);
+
+ mNumber = getArguments().getString(ARG_NUMBER);
+ mDisplayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+ mCountryIso = getArguments().getString(ARG_COUNTRY_ISO);
+
+ if (TextUtils.isEmpty(mDisplayNumber)) {
+ mDisplayNumber = mNumber;
+ }
+
+ mHandler = new FilteredNumberAsyncQueryHandler(getContext());
+ mVoicemailEnabledChecker = new VisualVoicemailEnabledChecker(getActivity(), null);
+ // Choose not to update VoicemailEnabledChecker, as checks should already been done in
+ // all current use cases.
+ mParentView = getActivity().findViewById(getArguments().getInt(ARG_PARENT_VIEW_ID));
+
+ CharSequence title;
+ String okText;
+ String message;
+ if (isBlocked) {
+ title = null;
+ okText = getString(R.string.unblock_number_ok);
+ message =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.unblock_number_confirmation_title, mDisplayNumber)
+ .toString();
+ } else {
+ title =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.block_number_confirmation_title, mDisplayNumber);
+ okText = getString(R.string.block_number_ok);
+ if (FilteredNumberCompat.useNewFiltering(getContext())) {
+ message = getString(R.string.block_number_confirmation_message_new_filtering);
+ } else if (mVoicemailEnabledChecker.isVisualVoicemailEnabled()) {
+ message = getString(R.string.block_number_confirmation_message_vvm);
+ } else {
+ message = getString(R.string.block_number_confirmation_message_no_vvm);
+ }
+ }
+
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(getActivity())
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(
+ okText,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ if (isBlocked) {
+ unblockNumber();
+ } else {
+ blockNumber();
+ }
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null);
+ return builder.create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ String e164Number = PhoneNumberUtils.formatNumberToE164(mNumber, mCountryIso);
+ if (!FilteredNumbersUtil.canBlockNumber(getContext(), e164Number, mNumber)) {
+ dismiss();
+ Toast.makeText(
+ getContext(),
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.invalidNumber, mDisplayNumber),
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ // Dismiss on rotation.
+ dismiss();
+ mCallback = null;
+
+ super.onPause();
+ }
+
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ private CharSequence getBlockedMessage() {
+ return ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.snackbar_number_blocked, mDisplayNumber);
+ }
+
+ private CharSequence getUnblockedMessage() {
+ return ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.snackbar_number_unblocked, mDisplayNumber);
+ }
+
+ private int getActionTextColor() {
+ return getContext().getResources().getColor(R.color.dialer_snackbar_action_text_color);
+ }
+
+ private void blockNumber() {
+ final CharSequence message = getBlockedMessage();
+ final CharSequence undoMessage = getUnblockedMessage();
+ final Callback callback = mCallback;
+ final int actionTextColor = getActionTextColor();
+ final Context applicationContext = getContext().getApplicationContext();
+
+ final OnUnblockNumberListener onUndoListener =
+ new OnUnblockNumberListener() {
+ @Override
+ public void onUnblockComplete(int rows, ContentValues values) {
+ Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show();
+ if (callback != null) {
+ callback.onChangeFilteredNumberUndo();
+ }
+ }
+ };
+
+ final OnBlockNumberListener onBlockNumberListener =
+ new OnBlockNumberListener() {
+ @Override
+ public void onBlockComplete(final Uri uri) {
+ final View.OnClickListener undoListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Delete the newly created row on 'undo'.
+ Logger.get(applicationContext)
+ .logInteraction(InteractionEvent.Type.UNDO_BLOCK_NUMBER);
+ mHandler.unblock(onUndoListener, uri);
+ }
+ };
+
+ Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.block_number_undo, undoListener)
+ .setActionTextColor(actionTextColor)
+ .show();
+
+ if (callback != null) {
+ callback.onFilterNumberSuccess();
+ }
+
+ if (FilteredNumbersUtil.hasRecentEmergencyCall(applicationContext)) {
+ FilteredNumbersUtil.maybeNotifyCallBlockingDisabled(applicationContext);
+ }
+ }
+ };
+
+ mHandler.blockNumber(onBlockNumberListener, mNumber, mCountryIso);
+ }
+
+ private void unblockNumber() {
+ final CharSequence message = getUnblockedMessage();
+ final CharSequence undoMessage = getBlockedMessage();
+ final Callback callback = mCallback;
+ final int actionTextColor = getActionTextColor();
+ final Context applicationContext = getContext().getApplicationContext();
+
+ final OnBlockNumberListener onUndoListener =
+ new OnBlockNumberListener() {
+ @Override
+ public void onBlockComplete(final Uri uri) {
+ Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show();
+ if (callback != null) {
+ callback.onChangeFilteredNumberUndo();
+ }
+ }
+ };
+
+ mHandler.unblock(
+ new OnUnblockNumberListener() {
+ @Override
+ public void onUnblockComplete(int rows, final ContentValues values) {
+ final View.OnClickListener undoListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Re-insert the row on 'undo', with a new ID.
+ Logger.get(applicationContext)
+ .logInteraction(InteractionEvent.Type.UNDO_UNBLOCK_NUMBER);
+ mHandler.blockNumber(onUndoListener, values);
+ }
+ };
+
+ Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.block_number_undo, undoListener)
+ .setActionTextColor(actionTextColor)
+ .show();
+
+ if (callback != null) {
+ callback.onUnfilterNumberSuccess();
+ }
+ }
+ },
+ getArguments().getInt(ARG_BLOCK_ID));
+ }
+
+ /**
+ * Use a callback interface to update UI after success/undo. Favor this approach over other more
+ * standard paradigms because of the variety of scenarios in which the DialogFragment can be
+ * invoked (by an Activity, by a fragment, by an adapter, by an adapter list item). Because of
+ * this, we do NOT support retaining state on rotation, and will dismiss the dialog upon rotation
+ * instead.
+ */
+ public interface Callback {
+
+ /** Called when a number is successfully added to the set of filtered numbers */
+ void onFilterNumberSuccess();
+
+ /** Called when a number is successfully removed from the set of filtered numbers */
+ void onUnfilterNumberSuccess();
+
+ /** Called when the action of filtering or unfiltering a number is undone */
+ void onChangeFilteredNumberUndo();
+ }
+}
diff --git a/java/com/android/dialer/blocking/BlockReportSpamDialogs.java b/java/com/android/dialer/blocking/BlockReportSpamDialogs.java
new file mode 100644
index 0000000..b5f81fc
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockReportSpamDialogs.java
@@ -0,0 +1,305 @@
+/*
+ * 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.blocking;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+/** Helper class for creating block/report dialog fragments. */
+public class BlockReportSpamDialogs {
+
+ public static final String BLOCK_REPORT_SPAM_DIALOG_TAG = "BlockReportSpamDialog";
+ public static final String BLOCK_DIALOG_TAG = "BlockDialog";
+ public static final String UNBLOCK_DIALOG_TAG = "UnblockDialog";
+ public static final String NOT_SPAM_DIALOG_TAG = "NotSpamDialog";
+
+ /** Creates a dialog with the default cancel button listener (dismisses dialog). */
+ private static AlertDialog.Builder createDialogBuilder(
+ Activity activity, final DialogFragment fragment) {
+ return new AlertDialog.Builder(activity, R.style.AlertDialogTheme)
+ .setCancelable(true)
+ .setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ fragment.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Creates a generic click listener which dismisses the fragment and then calls the actual
+ * listener.
+ */
+ private static DialogInterface.OnClickListener createGenericOnClickListener(
+ final DialogFragment fragment, final OnConfirmListener listener) {
+ return new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ fragment.dismiss();
+ listener.onClick();
+ }
+ };
+ }
+
+ private static String getBlockMessage(Context context) {
+ String message;
+ if (FilteredNumberCompat.useNewFiltering(context)) {
+ message = context.getString(R.string.block_number_confirmation_message_new_filtering);
+ } else {
+ message = context.getString(R.string.block_report_number_alert_details);
+ }
+ return message;
+ }
+
+ /**
+ * Listener passed to block/report spam dialog for positive click in {@link
+ * BlockReportSpamDialogFragment}.
+ */
+ public interface OnSpamDialogClickListener {
+
+ /**
+ * Called when user clicks on positive button in block/report spam dialog.
+ *
+ * @param isSpamChecked Whether the spam checkbox is checked.
+ */
+ void onClick(boolean isSpamChecked);
+ }
+
+ /** Listener passed to all dialogs except the block/report spam dialog for positive click. */
+ public interface OnConfirmListener {
+
+ /** Called when user clicks on positive button in the dialog. */
+ void onClick();
+ }
+
+ /** Contains the common attributes between all block/unblock/report dialog fragments. */
+ private static class CommonDialogsFragment extends DialogFragment {
+
+ /** The number to display in the dialog title. */
+ protected String mDisplayNumber;
+
+ /** Called when dialog positive button is pressed. */
+ protected OnConfirmListener mPositiveListener;
+
+ /** Called when dialog is dismissed. */
+ @Nullable protected DialogInterface.OnDismissListener mDismissListener;
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (mDismissListener != null) {
+ mDismissListener.onDismiss(dialog);
+ }
+ super.onDismiss(dialog);
+ }
+
+ @Override
+ public void onPause() {
+ // The dialog is dismissed onPause, i.e. rotation.
+ dismiss();
+ mDismissListener = null;
+ mPositiveListener = null;
+ mDisplayNumber = null;
+ super.onPause();
+ }
+ }
+
+ /** Dialog for block/report spam with the mark as spam checkbox. */
+ public static class BlockReportSpamDialogFragment extends CommonDialogsFragment {
+
+ /** Called when dialog positive button is pressed. */
+ private OnSpamDialogClickListener mPositiveListener;
+
+ /** Whether the mark as spam checkbox is checked before displaying the dialog. */
+ private boolean mSpamChecked;
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ boolean spamChecked,
+ OnSpamDialogClickListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ BlockReportSpamDialogFragment fragment = new BlockReportSpamDialogFragment();
+ fragment.mSpamChecked = spamChecked;
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ View dialogView = View.inflate(getActivity(), R.layout.block_report_spam_dialog, null);
+ final CheckBox isSpamCheckbox =
+ (CheckBox) dialogView.findViewById(R.id.report_number_as_spam_action);
+ // Listen for changes on the checkbox and update if orientation changes
+ isSpamCheckbox.setChecked(mSpamChecked);
+ isSpamCheckbox.setOnCheckedChangeListener(
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mSpamChecked = isChecked;
+ }
+ });
+
+ TextView details = (TextView) dialogView.findViewById(R.id.block_details);
+ details.setText(getBlockMessage(getContext()));
+
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ Dialog dialog =
+ alertDialogBuilder
+ .setView(dialogView)
+ .setTitle(getString(R.string.block_report_number_alert_title, mDisplayNumber))
+ .setPositiveButton(
+ R.string.block_number_ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ mPositiveListener.onClick(isSpamCheckbox.isChecked());
+ }
+ })
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+
+ /** Dialog for blocking a number. */
+ public static class BlockDialogFragment extends CommonDialogsFragment {
+
+ private boolean isSpamEnabled;
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ boolean isSpamEnabled,
+ OnConfirmListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ BlockDialogFragment fragment = new BlockDialogFragment();
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ fragment.isSpamEnabled = isSpamEnabled;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ // Return the newly created dialog
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ Dialog dialog =
+ alertDialogBuilder
+ .setTitle(getString(R.string.block_number_confirmation_title, mDisplayNumber))
+ .setMessage(
+ isSpamEnabled
+ ? getString(
+ R.string.block_number_alert_details, getBlockMessage(getContext()))
+ : getString(R.string.block_report_number_alert_details))
+ .setPositiveButton(
+ R.string.block_number_ok, createGenericOnClickListener(this, mPositiveListener))
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+
+ /** Dialog for unblocking a number. */
+ public static class UnblockDialogFragment extends CommonDialogsFragment {
+
+ /** Whether or not the number is spam. */
+ private boolean mIsSpam;
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ boolean isSpam,
+ OnConfirmListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ UnblockDialogFragment fragment = new UnblockDialogFragment();
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mIsSpam = isSpam;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ // Return the newly created dialog
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ if (mIsSpam) {
+ alertDialogBuilder
+ .setMessage(R.string.unblock_number_alert_details)
+ .setTitle(getString(R.string.unblock_report_number_alert_title, mDisplayNumber));
+ } else {
+ alertDialogBuilder.setMessage(
+ getString(R.string.unblock_report_number_alert_title, mDisplayNumber));
+ }
+ Dialog dialog =
+ alertDialogBuilder
+ .setPositiveButton(
+ R.string.unblock_number_ok, createGenericOnClickListener(this, mPositiveListener))
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+
+ /** Dialog for reporting a number as not spam. */
+ public static class ReportNotSpamDialogFragment extends CommonDialogsFragment {
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ OnConfirmListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ ReportNotSpamDialogFragment fragment = new ReportNotSpamDialogFragment();
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ // Return the newly created dialog
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ Dialog dialog =
+ alertDialogBuilder
+ .setTitle(R.string.report_not_spam_alert_title)
+ .setMessage(getString(R.string.report_not_spam_alert_details, mDisplayNumber))
+ .setPositiveButton(
+ R.string.report_not_spam_alert_button,
+ createGenericOnClickListener(this, mPositiveListener))
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+}
diff --git a/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java
new file mode 100644
index 0000000..1773e9b
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java
@@ -0,0 +1,110 @@
+/*
+ * 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.blocking;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener;
+import com.android.dialer.common.LogUtil;
+import java.util.Objects;
+
+/**
+ * Class responsible for checking if the user can be auto-migrated to {@link
+ * android.provider.BlockedNumberContract} blocking. In order for this to happen, the user cannot
+ * have any numbers that are blocked in the Dialer solution.
+ */
+public class BlockedNumbersAutoMigrator {
+
+ static final String HAS_CHECKED_AUTO_MIGRATE_KEY = "checkedAutoMigrate";
+
+ @NonNull private final Context context;
+ @NonNull private final SharedPreferences sharedPreferences;
+ @NonNull private final FilteredNumberAsyncQueryHandler queryHandler;
+
+ /**
+ * Constructs the BlockedNumbersAutoMigrator with the given {@link SharedPreferences} and {@link
+ * FilteredNumberAsyncQueryHandler}.
+ *
+ * @param sharedPreferences The SharedPreferences used to persist information.
+ * @param queryHandler The FilteredNumberAsyncQueryHandler used to determine if there are blocked
+ * numbers.
+ * @throws NullPointerException if sharedPreferences or queryHandler are null.
+ */
+ public BlockedNumbersAutoMigrator(
+ @NonNull Context context,
+ @NonNull SharedPreferences sharedPreferences,
+ @NonNull FilteredNumberAsyncQueryHandler queryHandler) {
+ this.context = Objects.requireNonNull(context);
+ this.sharedPreferences = Objects.requireNonNull(sharedPreferences);
+ this.queryHandler = Objects.requireNonNull(queryHandler);
+ }
+
+ /**
+ * Attempts to perform the auto-migration. Auto-migration will only be attempted once and can be
+ * performed only when the user has no blocked numbers. As a result of this method, the user will
+ * be migrated to the framework blocking solution, as determined by {@link
+ * FilteredNumberCompat#hasMigratedToNewBlocking()}.
+ */
+ public void autoMigrate() {
+ if (!shouldAttemptAutoMigrate()) {
+ return;
+ }
+
+ LogUtil.i("BlockedNumbersAutoMigrator", "attempting to auto-migrate.");
+ queryHandler.hasBlockedNumbers(
+ new OnHasBlockedNumbersListener() {
+ @Override
+ public void onHasBlockedNumbers(boolean hasBlockedNumbers) {
+ if (hasBlockedNumbers) {
+ LogUtil.i("BlockedNumbersAutoMigrator", "not auto-migrating: blocked numbers exist.");
+ return;
+ }
+ LogUtil.i("BlockedNumbersAutoMigrator", "auto-migrating: no blocked numbers.");
+ FilteredNumberCompat.setHasMigratedToNewBlocking(context, true);
+ }
+ });
+ }
+
+ private boolean shouldAttemptAutoMigrate() {
+ if (sharedPreferences.contains(HAS_CHECKED_AUTO_MIGRATE_KEY)) {
+ LogUtil.v("BlockedNumbersAutoMigrator", "not attempting auto-migrate: already checked once.");
+ return false;
+ }
+
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ // This may be the case where the user is on the lock screen, so we shouldn't record that the
+ // migration status was checked.
+ LogUtil.i(
+ "BlockedNumbersAutoMigrator", "not attempting auto-migrate: current user can't block");
+ return false;
+ }
+ LogUtil.i("BlockedNumbersAutoMigrator", "updating state as already checked for auto-migrate.");
+ sharedPreferences.edit().putBoolean(HAS_CHECKED_AUTO_MIGRATE_KEY, true).apply();
+
+ if (!FilteredNumberCompat.canUseNewFiltering()) {
+ LogUtil.i("BlockedNumbersAutoMigrator", "not attempting auto-migrate: not available.");
+ return false;
+ }
+
+ if (FilteredNumberCompat.hasMigratedToNewBlocking(context)) {
+ LogUtil.i("BlockedNumbersAutoMigrator", "not attempting auto-migrate: already migrated.");
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/dialer/blocking/BlockedNumbersMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java
new file mode 100644
index 0000000..88f474a
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2015 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.blocking;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.support.annotation.RequiresApi;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import java.util.Objects;
+
+/**
+ * Class which should be used to migrate numbers from {@link FilteredNumberContract} blocking to
+ * {@link android.provider.BlockedNumberContract} blocking.
+ */
+@TargetApi(VERSION_CODES.M)
+public class BlockedNumbersMigrator {
+
+ private final Context context;
+
+ /**
+ * Creates a new BlockedNumbersMigrate, using the given {@link ContentResolver} to perform queries
+ * against the blocked numbers tables.
+ */
+ public BlockedNumbersMigrator(Context context) {
+ this.context = Objects.requireNonNull(context);
+ }
+
+ @RequiresApi(VERSION_CODES.N)
+ @TargetApi(VERSION_CODES.N)
+ private static boolean migrateToNewBlockingInBackground(ContentResolver resolver) {
+ try (Cursor cursor =
+ resolver.query(
+ FilteredNumber.CONTENT_URI,
+ new String[] {FilteredNumberColumns.NUMBER},
+ null,
+ null,
+ null)) {
+ if (cursor == null) {
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground", "migrate - cursor was null");
+ return false;
+ }
+
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground",
+ "migrate - attempting to migrate " + cursor.getCount() + "numbers");
+
+ int numMigrated = 0;
+ while (cursor.moveToNext()) {
+ String originalNumber =
+ cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER));
+ if (isNumberInNewBlocking(resolver, originalNumber)) {
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground",
+ "migrate - number was already blocked in new blocking");
+ continue;
+ }
+ ContentValues values = new ContentValues();
+ values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, originalNumber);
+ resolver.insert(BlockedNumbers.CONTENT_URI, values);
+ ++numMigrated;
+ }
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground",
+ "migrate - migration complete. " + numMigrated + " numbers migrated.");
+ return true;
+ }
+ }
+
+ @RequiresApi(VERSION_CODES.N)
+ @TargetApi(VERSION_CODES.N)
+ private static boolean isNumberInNewBlocking(ContentResolver resolver, String originalNumber) {
+ try (Cursor cursor =
+ resolver.query(
+ BlockedNumbers.CONTENT_URI,
+ new String[] {BlockedNumbers.COLUMN_ID},
+ BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " = ?",
+ new String[] {originalNumber},
+ null)) {
+ return cursor != null && cursor.getCount() != 0;
+ }
+ }
+
+ /**
+ * Copies all of the numbers in the {@link FilteredNumberContract} block list to the {@link
+ * android.provider.BlockedNumberContract} block list.
+ *
+ * @param listener {@link Listener} called once the migration is complete.
+ * @return {@code true} if the migrate can be attempted, {@code false} otherwise.
+ * @throws NullPointerException if listener is null
+ */
+ public boolean migrate(final Listener listener) {
+ LogUtil.i("BlockedNumbersMigrator.migrate", "migrate - start");
+ if (!FilteredNumberCompat.canUseNewFiltering()) {
+ LogUtil.i("BlockedNumbersMigrator.migrate", "migrate - can't use new filtering");
+ return false;
+ }
+ Objects.requireNonNull(listener);
+ new MigratorTask(listener).execute();
+ return true;
+ }
+
+ /**
+ * Listener for the operation to migrate from {@link FilteredNumberContract} blocking to {@link
+ * android.provider.BlockedNumberContract} blocking.
+ */
+ public interface Listener {
+
+ /** Called when the migration operation is finished. */
+ void onComplete();
+ }
+
+ @TargetApi(VERSION_CODES.N)
+ private class MigratorTask extends AsyncTask<Void, Void, Boolean> {
+
+ private final Listener listener;
+
+ public MigratorTask(Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ LogUtil.i("BlockedNumbersMigrator.doInBackground", "migrate - start background migration");
+ return migrateToNewBlockingInBackground(context.getContentResolver());
+ }
+
+ @Override
+ protected void onPostExecute(Boolean isSuccessful) {
+ LogUtil.i("BlockedNumbersMigrator.onPostExecute", "migrate - marking migration complete");
+ FilteredNumberCompat.setHasMigratedToNewBlocking(context, isSuccessful);
+ LogUtil.i("BlockedNumbersMigrator.onPostExecute", "migrate - calling listener");
+ listener.onComplete();
+ }
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
new file mode 100644
index 0000000..852e7a0
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2015 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.blocking;
+
+import android.annotation.TargetApi;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.UserManagerCompat;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler {
+
+ public static final int INVALID_ID = -1;
+ // Id used to replace null for blocked id since ConcurrentHashMap doesn't allow null key/value.
+ @VisibleForTesting static final int BLOCKED_NUMBER_CACHE_NULL_ID = -1;
+
+ @VisibleForTesting
+ static final Map<String, Integer> blockedNumberCache = new ConcurrentHashMap<>();
+
+ private static final int NO_TOKEN = 0;
+ private final Context context;
+
+ public FilteredNumberAsyncQueryHandler(Context context) {
+ super(context.getContentResolver());
+ this.context = context;
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cookie != null) {
+ ((Listener) cookie).onQueryComplete(token, cookie, cursor);
+ }
+ }
+
+ @Override
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {
+ if (cookie != null) {
+ ((Listener) cookie).onInsertComplete(token, cookie, uri);
+ }
+ }
+
+ @Override
+ protected void onUpdateComplete(int token, Object cookie, int result) {
+ if (cookie != null) {
+ ((Listener) cookie).onUpdateComplete(token, cookie, result);
+ }
+ }
+
+ @Override
+ protected void onDeleteComplete(int token, Object cookie, int result) {
+ if (cookie != null) {
+ ((Listener) cookie).onDeleteComplete(token, cookie, result);
+ }
+ }
+
+ public void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) {
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ listener.onHasBlockedNumbers(false);
+ return;
+ }
+ startQuery(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0);
+ }
+ },
+ FilteredNumberCompat.getContentUri(context, null),
+ new String[] {FilteredNumberCompat.getIdColumnName(context)},
+ FilteredNumberCompat.useNewFiltering(context)
+ ? null
+ : FilteredNumberColumns.TYPE + "=" + FilteredNumberTypes.BLOCKED_NUMBER,
+ null,
+ null);
+ }
+
+ /**
+ * Checks if the given number is blocked, calling the given {@link OnCheckBlockedListener} with
+ * the id for the blocked number, {@link #INVALID_ID}, or {@code null} based on the result of the
+ * check.
+ */
+ public void isBlockedNumber(
+ final OnCheckBlockedListener listener, @Nullable final String number, String countryIso) {
+ if (number == null) {
+ listener.onCheckComplete(INVALID_ID);
+ return;
+ }
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ listener.onCheckComplete(null);
+ return;
+ }
+ Integer cachedId = blockedNumberCache.get(number);
+ if (cachedId != null) {
+ if (listener == null) {
+ return;
+ }
+ if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) {
+ cachedId = null;
+ }
+ listener.onCheckComplete(cachedId);
+ return;
+ }
+
+ String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number);
+ if (TextUtils.isEmpty(formattedNumber)) {
+ listener.onCheckComplete(INVALID_ID);
+ blockedNumberCache.put(number, INVALID_ID);
+ return;
+ }
+
+ if (!UserManagerCompat.isUserUnlocked(context)) {
+ LogUtil.i(
+ "FilteredNumberAsyncQueryHandler.isBlockedNumber",
+ "Device locked in FBE mode, cannot access blocked number database");
+ listener.onCheckComplete(INVALID_ID);
+ return;
+ }
+
+ startQuery(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ /*
+ * In the frameworking blocking, numbers can be blocked in both e164 format
+ * and not, resulting in multiple rows being returned for this query. For
+ * example, both '16502530000' and '6502530000' can exist at the same time
+ * and will be returned by this query.
+ */
+ if (cursor == null || cursor.getCount() == 0) {
+ blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
+ listener.onCheckComplete(null);
+ return;
+ }
+ cursor.moveToFirst();
+ // New filtering doesn't have a concept of type
+ if (!FilteredNumberCompat.useNewFiltering(context)
+ && cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE))
+ != FilteredNumberTypes.BLOCKED_NUMBER) {
+ blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
+ listener.onCheckComplete(null);
+ return;
+ }
+ Integer blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
+ blockedNumberCache.put(number, blockedId);
+ listener.onCheckComplete(blockedId);
+ }
+ },
+ FilteredNumberCompat.getContentUri(context, null),
+ FilteredNumberCompat.filter(
+ new String[] {
+ FilteredNumberCompat.getIdColumnName(context),
+ FilteredNumberCompat.getTypeColumnName(context)
+ }),
+ getIsBlockedNumberSelection(e164Number != null) + " = ?",
+ new String[] {formattedNumber},
+ null);
+ }
+
+ /**
+ * Synchronously check if this number has been blocked.
+ *
+ * @return blocked id.
+ */
+ @TargetApi(VERSION_CODES.M)
+ @Nullable
+ public Integer getBlockedIdSynchronousForCalllogOnly(@Nullable String number, String countryIso) {
+ Assert.isWorkerThread();
+ if (number == null) {
+ return null;
+ }
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ return null;
+ }
+ Integer cachedId = blockedNumberCache.get(number);
+ if (cachedId != null) {
+ if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) {
+ cachedId = null;
+ }
+ return cachedId;
+ }
+
+ String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number);
+ if (TextUtils.isEmpty(formattedNumber)) {
+ return null;
+ }
+
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ FilteredNumberCompat.getContentUri(context, null),
+ FilteredNumberCompat.filter(
+ new String[] {
+ FilteredNumberCompat.getIdColumnName(context),
+ FilteredNumberCompat.getTypeColumnName(context)
+ }),
+ getIsBlockedNumberSelection(e164Number != null) + " = ?",
+ new String[] {formattedNumber},
+ null)) {
+ /*
+ * In the frameworking blocking, numbers can be blocked in both e164 format
+ * and not, resulting in multiple rows being returned for this query. For
+ * example, both '16502530000' and '6502530000' can exist at the same time
+ * and will be returned by this query.
+ */
+ if (cursor == null || cursor.getCount() == 0) {
+ blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
+ return null;
+ }
+ cursor.moveToFirst();
+ int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
+ blockedNumberCache.put(number, blockedId);
+ return blockedId;
+ } catch (SecurityException e) {
+ LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly", null, e);
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ public void clearCache() {
+ blockedNumberCache.clear();
+ }
+
+ /*
+ * TODO: b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a
+ * temporary workaround, determine which column of the database to query based on whether the
+ * number is e164 or not.
+ */
+ private String getIsBlockedNumberSelection(boolean isE164Number) {
+ if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) {
+ return FilteredNumberCompat.getOriginalNumberColumnName(context);
+ }
+ return FilteredNumberCompat.getE164NumberColumnName(context);
+ }
+
+ public void blockNumber(
+ final OnBlockNumberListener listener, String number, @Nullable String countryIso) {
+ blockNumber(listener, null, number, countryIso);
+ }
+
+ /** Add a number manually blocked by the user. */
+ public void blockNumber(
+ final OnBlockNumberListener listener,
+ @Nullable String normalizedNumber,
+ String number,
+ @Nullable String countryIso) {
+ blockNumber(
+ listener,
+ FilteredNumberCompat.newBlockNumberContentValues(
+ context, number, normalizedNumber, countryIso));
+ }
+
+ /**
+ * Block a number with specified ContentValues. Can be manually added or a restored row from
+ * performing the 'undo' action after unblocking.
+ */
+ public void blockNumber(final OnBlockNumberListener listener, ContentValues values) {
+ blockedNumberCache.clear();
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ listener.onBlockComplete(null);
+ return;
+ }
+ startInsert(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ public void onInsertComplete(int token, Object cookie, Uri uri) {
+ if (listener != null) {
+ listener.onBlockComplete(uri);
+ }
+ }
+ },
+ FilteredNumberCompat.getContentUri(context, null),
+ values);
+ }
+
+ /**
+ * Unblocks the number with the given id.
+ *
+ * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
+ * unblocked.
+ * @param id The id of the number to unblock.
+ */
+ public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) {
+ if (id == null) {
+ throw new IllegalArgumentException("Null id passed into unblock");
+ }
+ unblock(listener, FilteredNumberCompat.getContentUri(context, id));
+ }
+
+ /**
+ * Removes row from database.
+ *
+ * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
+ * unblocked.
+ * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}.
+ */
+ public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) {
+ blockedNumberCache.clear();
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ if (listener != null) {
+ listener.onUnblockComplete(0, null);
+ }
+ return;
+ }
+ startQuery(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ int rowsReturned = cursor == null ? 0 : cursor.getCount();
+ if (rowsReturned != 1) {
+ throw new SQLiteDatabaseCorruptException(
+ "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected.");
+ }
+ cursor.moveToFirst();
+ final ContentValues values = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, values);
+ values.remove(FilteredNumberCompat.getIdColumnName(context));
+
+ startDelete(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ public void onDeleteComplete(int token, Object cookie, int result) {
+ if (listener != null) {
+ listener.onUnblockComplete(result, values);
+ }
+ }
+ },
+ uri,
+ null,
+ null);
+ }
+ },
+ uri,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ public interface OnCheckBlockedListener {
+
+ /**
+ * Invoked after querying if a number is blocked.
+ *
+ * @param id The ID of the row if blocked, null otherwise.
+ */
+ void onCheckComplete(Integer id);
+ }
+
+ public interface OnBlockNumberListener {
+
+ /**
+ * Invoked after inserting a blocked number.
+ *
+ * @param uri The uri of the newly created row.
+ */
+ void onBlockComplete(Uri uri);
+ }
+
+ public interface OnUnblockNumberListener {
+
+ /**
+ * Invoked after removing a blocked number
+ *
+ * @param rows The number of rows affected (expected value 1).
+ * @param values The deleted data (used for restoration).
+ */
+ void onUnblockComplete(int rows, ContentValues values);
+ }
+
+ public interface OnHasBlockedNumbersListener {
+
+ /**
+ * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false}
+ * otherwise.
+ */
+ void onHasBlockedNumbers(boolean hasBlockedNumbers);
+ }
+
+ /** Methods for FilteredNumberAsyncQueryHandler result returns. */
+ private abstract static class Listener {
+
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {}
+
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {}
+
+ protected void onUpdateComplete(int token, Object cookie, int result) {}
+
+ protected void onDeleteComplete(int token, Object cookie, int result) {}
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumberCompat.java b/java/com/android/dialer/blocking/FilteredNumberCompat.java
new file mode 100644
index 0000000..0ee85d8
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumberCompat.java
@@ -0,0 +1,320 @@
+/*
+ * 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.blocking;
+
+import android.annotation.TargetApi;
+import android.app.FragmentManager;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.UserManager;
+import android.preference.PreferenceManager;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
+import com.android.dialer.telecom.TelecomUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Compatibility class to encapsulate logic to switch between call blocking using {@link
+ * com.android.dialer.database.FilteredNumberContract} and using {@link
+ * android.provider.BlockedNumberContract}. This class should be used rather than explicitly
+ * referencing columns from either contract class in situations where both blocking solutions may be
+ * used.
+ */
+public class FilteredNumberCompat {
+
+ private static Boolean canAttemptBlockOperationsForTest;
+
+ @VisibleForTesting
+ public static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking";
+
+ /** @return The column name for ID in the filtered number database. */
+ public static String getIdColumnName(Context context) {
+ return useNewFiltering(context) ? BlockedNumbers.COLUMN_ID : FilteredNumberColumns._ID;
+ }
+
+ /**
+ * @return The column name for type in the filtered number database. Will be {@code null} for the
+ * framework blocking implementation.
+ */
+ @Nullable
+ public static String getTypeColumnName(Context context) {
+ return useNewFiltering(context) ? null : FilteredNumberColumns.TYPE;
+ }
+
+ /**
+ * @return The column name for source in the filtered number database. Will be {@code null} for
+ * the framework blocking implementation
+ */
+ @Nullable
+ public static String getSourceColumnName(Context context) {
+ return useNewFiltering(context) ? null : FilteredNumberColumns.SOURCE;
+ }
+
+ /** @return The column name for the original number in the filtered number database. */
+ public static String getOriginalNumberColumnName(Context context) {
+ return useNewFiltering(context)
+ ? BlockedNumbers.COLUMN_ORIGINAL_NUMBER
+ : FilteredNumberColumns.NUMBER;
+ }
+
+ /**
+ * @return The column name for country iso in the filtered number database. Will be {@code null}
+ * the framework blocking implementation
+ */
+ @Nullable
+ public static String getCountryIsoColumnName(Context context) {
+ return useNewFiltering(context) ? null : FilteredNumberColumns.COUNTRY_ISO;
+ }
+
+ /** @return The column name for the e164 formatted number in the filtered number database. */
+ public static String getE164NumberColumnName(Context context) {
+ return useNewFiltering(context)
+ ? BlockedNumbers.COLUMN_E164_NUMBER
+ : FilteredNumberColumns.NORMALIZED_NUMBER;
+ }
+
+ /**
+ * @return {@code true} if the current SDK version supports using new filtering, {@code false}
+ * otherwise.
+ */
+ public static boolean canUseNewFiltering() {
+ return VERSION.SDK_INT >= VERSION_CODES.N;
+ }
+
+ /**
+ * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary
+ * migration has been performed, {@code false} otherwise.
+ */
+ public static boolean useNewFiltering(Context context) {
+ return canUseNewFiltering() && hasMigratedToNewBlocking(context);
+ }
+
+ /**
+ * @return {@code true} if the user has migrated to use {@link
+ * android.provider.BlockedNumberContract} blocking, {@code false} otherwise.
+ */
+ public static boolean hasMigratedToNewBlocking(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false);
+ }
+
+ /**
+ * Called to inform this class whether the user has fully migrated to use {@link
+ * android.provider.BlockedNumberContract} blocking or not.
+ *
+ * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise.
+ */
+ public static void setHasMigratedToNewBlocking(Context context, boolean hasMigrated) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated)
+ .apply();
+ }
+
+ /**
+ * Gets the content {@link Uri} for number filtering.
+ *
+ * @param id The optional id to append with the base content uri.
+ * @return The Uri for number filtering.
+ */
+ public static Uri getContentUri(Context context, @Nullable Integer id) {
+ if (id == null) {
+ return getBaseUri(context);
+ }
+ return ContentUris.withAppendedId(getBaseUri(context), id);
+ }
+
+ private static Uri getBaseUri(Context context) {
+ // Explicit version check to aid static analysis
+ return useNewFiltering(context) && VERSION.SDK_INT >= VERSION_CODES.N
+ ? BlockedNumbers.CONTENT_URI
+ : FilteredNumber.CONTENT_URI;
+ }
+
+ /**
+ * Removes any null column names from the given projection array. This method is intended to be
+ * used to strip out any column names that aren't available in every version of number blocking.
+ * Example: {@literal getContext().getContentResolver().query( someUri, // Filtering ensures that
+ * no non-existant columns are queried FilteredNumberCompat.filter(new String[]
+ * {FilteredNumberCompat.getIdColumnName(), FilteredNumberCompat.getTypeColumnName()},
+ * FilteredNumberCompat.getE164NumberColumnName() + " = ?", new String[] {e164Number}); }
+ *
+ * @param projection The projection array.
+ * @return The filtered projection array.
+ */
+ @Nullable
+ public static String[] filter(@Nullable String[] projection) {
+ if (projection == null) {
+ return null;
+ }
+ List<String> filtered = new ArrayList<>();
+ for (String column : projection) {
+ if (column != null) {
+ filtered.add(column);
+ }
+ }
+ return filtered.toArray(new String[filtered.size()]);
+ }
+
+ /**
+ * Creates a new {@link ContentValues} suitable for inserting in the filtered number table.
+ *
+ * @param number The unformatted number to insert.
+ * @param e164Number (optional) The number to insert formatted to E164 standard.
+ * @param countryIso (optional) The country iso to use to format the number.
+ * @return The ContentValues to insert.
+ * @throws NullPointerException If number is null.
+ */
+ public static ContentValues newBlockNumberContentValues(
+ Context context, String number, @Nullable String e164Number, @Nullable String countryIso) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(getOriginalNumberColumnName(context), Objects.requireNonNull(number));
+ if (!useNewFiltering(context)) {
+ if (e164Number == null) {
+ e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ }
+ contentValues.put(getE164NumberColumnName(context), e164Number);
+ contentValues.put(getCountryIsoColumnName(context), countryIso);
+ contentValues.put(getTypeColumnName(context), FilteredNumberTypes.BLOCKED_NUMBER);
+ contentValues.put(getSourceColumnName(context), FilteredNumberSources.USER);
+ }
+ return contentValues;
+ }
+
+ /**
+ * Shows block number migration dialog if necessary.
+ *
+ * @param fragmentManager The {@link FragmentManager} used to show fragments.
+ * @param listener The {@link BlockedNumbersMigrator.Listener} to call when migration is complete.
+ * @return boolean True if migration dialog is shown.
+ */
+ public static boolean maybeShowBlockNumberMigrationDialog(
+ Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener) {
+ if (shouldShowMigrationDialog(context)) {
+ LogUtil.i(
+ "FilteredNumberCompat.maybeShowBlockNumberMigrationDialog",
+ "maybeShowBlockNumberMigrationDialog - showing migration dialog");
+ MigrateBlockedNumbersDialogFragment.newInstance(new BlockedNumbersMigrator(context), listener)
+ .show(fragmentManager, "MigrateBlockedNumbers");
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean shouldShowMigrationDialog(Context context) {
+ return canUseNewFiltering() && !hasMigratedToNewBlocking(context);
+ }
+
+ /**
+ * Creates the {@link Intent} which opens the blocked numbers management interface.
+ *
+ * @param context The {@link Context}.
+ * @return The intent.
+ */
+ public static Intent createManageBlockedNumbersIntent(Context context) {
+ // Explicit version check to aid static analysis
+ if (canUseNewFiltering()
+ && hasMigratedToNewBlocking(context)
+ && VERSION.SDK_INT >= VERSION_CODES.N) {
+ return context.getSystemService(TelecomManager.class).createManageBlockedNumbersIntent();
+ }
+ Intent intent = new Intent("com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS");
+ intent.setPackage(context.getPackageName());
+ return intent;
+ }
+
+ /**
+ * Method used to determine if block operations are possible.
+ *
+ * @param context The {@link Context}.
+ * @return {@code true} if the app and user can block numbers, {@code false} otherwise.
+ */
+ public static boolean canAttemptBlockOperations(Context context) {
+ if (canAttemptBlockOperationsForTest != null) {
+ return canAttemptBlockOperationsForTest;
+ }
+
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ // Dialer blocking, must be primary user
+ return context.getSystemService(UserManager.class).isSystemUser();
+ }
+
+ // Great Wall blocking, must be primary user and the default or system dialer
+ // TODO: check that we're the system Dialer
+ return TelecomUtil.isDefaultDialer(context)
+ && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
+ }
+
+ static void setCanAttemptBlockOperationsForTest(boolean canAttempt) {
+ canAttemptBlockOperationsForTest = canAttempt;
+ }
+
+ /**
+ * Used to determine if the call blocking settings can be opened.
+ *
+ * @param context The {@link Context}.
+ * @return {@code true} if the current user can open the call blocking settings, {@code false}
+ * otherwise.
+ */
+ public static boolean canCurrentUserOpenBlockSettings(Context context) {
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ // Dialer blocking, must be primary user
+ return context.getSystemService(UserManager.class).isSystemUser();
+ }
+ // BlockedNumberContract blocking, verify through Contract API
+ return TelecomUtil.isDefaultDialer(context)
+ && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
+ }
+
+ /**
+ * Calls {@link BlockedNumberContract#canCurrentUserBlockNumbers(Context)} in such a way that it
+ * never throws an exception. While on the CryptKeeper screen, the BlockedNumberContract isn't
+ * available, using this method ensures that the Dialer doesn't crash when on that screen.
+ *
+ * @param context The {@link Context}.
+ * @return the result of BlockedNumberContract#canCurrentUserBlockNumbers, or {@code false} if an
+ * exception was thrown.
+ */
+ @TargetApi(VERSION_CODES.N)
+ private static boolean safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context) {
+ try {
+ return BlockedNumberContract.canCurrentUserBlockNumbers(context);
+ } catch (Exception e) {
+ LogUtil.e(
+ "FilteredNumberCompat.safeBlockedNumbersContractCanCurrentUserBlockNumbers",
+ "Exception while querying BlockedNumberContract",
+ e);
+ return false;
+ }
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumberProvider.java b/java/com/android/dialer/blocking/FilteredNumberProvider.java
new file mode 100644
index 0000000..5d36903
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumberProvider.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 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.blocking;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.database.Database;
+import com.android.dialer.database.DialerDatabaseHelper;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+
+/** Filtered number content provider. */
+public class FilteredNumberProvider extends ContentProvider {
+
+ private static final int FILTERED_NUMBERS_TABLE = 1;
+ private static final int FILTERED_NUMBERS_TABLE_ID = 2;
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ private static final String TAG = FilteredNumberProvider.class.getSimpleName();
+ private DialerDatabaseHelper mDialerDatabaseHelper;
+
+ @Override
+ public boolean onCreate() {
+ mDialerDatabaseHelper = Database.get(getContext()).getDatabaseHelper(getContext());
+ if (mDialerDatabaseHelper == null) {
+ return false;
+ }
+ sUriMatcher.addURI(
+ FilteredNumberContract.AUTHORITY,
+ FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE,
+ FILTERED_NUMBERS_TABLE);
+ sUriMatcher.addURI(
+ FilteredNumberContract.AUTHORITY,
+ FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE + "/#",
+ FILTERED_NUMBERS_TABLE_ID);
+ return true;
+ }
+
+ @Override
+ public Cursor query(
+ Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ final SQLiteDatabase db = mDialerDatabaseHelper.getReadableDatabase();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE);
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case FILTERED_NUMBERS_TABLE:
+ break;
+ case FILTERED_NUMBERS_TABLE_ID:
+ qb.appendWhere(FilteredNumberColumns._ID + "=" + ContentUris.parseId(uri));
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null);
+ if (c != null) {
+ c.setNotificationUri(
+ getContext().getContentResolver(), FilteredNumberContract.FilteredNumber.CONTENT_URI);
+ } else {
+ Log.d(TAG, "CURSOR WAS NULL");
+ }
+ return c;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return FilteredNumberContract.FilteredNumber.CONTENT_ITEM_TYPE;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+ setDefaultValues(values);
+ long id = db.insert(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, null, values);
+ if (id < 0) {
+ return null;
+ }
+ notifyChange(uri);
+ return ContentUris.withAppendedId(uri, id);
+ }
+
+ @VisibleForTesting
+ protected long getCurrentTimeMs() {
+ return System.currentTimeMillis();
+ }
+
+ private void setDefaultValues(ContentValues values) {
+ if (values.getAsString(FilteredNumberColumns.COUNTRY_ISO) == null) {
+ values.put(FilteredNumberColumns.COUNTRY_ISO, GeoUtil.getCurrentCountryIso(getContext()));
+ }
+ if (values.getAsInteger(FilteredNumberColumns.TIMES_FILTERED) == null) {
+ values.put(FilteredNumberContract.FilteredNumberColumns.TIMES_FILTERED, 0);
+ }
+ if (values.getAsLong(FilteredNumberColumns.CREATION_TIME) == null) {
+ values.put(FilteredNumberColumns.CREATION_TIME, getCurrentTimeMs());
+ }
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case FILTERED_NUMBERS_TABLE:
+ break;
+ case FILTERED_NUMBERS_TABLE_ID:
+ selection = getSelectionWithId(selection, ContentUris.parseId(uri));
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ int rows =
+ db.delete(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, selection, selectionArgs);
+ if (rows > 0) {
+ notifyChange(uri);
+ }
+ return rows;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case FILTERED_NUMBERS_TABLE:
+ break;
+ case FILTERED_NUMBERS_TABLE_ID:
+ selection = getSelectionWithId(selection, ContentUris.parseId(uri));
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ int rows =
+ db.update(
+ DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, values, selection, selectionArgs);
+ if (rows > 0) {
+ notifyChange(uri);
+ }
+ return rows;
+ }
+
+ private String getSelectionWithId(String selection, long id) {
+ if (TextUtils.isEmpty(selection)) {
+ return FilteredNumberContract.FilteredNumberColumns._ID + "=" + id;
+ } else {
+ return selection + "AND " + FilteredNumberContract.FilteredNumberColumns._ID + "=" + id;
+ }
+ }
+
+ private void notifyChange(Uri uri) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
new file mode 100644
index 0000000..61ecf18
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2015 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.blocking;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.Settings;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.concurrent.TimeUnit;
+
+/** Utility to help with tasks related to filtered numbers. */
+public class FilteredNumbersUtil {
+
+ public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking";
+ public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10;
+ // Pref key for storing the time of end of the last emergency call in milliseconds after epoch.
+ protected static final String LAST_EMERGENCY_CALL_MS_PREF_KEY = "last_emergency_call_ms";
+ // Pref key for storing whether a notification has been dispatched to notify the user that call
+ // blocking has been disabled because of a recent emergency call.
+ protected static final String NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY =
+ "notified_call_blocking_disabled_by_emergency_call";
+ // Disable incoming call blocking if there was a call within the past 2 days.
+ private static final long RECENT_EMERGENCY_CALL_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 2;
+
+ /**
+ * Used for testing to specify the custom threshold value, in milliseconds for whether an
+ * emergency call is "recent". The default value will be used if this custom threshold is less
+ * than zero. For example, to set this threshold to 60 seconds:
+ *
+ * <p>adb shell settings put system dialer_emergency_call_threshold_ms 60000
+ */
+ private static final String RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY =
+ "dialer_emergency_call_threshold_ms";
+
+ /** Checks if there exists a contact with {@code Contacts.SEND_TO_VOICEMAIL} set to true. */
+ public static void checkForSendToVoicemailContact(
+ final Context context, final CheckForSendToVoicemailContactListener listener) {
+ final AsyncTask task =
+ new AsyncTask<Object, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Object... params) {
+ if (context == null || !PermissionsUtil.hasContactsPermissions(context)) {
+ return false;
+ }
+
+ final Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ Contacts.CONTENT_URI,
+ ContactsQuery.PROJECTION,
+ ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+ null,
+ null);
+
+ boolean hasSendToVoicemailContacts = false;
+ if (cursor != null) {
+ try {
+ hasSendToVoicemailContacts = cursor.getCount() > 0;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return hasSendToVoicemailContacts;
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasSendToVoicemailContact) {
+ if (listener != null) {
+ listener.onComplete(hasSendToVoicemailContact);
+ }
+ }
+ };
+ task.execute();
+ }
+
+ /**
+ * Blocks all the phone numbers of any contacts marked as SEND_TO_VOICEMAIL, then clears the
+ * SEND_TO_VOICEMAIL flag on those contacts.
+ */
+ public static void importSendToVoicemailContacts(
+ final Context context, final ImportSendToVoicemailContactsListener listener) {
+ Logger.get(context).logInteraction(InteractionEvent.Type.IMPORT_SEND_TO_VOICEMAIL);
+ final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler =
+ new FilteredNumberAsyncQueryHandler(context);
+
+ final AsyncTask<Object, Void, Boolean> task =
+ new AsyncTask<Object, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Object... params) {
+ if (context == null) {
+ return false;
+ }
+
+ // Get the phone number of contacts marked as SEND_TO_VOICEMAIL.
+ final Cursor phoneCursor =
+ context
+ .getContentResolver()
+ .query(
+ Phone.CONTENT_URI,
+ PhoneQuery.PROJECTION,
+ PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+ null,
+ null);
+
+ if (phoneCursor == null) {
+ return false;
+ }
+
+ try {
+ while (phoneCursor.moveToNext()) {
+ final String normalizedNumber =
+ phoneCursor.getString(PhoneQuery.NORMALIZED_NUMBER_COLUMN_INDEX);
+ final String number = phoneCursor.getString(PhoneQuery.NUMBER_COLUMN_INDEX);
+ if (normalizedNumber != null) {
+ // Block the phone number of the contact.
+ mFilteredNumberAsyncQueryHandler.blockNumber(
+ null, normalizedNumber, number, null);
+ }
+ }
+ } finally {
+ phoneCursor.close();
+ }
+
+ // Clear SEND_TO_VOICEMAIL on all contacts. The setting has been imported to Dialer.
+ ContentValues newValues = new ContentValues();
+ newValues.put(Contacts.SEND_TO_VOICEMAIL, 0);
+ context
+ .getContentResolver()
+ .update(
+ Contacts.CONTENT_URI,
+ newValues,
+ ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+ null);
+
+ return true;
+ }
+
+ @Override
+ public void onPostExecute(Boolean success) {
+ if (success) {
+ if (listener != null) {
+ listener.onImportComplete();
+ }
+ } else if (context != null) {
+ String toastStr = context.getString(R.string.send_to_voicemail_import_failed);
+ Toast.makeText(context, toastStr, Toast.LENGTH_SHORT).show();
+ }
+ }
+ };
+ task.execute();
+ }
+
+ /**
+ * WARNING: This method should NOT be executed on the UI thread. Use {@code
+ * FilteredNumberAsyncQueryHandler} to asynchronously check if a number is blocked.
+ */
+ public static boolean shouldBlockVoicemail(
+ Context context, String number, String countryIso, long voicemailDateMs) {
+ final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ if (TextUtils.isEmpty(normalizedNumber)) {
+ return false;
+ }
+
+ if (hasRecentEmergencyCall(context)) {
+ return false;
+ }
+
+ final Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ FilteredNumber.CONTENT_URI,
+ new String[] {FilteredNumberColumns.CREATION_TIME},
+ FilteredNumberColumns.NORMALIZED_NUMBER + "=?",
+ new String[] {normalizedNumber},
+ null);
+ if (cursor == null) {
+ return false;
+ }
+ try {
+ /*
+ * Block if number is found and it was added before this voicemail was received.
+ * The VVM's date is reported with precision to the minute, even though its
+ * magnitude is in milliseconds, so we perform the comparison in minutes.
+ */
+ return cursor.moveToFirst()
+ && TimeUnit.MINUTES.convert(voicemailDateMs, TimeUnit.MILLISECONDS)
+ >= TimeUnit.MINUTES.convert(cursor.getLong(0), TimeUnit.MILLISECONDS);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public static long getLastEmergencyCallTimeMillis(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, 0);
+ }
+
+ public static boolean hasRecentEmergencyCall(Context context) {
+ if (context == null) {
+ return false;
+ }
+
+ Long lastEmergencyCallTime = getLastEmergencyCallTimeMillis(context);
+ if (lastEmergencyCallTime == 0) {
+ return false;
+ }
+
+ return (System.currentTimeMillis() - lastEmergencyCallTime)
+ < getRecentEmergencyCallThresholdMs(context);
+ }
+
+ public static void recordLastEmergencyCallTime(Context context) {
+ if (context == null) {
+ return;
+ }
+
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, System.currentTimeMillis())
+ .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)
+ .apply();
+
+ maybeNotifyCallBlockingDisabled(context);
+ }
+
+ public static void maybeNotifyCallBlockingDisabled(final Context context) {
+ // The Dialer is not responsible for this notification after migrating
+ if (FilteredNumberCompat.useNewFiltering(context)) {
+ return;
+ }
+ // Skip if the user has already received a notification for the most recent emergency call.
+ if (PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)) {
+ return;
+ }
+
+ // If the user has blocked numbers, notify that call blocking is temporarily disabled.
+ FilteredNumberAsyncQueryHandler queryHandler = new FilteredNumberAsyncQueryHandler(context);
+ queryHandler.hasBlockedNumbers(
+ new OnHasBlockedNumbersListener() {
+ @Override
+ public void onHasBlockedNumbers(boolean hasBlockedNumbers) {
+ if (context == null || !hasBlockedNumbers) {
+ return;
+ }
+
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification.Builder builder =
+ new Notification.Builder(context)
+ .setSmallIcon(R.drawable.ic_block_24dp)
+ .setContentTitle(
+ context.getString(R.string.call_blocking_disabled_notification_title))
+ .setContentText(
+ context.getString(R.string.call_blocking_disabled_notification_text))
+ .setAutoCancel(true);
+
+ builder.setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 0,
+ FilteredNumberCompat.createManageBlockedNumbersIntent(context),
+ PendingIntent.FLAG_UPDATE_CURRENT));
+
+ notificationManager.notify(
+ CALL_BLOCKING_NOTIFICATION_TAG,
+ CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID,
+ builder.build());
+
+ // Record that the user has been notified for this emergency call.
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, true)
+ .apply();
+ }
+ });
+ }
+
+ /**
+ * @param e164Number The e164 formatted version of the number, or {@code null} if such a format
+ * doesn't exist.
+ * @param number The number to attempt blocking.
+ * @return {@code true} if the number can be blocked, {@code false} otherwise.
+ */
+ public static boolean canBlockNumber(Context context, String e164Number, String number) {
+ String blockableNumber = getBlockableNumber(context, e164Number, number);
+ return !TextUtils.isEmpty(blockableNumber)
+ && !PhoneNumberUtils.isEmergencyNumber(blockableNumber);
+ }
+
+ /**
+ * @param e164Number The e164 formatted version of the number, or {@code null} if such a format
+ * doesn't exist..
+ * @param number The number to attempt blocking.
+ * @return The version of the given number that can be blocked with the current blocking solution.
+ */
+ @Nullable
+ public static String getBlockableNumber(
+ Context context, @Nullable String e164Number, String number) {
+ if (!FilteredNumberCompat.useNewFiltering(context)) {
+ return e164Number;
+ }
+ return TextUtils.isEmpty(e164Number) ? number : e164Number;
+ }
+
+ private static long getRecentEmergencyCallThresholdMs(Context context) {
+ if (LogUtil.isVerboseEnabled()) {
+ long thresholdMs =
+ Settings.System.getLong(
+ context.getContentResolver(), RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY, 0);
+ return thresholdMs > 0 ? thresholdMs : RECENT_EMERGENCY_CALL_THRESHOLD_MS;
+ } else {
+ return RECENT_EMERGENCY_CALL_THRESHOLD_MS;
+ }
+ }
+
+ public interface CheckForSendToVoicemailContactListener {
+
+ void onComplete(boolean hasSendToVoicemailContact);
+ }
+
+ public interface ImportSendToVoicemailContactsListener {
+
+ void onImportComplete();
+ }
+
+ private static class ContactsQuery {
+
+ static final String[] PROJECTION = {Contacts._ID};
+
+ static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
+
+ static final int ID_COLUMN_INDEX = 0;
+ }
+
+ public static class PhoneQuery {
+
+ public static final String[] PROJECTION = {Contacts._ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER};
+
+ public static final int ID_COLUMN_INDEX = 0;
+ public static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1;
+ public static final int NUMBER_COLUMN_INDEX = 2;
+
+ public static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
+ }
+}
diff --git a/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java
new file mode 100644
index 0000000..76e50b3
--- /dev/null
+++ b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java
@@ -0,0 +1,113 @@
+/*
+ * 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.blocking;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnShowListener;
+import android.os.Bundle;
+import android.view.View;
+import com.android.dialer.blocking.BlockedNumbersMigrator.Listener;
+import java.util.Objects;
+
+/**
+ * Dialog fragment shown to users when they need to migrate to use {@link
+ * android.provider.BlockedNumberContract} for blocking.
+ */
+public class MigrateBlockedNumbersDialogFragment extends DialogFragment {
+
+ private BlockedNumbersMigrator mBlockedNumbersMigrator;
+ private BlockedNumbersMigrator.Listener mMigrationListener;
+
+ /**
+ * Creates a new MigrateBlockedNumbersDialogFragment.
+ *
+ * @param blockedNumbersMigrator The {@link BlockedNumbersMigrator} which will be used to migrate
+ * the numbers.
+ * @param migrationListener The {@link BlockedNumbersMigrator.Listener} to call when the migration
+ * is complete.
+ * @return The new MigrateBlockedNumbersDialogFragment.
+ * @throws NullPointerException if blockedNumbersMigrator or migrationListener are {@code null}.
+ */
+ public static DialogFragment newInstance(
+ BlockedNumbersMigrator blockedNumbersMigrator,
+ BlockedNumbersMigrator.Listener migrationListener) {
+ MigrateBlockedNumbersDialogFragment fragment = new MigrateBlockedNumbersDialogFragment();
+ fragment.mBlockedNumbersMigrator = Objects.requireNonNull(blockedNumbersMigrator);
+ fragment.mMigrationListener = Objects.requireNonNull(migrationListener);
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ AlertDialog dialog =
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.migrate_blocked_numbers_dialog_title)
+ .setMessage(R.string.migrate_blocked_numbers_dialog_message)
+ .setPositiveButton(R.string.migrate_blocked_numbers_dialog_allow_button, null)
+ .setNegativeButton(R.string.migrate_blocked_numbers_dialog_cancel_button, null)
+ .create();
+ // The Dialog's buttons aren't available until show is called, so an OnShowListener
+ // is used to set the positive button callback.
+ dialog.setOnShowListener(
+ new OnShowListener() {
+ @Override
+ public void onShow(DialogInterface dialog) {
+ final AlertDialog alertDialog = (AlertDialog) dialog;
+ alertDialog
+ .getButton(AlertDialog.BUTTON_POSITIVE)
+ .setOnClickListener(newPositiveButtonOnClickListener(alertDialog));
+ }
+ });
+ return dialog;
+ }
+
+ /*
+ * Creates a new View.OnClickListener to be used as the positive button in this dialog. The
+ * OnClickListener will grey out the dialog's positive and negative buttons while the migration
+ * is underway, and close the dialog once the migrate is complete.
+ */
+ private View.OnClickListener newPositiveButtonOnClickListener(final AlertDialog alertDialog) {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+ alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
+ mBlockedNumbersMigrator.migrate(
+ new Listener() {
+ @Override
+ public void onComplete() {
+ alertDialog.dismiss();
+ mMigrationListener.onComplete();
+ }
+ });
+ }
+ };
+ }
+
+ @Override
+ public void onPause() {
+ // The dialog is dismissed and state is cleaned up onPause, i.e. rotation.
+ dismiss();
+ mBlockedNumbersMigrator = null;
+ mMigrationListener = null;
+ super.onPause();
+ }
+}
diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png
new file mode 100644
index 0000000..2ccc89d
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png
new file mode 100644
index 0000000..dc0c995
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png
new file mode 100644
index 0000000..919a872
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png
new file mode 100644
index 0000000..ec1b33f
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png
new file mode 100644
index 0000000..70b82d6
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png
new file mode 100644
index 0000000..dc0c995
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png
new file mode 100644
index 0000000..7aba97b
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png
new file mode 100644
index 0000000..18e7764
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png
new file mode 100644
index 0000000..aed7668
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png
new file mode 100644
index 0000000..fddfa54
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png
new file mode 100644
index 0000000..aed7668
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png
new file mode 100644
index 0000000..f7cfacb
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png
new file mode 100644
index 0000000..0378d1b
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png
new file mode 100644
index 0000000..855e590
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png
new file mode 100644
index 0000000..7ef0d7a
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml b/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml
new file mode 100644
index 0000000..09d7989
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2015 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>
+ <shape android:shape="oval">
+ <solid android:color="@color/blocked_contact_background"/>
+ <size
+ android:height="24dp"
+ android:width="24dp"/>
+ </shape>
+ </item>
+
+ <item
+ android:drawable="@drawable/ic_report_24dp"
+ android:gravity="center"
+ android:height="18dp"
+ android:width="18dp"/>
+
+</layer-list>
diff --git a/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml b/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml
new file mode 100644
index 0000000..82e8d80
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="25dp"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/block_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:text="@string/block_report_number_alert_details"
+ android:textColor="@color/block_report_spam_primary_text_color"
+ android:textSize="@dimen/blocked_report_spam_primary_text_size"/>
+
+ <CheckBox
+ android:id="@+id/report_number_as_spam_action"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/checkbox_report_as_spam_action"
+ android:textSize="@dimen/blocked_report_spam_primary_text_size"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/blocking/res/values/colors.xml b/java/com/android/dialer/blocking/res/values/colors.xml
new file mode 100644
index 0000000..d1a567d
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/values/colors.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <!-- 87% black -->
+ <color name="block_report_spam_primary_text_color">#de000000</color>
+
+ <!-- Note, this is also used by InCallUi. -->
+ <color name="blocked_contact_background">#A52714</color>
+
+</resources>
diff --git a/java/com/android/dialer/blocking/res/values/dimens.xml b/java/com/android/dialer/blocking/res/values/dimens.xml
new file mode 100644
index 0000000..cd7cfe2
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/values/dimens.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2012 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="blocked_report_spam_primary_text_size">16sp</dimen>
+</resources>
diff --git a/java/com/android/dialer/blocking/res/values/strings.xml b/java/com/android/dialer/blocking/res/values/strings.xml
new file mode 100644
index 0000000..8abff45
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/values/strings.xml
@@ -0,0 +1,122 @@
+<!--
+ ~ 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Title for dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=30]-->
+ <string name="migrate_blocked_numbers_dialog_title">New, simplified blocking</string>
+
+ <!-- Body text for dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=NONE]-->
+ <string name="migrate_blocked_numbers_dialog_message">To better protect you, Phone needs to change how blocking works. Your blocked numbers will now stop both calls and texts and may be shared with other apps.</string>
+
+ <!-- Positive confirmation button for the dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=NONE]-->
+ <string name="migrate_blocked_numbers_dialog_allow_button">Allow</string>
+
+ <!-- Do not translate -->
+ <string name="migrate_blocked_numbers_dialog_cancel_button">@android:string/cancel</string>
+
+ <!-- Confirmation dialog title for blocking a number. [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_title">Block
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>?</string>
+
+ <!-- Confirmation dialog message for blocking a number with visual voicemail active.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_message_vvm">
+ Calls from this number will be blocked and voicemails will be automatically deleted.
+ </string>
+
+ <!-- Confirmation dialog message for blocking a number with no visual voicemail.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_message_no_vvm">
+ Calls from this number will be blocked, but the caller may still be able to leave you voicemails.
+ </string>
+
+ <!-- Confirmation dialog message for blocking a number with new filtering enabled.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_message_new_filtering">
+ You will no longer receive calls or texts from this number.
+ </string>
+
+ <!-- Block number alert dialog button [CHAR LIMIT=32] -->
+ <string name="block_number_ok">BLOCK</string>
+
+ <!-- Confirmation dialog for unblocking a number. [CHAR LIMIT=NONE] -->
+ <string name="unblock_number_confirmation_title">Unblock
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>?</string>
+
+ <!-- Unblock number alert dialog button [CHAR LIMIT=32] -->
+ <string name="unblock_number_ok">UNBLOCK</string>
+
+ <!-- Error message shown when user tries to add invalid number to the block list.
+ [CHAR LIMIT=64] -->
+ <string name="invalidNumber"><xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>
+ is invalid.</string>
+
+ <!-- Text for snackbar to undo blocking a number. [CHAR LIMIT=64] -->
+ <string name="snackbar_number_blocked">
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> blocked</string>
+
+ <!-- Text for snackbar to undo unblocking a number. [CHAR LIMIT=64] -->
+ <string name="snackbar_number_unblocked">
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>
+ unblocked</string>
+
+ <!-- Text for undo button in snackbar for blocking/unblocking number. [CHAR LIMIT=10] -->
+ <string name="block_number_undo">UNDO</string>
+
+ <!-- Error toast message for when send to voicemail import fails. [CHAR LIMIT=40] -->
+ <string name="send_to_voicemail_import_failed">Import failed</string>
+
+ <!-- Title of notification telling the user that call blocking has been temporarily disabled.
+ [CHAR LIMIT=56] -->
+ <string name="call_blocking_disabled_notification_title">
+ Call blocking disabled for 48 hours
+ </string>
+
+ <!-- Text for notification which provides the reason that call blocking has been temporarily
+ disabled. Namely, we disable call blocking after an emergency call in case of return
+ phone calls made by emergency services. [CHAR LIMIT=64] -->
+ <string name="call_blocking_disabled_notification_text">
+ Disabled because an emergency call was made.
+ </string>
+
+ <!-- Title of alert dialog after clicking on Block/report as spam. [CHAR LIMIT=100] -->
+ <string name="block_report_number_alert_title">Block <xliff:g id="number">%1$s</xliff:g>?</string>
+
+ <!-- Text in alert dialog after clicking on Block/report as spam. [CHAR LIMIT=100] -->
+ <string name="block_report_number_alert_details">You will no longer receive calls from this number.</string>
+
+ <!-- Text in alert dialog after clicking on Block. [CHAR LIMIT=100] -->
+ <string name="block_number_alert_details"><xliff:g id="text">%1$s</xliff:g> This call will be reported as spam.</string>
+
+ <!-- Text in alert dialog after clicking on Unblock. [CHAR LIMIT=100] -->
+ <string name="unblock_number_alert_details">This number will be unblocked and reported as not spam. Future calls won\'t be identified as spam.</string>
+
+ <!-- Title of alert dialog after clicking on Unblock. [CHAR LIMIT=100] -->
+ <string name="unblock_report_number_alert_title">Unblock <xliff:g id="number">%1$s</xliff:g>?</string>
+
+ <!-- Report not spam number alert dialog button [CHAR LIMIT=32] -->
+ <string name="report_not_spam_alert_button">Report</string>
+
+ <!-- Title of alert dialog after clicking on Report as not spam. [CHAR LIMIT=100] -->
+ <string name="report_not_spam_alert_title">Report a mistake?</string>
+
+ <!-- Text in alert dialog after clicking on Report as not spam. [CHAR LIMIT=100] -->
+ <string name="report_not_spam_alert_details">Future calls from <xliff:g id="number">%1$s</xliff:g> will no longer be identified as spam.</string>
+
+ <!-- Label for checkbox in the Alert dialog to allow the user to report the number as spam as well. [CHAR LIMIT=30] -->
+ <string name="checkbox_report_as_spam_action">Report call as spam</string>
+
+</resources>
diff --git a/java/com/android/dialer/buildtype/BuildType.java b/java/com/android/dialer/buildtype/BuildType.java
new file mode 100644
index 0000000..c0a54a5
--- /dev/null
+++ b/java/com/android/dialer/buildtype/BuildType.java
@@ -0,0 +1,62 @@
+/*
+ * 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.buildtype;
+
+import android.support.annotation.IntDef;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Utility to find out which build type the app is running as. */
+public class BuildType {
+
+ /** The type of build. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ BUGFOOD, FISHFOOD, DOGFOOD, RELEASE,
+ })
+ public @interface Type {}
+
+ public static final int BUGFOOD = 1;
+ public static final int FISHFOOD = 2;
+ public static final int DOGFOOD = 3;
+ public static final int RELEASE = 4;
+
+ private static int cachedBuildType;
+ private static boolean didInitializeBuildType;
+
+ @Type
+ public static synchronized int get() {
+ if (!didInitializeBuildType) {
+ didInitializeBuildType = true;
+ try {
+ Class<?> clazz = Class.forName(BuildTypeAccessor.class.getName() + "Impl");
+ BuildTypeAccessor accessorImpl = (BuildTypeAccessor) clazz.getConstructor().newInstance();
+ cachedBuildType = accessorImpl.getBuildType();
+ } catch (ReflectiveOperationException e) {
+ LogUtil.e("BuildType.get", "error creating BuildTypeAccessorImpl", e);
+ Assert.fail(
+ "Unable to get build type. To fix this error include one of the build type "
+ + "modules (bugfood, etc...) in your target.");
+ }
+ }
+ return cachedBuildType;
+ }
+
+ private BuildType() {}
+}
diff --git a/java/com/android/dialer/buildtype/BuildTypeAccessor.java b/java/com/android/dialer/buildtype/BuildTypeAccessor.java
new file mode 100644
index 0000000..940cf81
--- /dev/null
+++ b/java/com/android/dialer/buildtype/BuildTypeAccessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.buildtype;
+
+import com.android.dialer.proguard.UsedByReflection;
+
+/**
+ * Gets the build type. The functionality depends on a an implementation being present in the app
+ * that has the same package and the class name ending in "Impl". For example,
+ * com.android.dialer.buildtype.BuildTypeAccessorImpl. This class is found by the module using
+ * reflection.
+ */
+@UsedByReflection(value = "BuildType.java")
+/* package */ interface BuildTypeAccessor {
+ @BuildType.Type
+ int getBuildType();
+}
diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
new file mode 100644
index 0000000..e1f2cdc
--- /dev/null
+++ b/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
@@ -0,0 +1,30 @@
+/*
+ * 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.buildtype;
+
+import com.android.dialer.proguard.UsedByReflection;
+
+/** Gets the build type. */
+@UsedByReflection(value = "BuildType.java")
+public class BuildTypeAccessorImpl implements BuildTypeAccessor {
+
+ @Override
+ @BuildType.Type
+ public int getBuildType() {
+ return BuildType.DOGFOOD;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/AndroidManifest.xml b/java/com/android/dialer/callcomposer/AndroidManifest.xml
new file mode 100644
index 0000000..c99f22b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ 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.callcomposer">
+
+ <application>
+ <activity
+ android:name="com.android.dialer.callcomposer.CallComposerActivity"
+ android:exported="false"
+ android:theme="@style/Theme.AppCompat.CallComposer"
+ android:windowSoftInputMode="adjustResize"
+ android:screenOrientation="portrait"/>
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java
new file mode 100644
index 0000000..eef3d21
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java
@@ -0,0 +1,728 @@
+/*
+ * 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.callcomposer;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.FileProvider;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toolbar;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.callcomposer.CallComposerFragment.CallComposerListener;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask.Callback;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.UiUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.constants.Constants;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.State;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.extensions.StateExtension;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.protos.ProtoParsers;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.ViewUtil;
+import java.io.File;
+
+/**
+ * Implements an activity which prompts for a call with additional media for an outgoing call. The
+ * activity includes a pop up with:
+ *
+ * <ul>
+ * <li>Contact galleryIcon
+ * <li>Name
+ * <li>Number
+ * <li>Media options to attach a gallery image, camera image or a message
+ * </ul>
+ */
+public class CallComposerActivity extends AppCompatActivity
+ implements OnClickListener,
+ OnPageChangeListener,
+ CallComposerListener,
+ OnLayoutChangeListener,
+ AnimatorListener,
+ EnrichedCallManager.StateChangedListener {
+
+ private static final int VIEW_PAGER_ANIMATION_DURATION_MILLIS = 300;
+ private static final int ENTRANCE_ANIMATION_DURATION_MILLIS = 500;
+
+ private static final String ARG_CALL_COMPOSER_CONTACT = "CALL_COMPOSER_CONTACT";
+
+ private static final String ENTRANCE_ANIMATION_KEY = "entrance_animation_key";
+ private static final String CURRENT_INDEX_KEY = "current_index_key";
+ private static final String VIEW_PAGER_STATE_KEY = "view_pager_state_key";
+ private static final String LOCATIONS_KEY = "locations_key";
+ private static final String SESSION_ID_KEY = "session_id_key";
+
+ private CallComposerContact contact;
+ private Long sessionId = Session.NO_SESSION_ID;
+
+ private TextView nameView;
+ private TextView numberView;
+ private QuickContactBadge contactPhoto;
+ private RelativeLayout contactContainer;
+ private Toolbar toolbar;
+ private View sendAndCall;
+
+ private ImageView cameraIcon;
+ private ImageView galleryIcon;
+ private ImageView messageIcon;
+ private ViewPager pager;
+ private CallComposerPagerAdapter adapter;
+
+ private FrameLayout background;
+ private LinearLayout windowContainer;
+
+ private FastOutSlowInInterpolator interpolator;
+ private boolean shouldAnimateEntrance = true;
+ private boolean inFullscreenMode;
+ private boolean isSendAndCallHidingOrHidden = true;
+ private boolean isAnimatingContactBar;
+ private boolean layoutChanged;
+ private int currentIndex;
+ private int[] locations;
+ private int currentLocation;
+
+ @NonNull private EnrichedCallManager enrichedCallManager;
+
+ public static Intent newIntent(Context context, CallComposerContact contact) {
+ Intent intent = new Intent(context, CallComposerActivity.class);
+ ProtoParsers.put(intent, ARG_CALL_COMPOSER_CONTACT, contact);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.call_composer_activity);
+
+ nameView = (TextView) findViewById(R.id.contact_name);
+ numberView = (TextView) findViewById(R.id.phone_number);
+ contactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo);
+ cameraIcon = (ImageView) findViewById(R.id.call_composer_camera);
+ galleryIcon = (ImageView) findViewById(R.id.call_composer_photo);
+ messageIcon = (ImageView) findViewById(R.id.call_composer_message);
+ contactContainer = (RelativeLayout) findViewById(R.id.contact_bar);
+ pager = (ViewPager) findViewById(R.id.call_composer_view_pager);
+ background = (FrameLayout) findViewById(R.id.background);
+ windowContainer = (LinearLayout) findViewById(R.id.call_composer_container);
+ toolbar = (Toolbar) findViewById(R.id.toolbar);
+ sendAndCall = findViewById(R.id.send_and_call_button);
+
+ interpolator = new FastOutSlowInInterpolator();
+ adapter =
+ new CallComposerPagerAdapter(
+ getSupportFragmentManager(),
+ getResources().getInteger(R.integer.call_composer_message_limit));
+ pager.setAdapter(adapter);
+ pager.addOnPageChangeListener(this);
+
+ setActionBar(toolbar);
+ toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_white_24);
+ toolbar.setNavigationOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ background.addOnLayoutChangeListener(this);
+ cameraIcon.setOnClickListener(this);
+ galleryIcon.setOnClickListener(this);
+ messageIcon.setOnClickListener(this);
+ sendAndCall.setOnClickListener(this);
+
+ onHandleIntent(getIntent());
+
+ enrichedCallManager = EnrichedCallManager.Accessor.getInstance(getApplication());
+ if (savedInstanceState != null) {
+ shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_KEY);
+ locations = savedInstanceState.getIntArray(LOCATIONS_KEY);
+ pager.onRestoreInstanceState(savedInstanceState.getParcelable(VIEW_PAGER_STATE_KEY));
+ currentIndex = savedInstanceState.getInt(CURRENT_INDEX_KEY);
+ sessionId = savedInstanceState.getLong(SESSION_ID_KEY);
+ onPageSelected(currentIndex);
+ } else {
+ locations = new int[adapter.getCount()];
+ for (int i = 0; i < locations.length; i++) {
+ locations[i] = CallComposerFragment.CONTENT_TOP_UNSET;
+ }
+ sessionId = enrichedCallManager.startCallComposerSession(contact.number);
+ }
+
+ // Since we can't animate the views until they are ready to be drawn, we use this listener to
+ // track that and animate the call compose UI as soon as it's ready.
+ ViewUtil.doOnPreDraw(
+ windowContainer,
+ false,
+ new Runnable() {
+ @Override
+ public void run() {
+ runEntranceAnimation();
+ }
+ });
+
+ setMediaIconSelected(0);
+
+ // This activity is started using startActivityForResult. By default, mark this as succeeded
+ // and flip this to RESULT_CANCELED if something goes wrong.
+ setResult(RESULT_OK);
+
+ if (sessionId == Session.NO_SESSION_ID) {
+ LogUtil.w("CallComposerActivity.onCreate", "failed to create call composer session");
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ enrichedCallManager.registerStateChangedListener(this);
+ refreshUiForCallComposerState();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ enrichedCallManager.unregisterStateChangedListener(this);
+ }
+
+ @Override
+ public void onEnrichedCallStateChanged() {
+ refreshUiForCallComposerState();
+ }
+
+ private void refreshUiForCallComposerState() {
+ Session session = enrichedCallManager.getSession(sessionId);
+ if (session == null) {
+ return;
+ }
+
+ @State int state = session.getState();
+ LogUtil.i(
+ "CallComposerActivity.refreshUiForCallComposerState",
+ "state: %s",
+ StateExtension.toString(state));
+
+ if (state == EnrichedCallManager.STATE_START_FAILED
+ || state == EnrichedCallManager.STATE_CLOSED) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ onHandleIntent(intent);
+ }
+
+ @Override
+ public void onClick(View view) {
+ LogUtil.enterBlock("CallComposerActivity.onClick");
+ if (view == cameraIcon) {
+ pager.setCurrentItem(CallComposerPagerAdapter.INDEX_CAMERA, true /* animate */);
+ } else if (view == galleryIcon) {
+ pager.setCurrentItem(CallComposerPagerAdapter.INDEX_GALLERY, true /* animate */);
+ } else if (view == messageIcon) {
+ pager.setCurrentItem(CallComposerPagerAdapter.INDEX_MESSAGE, true /* animate */);
+ } else if (view == sendAndCall) {
+ if (!sessionReady()) {
+ LogUtil.i(
+ "CallComposerActivity.onClick", "sendAndCall pressed, but the session isn't ready");
+ Logger.get(this)
+ .logImpression(
+ DialerImpression.Type
+ .CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY);
+ return;
+ }
+ sendAndCall.setEnabled(false);
+ CallComposerFragment fragment =
+ (CallComposerFragment) adapter.instantiateItem(pager, currentIndex);
+ MultimediaData.Builder builder = MultimediaData.builder();
+
+ if (fragment instanceof MessageComposerFragment) {
+ MessageComposerFragment messageComposerFragment = (MessageComposerFragment) fragment;
+ builder.setSubject(messageComposerFragment.getMessage());
+ placeRCSCall(builder);
+ }
+ if (fragment instanceof GalleryComposerFragment) {
+ GalleryComposerFragment galleryComposerFragment = (GalleryComposerFragment) fragment;
+ // If the current data is not a copy, make one.
+ if (!galleryComposerFragment.selectedDataIsCopy()) {
+ new CopyAndResizeImageTask(
+ CallComposerActivity.this,
+ galleryComposerFragment.getGalleryData().getFileUri(),
+ new Callback() {
+ @Override
+ public void onCopySuccessful(File file, String mimeType) {
+ Uri shareableUri =
+ FileProvider.getUriForFile(
+ CallComposerActivity.this,
+ Constants.get().getFileProviderAuthority(),
+ file);
+
+ builder.setImage(grantUriPermission(shareableUri), mimeType);
+ placeRCSCall(builder);
+ }
+
+ @Override
+ public void onCopyFailed(Throwable throwable) {
+ // TODO(b/33753902)
+ LogUtil.e("CallComposerActivity.onCopyFailed", "copy Failed", throwable);
+ }
+ })
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ Uri shareableUri =
+ FileProvider.getUriForFile(
+ this,
+ Constants.get().getFileProviderAuthority(),
+ new File(galleryComposerFragment.getGalleryData().getFilePath()));
+
+ builder.setImage(
+ grantUriPermission(shareableUri),
+ galleryComposerFragment.getGalleryData().getMimeType());
+
+ placeRCSCall(builder);
+ }
+ }
+ if (fragment instanceof CameraComposerFragment) {
+ CameraComposerFragment cameraComposerFragment = (CameraComposerFragment) fragment;
+ cameraComposerFragment.getCameraUriWhenReady(
+ uri -> {
+ builder.setImage(grantUriPermission(uri), cameraComposerFragment.getMimeType());
+ placeRCSCall(builder);
+ });
+ }
+ } else {
+ Assert.fail();
+ }
+ }
+
+ private boolean sessionReady() {
+ Session session = enrichedCallManager.getSession(sessionId);
+ if (session == null) {
+ return false;
+ }
+
+ return session.getState() == EnrichedCallManager.STATE_STARTED;
+ }
+
+ private void placeRCSCall(MultimediaData.Builder builder) {
+ LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call");
+ Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL);
+ enrichedCallManager.sendCallComposerData(sessionId, builder.build());
+ TelecomUtil.placeCall(
+ this, new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_COMPOSER).build());
+ finish();
+ }
+
+ /** Give permission to Messenger to view our image for RCS purposes. */
+ private Uri grantUriPermission(Uri uri) {
+ // TODO: Move this to the enriched call manager.
+ grantUriPermission(
+ "com.google.android.apps.messaging", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ return uri;
+ }
+
+ /** Animates {@code contactContainer} to align with content inside viewpager. */
+ @Override
+ public void onPageSelected(int position) {
+ if (currentIndex == CallComposerPagerAdapter.INDEX_MESSAGE) {
+ UiUtil.hideKeyboardFrom(this, windowContainer);
+ } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE && inFullscreenMode) {
+ UiUtil.openKeyboardFrom(this, windowContainer);
+ }
+ currentIndex = position;
+ CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
+ locations[currentIndex] = fragment.getContentTopPx();
+ animateContactContainer(locations[currentIndex]);
+ animateSendAndCall(fragment.shouldHide());
+ setMediaIconSelected(position);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
+ animateContactContainer(fragment.getContentTopPx());
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {}
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(VIEW_PAGER_STATE_KEY, pager.onSaveInstanceState());
+ outState.putBoolean(ENTRANCE_ANIMATION_KEY, shouldAnimateEntrance);
+ outState.putInt(CURRENT_INDEX_KEY, currentIndex);
+ outState.putIntArray(LOCATIONS_KEY, locations);
+ outState.putLong(SESSION_ID_KEY, sessionId);
+ }
+
+ @Override
+ public void onBackPressed() {
+ runExitAnimation();
+ }
+
+ @Override
+ public void composeCall(CallComposerFragment fragment) {
+ // Since our ViewPager restores state to our fragments, it's possible that they could call
+ // #composeCall, so we have to check if the calling fragment is the current fragment.
+ if (adapter.instantiateItem(pager, currentIndex) != fragment) {
+ return;
+ }
+ animateSendAndCall(fragment.shouldHide());
+ }
+
+ // To detect when the keyboard changes.
+ @Override
+ public void onLayoutChange(
+ View view,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) {
+ // To prevent infinite layout change loops
+ if (layoutChanged) {
+ layoutChanged = false;
+ return;
+ }
+
+ layoutChanged = true;
+ if (pager.getTop() < 0 || inFullscreenMode) {
+ ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
+ layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
+ pager.setLayoutParams(layoutParams);
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ isAnimatingContactBar = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ isAnimatingContactBar = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+
+ /**
+ * Reads arguments from the fragment arguments and populates the necessary instance variables.
+ * Copied from {@link com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ private void onHandleIntent(Intent intent) {
+ Bundle arguments = intent.getExtras();
+ if (arguments == null) {
+ throw new RuntimeException("CallComposerActivity.onHandleIntent, Arguments cannot be null.");
+ }
+ contact =
+ ProtoParsers.getFromInstanceState(
+ arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact());
+ updateContactInfo();
+ }
+
+ /**
+ * Populates the contact info fields based on the current contact information. Copied from {@link
+ * com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ private void updateContactInfo() {
+ if (contact.contactUri != null) {
+ setPhoto(
+ contact.photoId,
+ Uri.parse(contact.photoUri),
+ Uri.parse(contact.contactUri),
+ contact.nameOrNumber,
+ contact.isBusiness);
+ } else {
+ contactPhoto.setVisibility(View.GONE);
+ }
+ nameView.setText(contact.nameOrNumber);
+ getActionBar().setTitle(contact.nameOrNumber);
+ if (!TextUtils.isEmpty(contact.numberLabel) && !TextUtils.isEmpty(contact.displayNumber)) {
+ numberView.setVisibility(View.VISIBLE);
+ String secondaryInfo =
+ getString(
+ com.android.contacts.common.R.string.call_subject_type_and_number,
+ contact.numberLabel,
+ contact.displayNumber);
+ numberView.setText(secondaryInfo);
+ toolbar.setSubtitle(secondaryInfo);
+ } else {
+ numberView.setVisibility(View.GONE);
+ numberView.setText(null);
+ }
+ }
+
+ /**
+ * Sets the photo on the quick contact galleryIcon. Copied from {@link
+ * com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ private void setPhoto(
+ long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) {
+ contactPhoto.assignContactUri(contactUri);
+ if (CompatUtils.isLollipopCompatible()) {
+ contactPhoto.setOverlay(null);
+ }
+
+ int contactType;
+ if (isBusiness) {
+ contactType = ContactPhotoManager.TYPE_BUSINESS;
+ } else {
+ contactType = ContactPhotoManager.TYPE_DEFAULT;
+ }
+
+ String lookupKey = null;
+ if (contactUri != null) {
+ lookupKey = UriUtils.getLookupKeyFromUri(contactUri);
+ }
+
+ ContactPhotoManager.DefaultImageRequest request =
+ new ContactPhotoManager.DefaultImageRequest(
+ displayName, lookupKey, contactType, true /* isCircular */);
+
+ if (photoId == 0 && photoUri != null) {
+ contactPhoto.setImageDrawable(
+ getDrawable(R.drawable.product_logo_avatar_anonymous_color_120));
+ } else {
+ ContactPhotoManager.getInstance(this)
+ .loadThumbnail(
+ contactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request);
+ }
+ }
+
+ private void animateContactContainer(int toY) {
+ if (toY == CallComposerFragment.CONTENT_TOP_UNSET
+ || toY == currentLocation
+ || (toY != locations[currentIndex]
+ && locations[currentIndex] != CallComposerFragment.CONTENT_TOP_UNSET)
+ || isAnimatingContactBar
+ || inFullscreenMode) {
+ return;
+ }
+ currentLocation = toY;
+ contactContainer
+ .animate()
+ .translationY(toY)
+ .setInterpolator(interpolator)
+ .setDuration(VIEW_PAGER_ANIMATION_DURATION_MILLIS)
+ .setListener(this)
+ .start();
+ }
+
+ /** Animates compose UI into view */
+ private void runEntranceAnimation() {
+ if (!shouldAnimateEntrance) {
+ return;
+ }
+ shouldAnimateEntrance = false;
+
+ int colorFrom = ContextCompat.getColor(this, android.R.color.transparent);
+ int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color);
+ ValueAnimator backgroundAnimation =
+ ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+ backgroundAnimation.setInterpolator(interpolator);
+ backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
+ backgroundAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ background.setBackgroundColor((int) animator.getAnimatedValue());
+ }
+ });
+
+ ValueAnimator contentAnimation = ValueAnimator.ofFloat(windowContainer.getHeight(), 0);
+ contentAnimation.setInterpolator(interpolator);
+ contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
+ contentAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ windowContainer.setY((Float) animation.getAnimatedValue());
+ }
+ });
+
+ AnimatorSet set = new AnimatorSet();
+ set.play(contentAnimation).with(backgroundAnimation);
+ set.start();
+ }
+
+ /** Animates compose UI out of view and ends the activity. */
+ private void runExitAnimation() {
+ int colorTo = ContextCompat.getColor(this, android.R.color.transparent);
+ int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color);
+ ValueAnimator backgroundAnimation =
+ ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+ backgroundAnimation.setInterpolator(interpolator);
+ backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
+ backgroundAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ background.setBackgroundColor((int) animator.getAnimatedValue());
+ }
+ });
+
+ ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, windowContainer.getHeight());
+ contentAnimation.setInterpolator(interpolator);
+ contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
+ contentAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ windowContainer.setY((Float) animation.getAnimatedValue());
+ if (animation.getAnimatedFraction() > .75) {
+ finish();
+ }
+ }
+ });
+ AnimatorSet set = new AnimatorSet();
+ set.play(contentAnimation).with(backgroundAnimation);
+ set.start();
+ }
+
+ @Override
+ public void showFullscreen(boolean show) {
+ if (inFullscreenMode == show) {
+ return;
+ }
+ inFullscreenMode = show;
+ toolbar.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
+ contactContainer.setVisibility(show ? View.GONE : View.VISIBLE);
+ ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
+ if (show) {
+ layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
+ } else {
+ layoutParams.height =
+ getResources().getDimensionPixelSize(R.dimen.call_composer_view_pager_height);
+ }
+ pager.setLayoutParams(layoutParams);
+ }
+
+ @Override
+ public boolean isFullscreen() {
+ return inFullscreenMode;
+ }
+
+ private void animateSendAndCall(final boolean shouldHide) {
+ // createCircularReveal doesn't respect animations being disabled, handle it here.
+ if (ViewUtil.areAnimationsDisabled(this)) {
+ isSendAndCallHidingOrHidden = shouldHide;
+ sendAndCall.setVisibility(shouldHide ? View.INVISIBLE : View.VISIBLE);
+ return;
+ }
+
+ // If the animation is changing directions, start it again. Else do nothing.
+ if (isSendAndCallHidingOrHidden != shouldHide) {
+ int centerX = sendAndCall.getWidth() / 2;
+ int centerY = sendAndCall.getHeight() / 2;
+ int startRadius = shouldHide ? centerX : 0;
+ int endRadius = shouldHide ? 0 : centerX;
+
+ // When the device rotates and state is restored, the send and call button may not be attached
+ // yet and this causes a crash when we attempt to to reveal it. To prevent this, we wait until
+ // {@code sendAndCall} is ready, then animate and reveal it.
+ ViewUtil.doOnPreDraw(
+ sendAndCall,
+ true,
+ new Runnable() {
+ @Override
+ public void run() {
+ Animator animator =
+ ViewAnimationUtils.createCircularReveal(
+ sendAndCall, centerX, centerY, startRadius, endRadius);
+ animator.addListener(
+ new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ isSendAndCallHidingOrHidden = shouldHide;
+ sendAndCall.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (isSendAndCallHidingOrHidden) {
+ sendAndCall.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+ animator.start();
+ }
+ });
+ }
+ }
+
+ private void setMediaIconSelected(int position) {
+ float alpha = 0.54f;
+ cameraIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_CAMERA ? 1 : alpha);
+ galleryIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_GALLERY ? 1 : alpha);
+ messageIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_MESSAGE ? 1 : alpha);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/CallComposerFragment.java b/java/com/android/dialer/callcomposer/CallComposerFragment.java
new file mode 100644
index 0000000..d6f9449
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CallComposerFragment.java
@@ -0,0 +1,125 @@
+/*
+ * 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.callcomposer;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+
+/** Base fragment with fields and methods needed for all fragments in the call compose UI. */
+public abstract class CallComposerFragment extends Fragment {
+
+ protected static final int CAMERA_PERMISSION = 1;
+ protected static final int STORAGE_PERMISSION = 2;
+
+ private static final String LOCATION_KEY = "location_key";
+ public static final int CONTENT_TOP_UNSET = Integer.MAX_VALUE;
+
+ private View topView;
+ private int contentTopPx = CONTENT_TOP_UNSET;
+ private CallComposerListener testListener;
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = super.onCreateView(layoutInflater, viewGroup, bundle);
+ Assert.isNotNull(topView);
+ return view;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (!(context instanceof CallComposerListener) && testListener == null) {
+ LogUtil.e(
+ "CallComposerFragment.onAttach",
+ "Container activity must implement CallComposerListener.");
+ Assert.fail();
+ }
+ }
+
+ /** Call this method to declare which view is located at the top of the fragment's layout. */
+ public void setTopView(View view) {
+ topView = view;
+ // For each fragment that extends CallComposerFragment, the heights may vary and since
+ // ViewPagers cannot have their height set to wrap_content, we have to adjust the top of our
+ // container to match the top of the fragment. This listener populates {@code contentTopPx} as
+ // it's available.
+ topView
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ topView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ contentTopPx = topView.getTop();
+ }
+ });
+ }
+
+ public int getContentTopPx() {
+ return contentTopPx;
+ }
+
+ public void setParentForTesting(CallComposerListener listener) {
+ testListener = listener;
+ }
+
+ public CallComposerListener getListener() {
+ if (testListener != null) {
+ return testListener;
+ }
+ return FragmentUtils.getParentUnsafe(this, CallComposerListener.class);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(LOCATION_KEY, contentTopPx);
+ }
+
+ @Override
+ public void onViewStateRestored(Bundle savedInstanceState) {
+ super.onViewStateRestored(savedInstanceState);
+ if (savedInstanceState != null) {
+ contentTopPx = savedInstanceState.getInt(LOCATION_KEY);
+ }
+ }
+
+ public abstract boolean shouldHide();
+
+ /** Interface used to listen to CallComposeFragments */
+ public interface CallComposerListener {
+ /** Let the listener know when a call is ready to be composed. */
+ void composeCall(CallComposerFragment fragment);
+
+ /** Let the listener know when the layout has changed to full screen */
+ void showFullscreen(boolean show);
+
+ /** True is the listener is in fullscreen. */
+ boolean isFullscreen();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
new file mode 100644
index 0000000..4d4058a
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * 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.callcomposer;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import com.android.dialer.common.Assert;
+
+/** ViewPager adapter for call compose UI. */
+public class CallComposerPagerAdapter extends FragmentStatePagerAdapter {
+
+ public static final int INDEX_CAMERA = 0;
+ public static final int INDEX_GALLERY = 1;
+ public static final int INDEX_MESSAGE = 2;
+
+ private final int messageComposerCharLimit;
+
+ public CallComposerPagerAdapter(FragmentManager fragmentManager, int messageComposerCharLimit) {
+ super(fragmentManager);
+ this.messageComposerCharLimit = messageComposerCharLimit;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case INDEX_MESSAGE:
+ return MessageComposerFragment.newInstance(messageComposerCharLimit);
+ case INDEX_GALLERY:
+ return GalleryComposerFragment.newInstance();
+ case INDEX_CAMERA:
+ return new CameraComposerFragment();
+ default:
+ Assert.fail();
+ return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return 3;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/CameraComposerFragment.java b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
new file mode 100644
index 0000000..f2d0a94
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
@@ -0,0 +1,378 @@
+/*
+ * 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.callcomposer;
+
+import android.Manifest;
+import android.Manifest.permission;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Animatable;
+import android.hardware.Camera.CameraInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.dialer.callcomposer.camera.CameraManager;
+import com.android.dialer.callcomposer.camera.CameraManager.CameraManagerListener;
+import com.android.dialer.callcomposer.camera.CameraManager.MediaCallback;
+import com.android.dialer.callcomposer.camera.CameraPreview.CameraPreviewHost;
+import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay;
+import com.android.dialer.callcomposer.cameraui.CameraMediaChooserView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Fragment used to compose call with image from the user's camera. */
+public class CameraComposerFragment extends CallComposerFragment
+ implements CameraManagerListener, OnClickListener, CameraManager.MediaCallback {
+
+ private View permissionView;
+ private ImageButton exitFullscreen;
+ private ImageButton fullscreen;
+ private ImageButton swapCamera;
+ private ImageButton capture;
+ private ImageButton cancel;
+ private CameraMediaChooserView cameraView;
+ private RenderOverlay focus;
+ private View shutter;
+ private View allowPermission;
+ private CameraPreviewHost preview;
+ private ProgressBar loading;
+
+ private Uri cameraUri;
+ private boolean processingUri;
+ private String[] permissions = new String[] {Manifest.permission.CAMERA};
+ private CameraUriCallback uriCallback;
+
+ public static CameraComposerFragment newInstance() {
+ return new CameraComposerFragment();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) {
+ View root = inflater.inflate(R.layout.fragment_camera_composer, container, false);
+ permissionView = root.findViewById(R.id.permission_view);
+ loading = (ProgressBar) root.findViewById(R.id.loading);
+ cameraView = (CameraMediaChooserView) root.findViewById(R.id.camera_view);
+ shutter = cameraView.findViewById(R.id.camera_shutter_visual);
+ exitFullscreen = (ImageButton) cameraView.findViewById(R.id.camera_exit_fullscreen);
+ fullscreen = (ImageButton) cameraView.findViewById(R.id.camera_fullscreen);
+ swapCamera = (ImageButton) cameraView.findViewById(R.id.swap_camera_button);
+ capture = (ImageButton) cameraView.findViewById(R.id.camera_capture_button);
+ cancel = (ImageButton) cameraView.findViewById(R.id.camera_cancel_button);
+ focus = (RenderOverlay) cameraView.findViewById(R.id.focus_visual);
+ preview = (CameraPreviewHost) cameraView.findViewById(R.id.camera_preview);
+
+ exitFullscreen.setOnClickListener(this);
+ fullscreen.setOnClickListener(this);
+ swapCamera.setOnClickListener(this);
+ capture.setOnClickListener(this);
+ cancel.setOnClickListener(this);
+
+ if (!PermissionsUtil.hasPermission(getContext(), permission.CAMERA)) {
+ LogUtil.i("CameraComposerFragment.onCreateView", "Permission view shown.");
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DISPLAYED);
+ ImageView permissionImage = (ImageView) permissionView.findViewById(R.id.permission_icon);
+ TextView permissionText = (TextView) permissionView.findViewById(R.id.permission_text);
+ allowPermission = permissionView.findViewById(R.id.allow);
+
+ allowPermission.setOnClickListener(this);
+ permissionText.setText(R.string.camera_permission_text);
+ permissionImage.setImageResource(R.drawable.quantum_ic_camera_alt_white_48);
+ permissionImage.setColorFilter(
+ ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
+ permissionView.setVisibility(View.VISIBLE);
+ } else {
+ setupCamera();
+ }
+
+ setTopView(cameraView);
+ return root;
+ }
+
+ private void setupCamera() {
+ CameraManager.get().setListener(this);
+ preview.setShown();
+ CameraManager.get().setRenderOverlay(focus);
+ CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
+ setCameraUri(null);
+ }
+
+ @Override
+ public void onCameraError(int errorCode, Exception exception) {
+ LogUtil.e("CameraComposerFragment.onCameraError", "errorCode: ", errorCode, exception);
+ }
+
+ @Override
+ public void onCameraChanged() {
+ updateViewState();
+ }
+
+ @Override
+ public boolean shouldHide() {
+ return !processingUri && cameraUri == null;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == capture) {
+ float heightPercent = 1;
+ if (!getListener().isFullscreen()) {
+ heightPercent = Math.min((float) cameraView.getHeight() / preview.getView().getHeight(), 1);
+ }
+
+ showShutterEffect(shutter);
+ processingUri = true;
+ setCameraUri(null);
+ focus.getPieRenderer().clear();
+ CameraManager.get().takePicture(heightPercent, this);
+ } else if (view == swapCamera) {
+ ((Animatable) swapCamera.getDrawable()).start();
+ CameraManager.get().swapCamera();
+ } else if (view == cancel) {
+ processingUri = false;
+ setCameraUri(null);
+ } else if (view == exitFullscreen) {
+ getListener().showFullscreen(false);
+ fullscreen.setVisibility(View.VISIBLE);
+ exitFullscreen.setVisibility(View.GONE);
+ } else if (view == fullscreen) {
+ getListener().showFullscreen(true);
+ fullscreen.setVisibility(View.GONE);
+ exitFullscreen.setVisibility(View.VISIBLE);
+ } else if (view == allowPermission) {
+ // Checks to see if the user has permanently denied this permission. If this is the first
+ // time seeing this permission or they only pressed deny previously, they will see the
+ // permission request. If they permanently denied the permission, they will be sent to Dialer
+ // settings in order enable the permission.
+ if (PermissionsUtil.isFirstRequest(getContext(), permissions[0])
+ || shouldShowRequestPermissionRationale(permissions[0])) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_REQUESTED);
+ LogUtil.i("CameraComposerFragment.onClick", "Camera permission requested.");
+ requestPermissions(permissions, CAMERA_PERMISSION);
+ } else {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_SETTINGS);
+ LogUtil.i("CameraComposerFragment.onClick", "Settings opened to enable permission.");
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setData(Uri.parse("package:" + getContext().getPackageName()));
+ startActivity(intent);
+ }
+ }
+ }
+
+ /**
+ * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image is
+ * finished being cropped and stored on the device.
+ */
+ @Override
+ public void onMediaReady(Uri uri, String contentType, int width, int height) {
+ if (processingUri) {
+ processingUri = false;
+ setCameraUri(uri);
+ // If the user needed the URI before it was ready, uriCallback will be set and we should
+ // send the URI to them ASAP.
+ if (uriCallback != null) {
+ uriCallback.uriReady(uri);
+ uriCallback = null;
+ }
+ } else {
+ updateViewState();
+ }
+ }
+
+ /**
+ * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image failed
+ * to crop or be stored on the device.
+ */
+ @Override
+ public void onMediaFailed(Exception exception) {
+ LogUtil.e("CallComposerFragment.onMediaFailed", null, exception);
+ Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show();
+ setCameraUri(null);
+ processingUri = false;
+ if (uriCallback != null) {
+ loading.setVisibility(View.GONE);
+ uriCallback = null;
+ }
+ }
+
+ /**
+ * Usually called by {@link CameraManager} if the user does something to interrupt the picture
+ * while it's being taken (like switching the camera).
+ */
+ @Override
+ public void onMediaInfo(int what) {
+ if (what == MediaCallback.MEDIA_NO_DATA) {
+ Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show();
+ }
+ setCameraUri(null);
+ processingUri = false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ CameraManager.get().setListener(null);
+ }
+
+ private void showShutterEffect(final View shutterVisual) {
+ float maxAlpha = .7f;
+ int animationDurationMillis = 100;
+
+ AnimationSet animation = new AnimationSet(false /* shareInterpolator */);
+ Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha);
+ alphaInAnimation.setDuration(animationDurationMillis);
+ animation.addAnimation(alphaInAnimation);
+
+ Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f);
+ alphaOutAnimation.setStartOffset(animationDurationMillis);
+ alphaOutAnimation.setDuration(animationDurationMillis);
+ animation.addAnimation(alphaOutAnimation);
+
+ animation.setAnimationListener(
+ new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ shutterVisual.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ shutterVisual.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ shutterVisual.startAnimation(animation);
+ }
+
+ @NonNull
+ public String getMimeType() {
+ return "image/jpeg";
+ }
+
+ private void setCameraUri(Uri uri) {
+ cameraUri = uri;
+ // It's possible that if the user takes a picture and press back very quickly, the activity will
+ // no longer be alive and when the image cropping process completes, so we need to check that
+ // activity is still alive before trying to invoke it.
+ if (getListener() != null) {
+ updateViewState();
+ getListener().composeCall(this);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (PermissionsUtil.hasCameraPermissions(getContext())) {
+ permissionView.setVisibility(View.GONE);
+ setupCamera();
+ }
+ }
+
+ /** Updates the state of the buttons and overlays based on the current state of the view */
+ private void updateViewState() {
+ Assert.isNotNull(cameraView);
+ Assert.isNotNull(getContext());
+
+ boolean isCameraAvailable = CameraManager.get().isCameraAvailable();
+ boolean uriReadyOrProcessing = cameraUri != null || processingUri;
+
+ if (cameraUri == null && isCameraAvailable) {
+ CameraManager.get().resetPreview();
+ cancel.setVisibility(View.GONE);
+ }
+
+ if (!CameraManager.get().hasFrontAndBackCamera()) {
+ swapCamera.setVisibility(View.GONE);
+ } else {
+ swapCamera.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
+ }
+
+ capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
+ cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE);
+
+ if (uriReadyOrProcessing) {
+ fullscreen.setVisibility(View.GONE);
+ exitFullscreen.setVisibility(View.GONE);
+ } else if (getListener().isFullscreen()) {
+ exitFullscreen.setVisibility(View.VISIBLE);
+ fullscreen.setVisibility(View.GONE);
+ } else {
+ exitFullscreen.setVisibility(View.GONE);
+ fullscreen.setVisibility(View.VISIBLE);
+ }
+
+ swapCamera.setEnabled(isCameraAvailable);
+ capture.setEnabled(isCameraAvailable);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
+ PermissionsUtil.permissionRequested(getContext(), permissions[0]);
+ }
+ if (requestCode == CAMERA_PERMISSION
+ && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_GRANTED);
+ LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission granted.");
+ permissionView.setVisibility(View.GONE);
+ setupCamera();
+ } else if (requestCode == CAMERA_PERMISSION) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DENIED);
+ LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission denied.");
+ }
+ }
+
+ public void getCameraUriWhenReady(CameraUriCallback callback) {
+ if (processingUri) {
+ loading.setVisibility(View.VISIBLE);
+ uriCallback = callback;
+ } else {
+ callback.uriReady(cameraUri);
+ }
+ }
+
+ /** Callback to let the caller know when the URI is ready. */
+ public interface CameraUriCallback {
+ void uriReady(Uri uri);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
new file mode 100644
index 0000000..6231279
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
@@ -0,0 +1,256 @@
+/*
+ * 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.callcomposer;
+
+import static android.app.Activity.RESULT_OK;
+
+import android.Manifest.permission;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask.Callback;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.PermissionsUtil;
+import java.io.File;
+
+/** Fragment used to compose call with image from the user's gallery. */
+public class GalleryComposerFragment extends CallComposerFragment
+ implements LoaderCallbacks<Cursor>, OnClickListener {
+
+ private static final int RESULT_LOAD_IMAGE = 1;
+ private static final int RESULT_OPEN_SETTINGS = 2;
+
+ private GalleryGridAdapter adapter;
+ private GridView galleryGridView;
+ private View permissionView;
+ private View allowPermission;
+
+ private String[] permissions = new String[] {permission.READ_EXTERNAL_STORAGE};
+ private CursorLoader cursorLoader;
+ private GalleryGridItemData selectedData = null;
+ private boolean selectedDataIsCopy;
+
+ public static GalleryComposerFragment newInstance() {
+ return new GalleryComposerFragment();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) {
+ View view = inflater.inflate(R.layout.fragment_gallery_composer, container, false);
+ galleryGridView = (GridView) view.findViewById(R.id.gallery_grid_view);
+ permissionView = view.findViewById(R.id.permission_view);
+
+ if (!PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DISPLAYED);
+ LogUtil.i("GalleryComposerFragment.onCreateView", "Permission view shown.");
+ ImageView permissionImage = (ImageView) permissionView.findViewById(R.id.permission_icon);
+ TextView permissionText = (TextView) permissionView.findViewById(R.id.permission_text);
+ allowPermission = permissionView.findViewById(R.id.allow);
+
+ allowPermission.setOnClickListener(this);
+ permissionText.setText(R.string.gallery_permission_text);
+ permissionImage.setImageResource(R.drawable.quantum_ic_photo_white_48);
+ permissionImage.setColorFilter(
+ ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
+ permissionView.setVisibility(View.VISIBLE);
+ } else {
+ setupGallery();
+ }
+
+ setTopView(galleryGridView);
+ return view;
+ }
+
+ private void setupGallery() {
+ adapter = new GalleryGridAdapter(getContext(), null, this);
+ galleryGridView.setAdapter(adapter);
+ getLoaderManager().initLoader(0 /* id */, null /* args */, this /* loaderCallbacks */);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ return cursorLoader = new GalleryCursorLoader(getContext());
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ adapter.swapCursor(cursor);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ adapter.swapCursor(null);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == allowPermission) {
+ // Checks to see if the user has permanently denied this permission. If this is their first
+ // time seeing this permission or they've only pressed deny previously, they will see the
+ // permission request. If they've permanently denied the permission, they will be sent to
+ // Dialer settings in order to enable the permission.
+ if (PermissionsUtil.isFirstRequest(getContext(), permissions[0])
+ || shouldShowRequestPermissionRationale(permissions[0])) {
+ LogUtil.i("GalleryComposerFragment.onClick", "Storage permission requested.");
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_REQUESTED);
+ requestPermissions(permissions, STORAGE_PERMISSION);
+ } else {
+ LogUtil.i("GalleryComposerFragment.onClick", "Settings opened to enable permission.");
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_SETTINGS);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.parse("package:" + getContext().getPackageName()));
+ startActivityForResult(intent, RESULT_OPEN_SETTINGS);
+ }
+ return;
+ } else {
+ GalleryGridItemView itemView = ((GalleryGridItemView) view);
+ if (itemView.isGallery()) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, GalleryCursorLoader.ACCEPTABLE_IMAGE_TYPES);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ startActivityForResult(intent, RESULT_LOAD_IMAGE);
+ } else if (itemView.getData().equals(selectedData)) {
+ setSelected(null, false);
+ } else {
+ setSelected(new GalleryGridItemData(itemView.getData()), false);
+ }
+ }
+ }
+
+ @Nullable
+ public GalleryGridItemData getGalleryData() {
+ return selectedData;
+ }
+
+ public GridView getGalleryGridView() {
+ return galleryGridView;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && data != null) {
+ prepareDataForAttachment(data);
+ } else if (requestCode == RESULT_OPEN_SETTINGS
+ && PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) {
+ permissionView.setVisibility(View.GONE);
+ setupGallery();
+ }
+ }
+
+ private void setSelected(GalleryGridItemData data, boolean isCopy) {
+ selectedData = data;
+ selectedDataIsCopy = isCopy;
+ adapter.setSelected(selectedData);
+ getListener().composeCall(this);
+ }
+
+ @Override
+ public boolean shouldHide() {
+ return selectedData == null
+ || selectedData.getFilePath() == null
+ || selectedData.getMimeType() == null;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
+ PermissionsUtil.permissionRequested(getContext(), permissions[0]);
+ }
+ if (requestCode == STORAGE_PERMISSION
+ && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_GRANTED);
+ LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission granted.");
+ permissionView.setVisibility(View.GONE);
+ setupGallery();
+ } else if (requestCode == STORAGE_PERMISSION) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DENIED);
+ LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission denied.");
+ }
+ }
+
+ public CursorLoader getCursorLoader() {
+ return cursorLoader;
+ }
+
+ public boolean selectedDataIsCopy() {
+ return selectedDataIsCopy;
+ }
+
+ private void prepareDataForAttachment(Intent data) {
+ // We're using the builtin photo picker which supplies the return url as it's "data".
+ String url = data.getDataString();
+ if (url == null) {
+ final Bundle extras = data.getExtras();
+ if (extras != null) {
+ final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ url = uri.toString();
+ }
+ }
+ }
+
+ // This should never happen, but just in case..
+ // Guard against null uri cases for when the activity returns a null/invalid intent.
+ if (url != null) {
+ new CopyAndResizeImageTask(
+ getContext(),
+ Uri.parse(url),
+ new Callback() {
+ @Override
+ public void onCopySuccessful(File file, String mimeType) {
+ setSelected(adapter.insertEntry(file.getAbsolutePath(), mimeType), true);
+ }
+
+ @Override
+ public void onCopyFailed(Throwable throwable) {
+ // TODO(b/33753902)
+ LogUtil.e(
+ "GalleryComposerFragment.onFailure", "Data preparation failed", throwable);
+ }
+ })
+ .execute();
+ } else {
+ // TODO(b/33753902)
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryCursorLoader.java b/java/com/android/dialer/callcomposer/GalleryCursorLoader.java
new file mode 100644
index 0000000..f9990e1
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryCursorLoader.java
@@ -0,0 +1,54 @@
+/*
+ * 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.callcomposer;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.MediaStore.Files;
+import android.provider.MediaStore.Files.FileColumns;
+import android.provider.MediaStore.Images.Media;
+import android.support.v4.content.CursorLoader;
+
+/** A BoundCursorLoader that reads local media on the device. */
+public class GalleryCursorLoader extends CursorLoader {
+ public static final String MEDIA_SCANNER_VOLUME_EXTERNAL = "external";
+ public static final String[] ACCEPTABLE_IMAGE_TYPES =
+ new String[] {"image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"};
+
+ private static final Uri STORAGE_URI = Files.getContentUri(MEDIA_SCANNER_VOLUME_EXTERNAL);
+ private static final String SORT_ORDER = Media.DATE_MODIFIED + " DESC";
+ private static final String IMAGE_SELECTION = createSelection();
+
+ public GalleryCursorLoader(Context context) {
+ super(
+ context,
+ STORAGE_URI,
+ GalleryGridItemData.IMAGE_PROJECTION,
+ IMAGE_SELECTION,
+ null,
+ SORT_ORDER);
+ }
+
+ @SuppressLint("DefaultLocale")
+ private static String createSelection() {
+ return String.format(
+ "mime_type IN ('image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp')"
+ + " AND media_type in (%d)",
+ FileColumns.MEDIA_TYPE_IMAGE);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
new file mode 100644
index 0000000..0a7fd76
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryGridAdapter.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.callcomposer;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Bridges between the image cursor loaded by GalleryBoundCursorLoader and the GalleryGridView. */
+public class GalleryGridAdapter extends CursorAdapter {
+
+ @NonNull private final OnClickListener onClickListener;
+ @NonNull private final List<GalleryGridItemView> views = new ArrayList<>();
+ @NonNull private final Context context;
+
+ private GalleryGridItemData selectedData;
+
+ public GalleryGridAdapter(
+ @NonNull Context context, Cursor cursor, @NonNull OnClickListener onClickListener) {
+ super(context, cursor, 0);
+ this.onClickListener = Assert.isNotNull(onClickListener);
+ this.context = Assert.isNotNull(context);
+ }
+
+ @Override
+ public int getCount() {
+ // Add one for the header.
+ return super.getCount() + 1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // At position 0, we want to insert a header. If position == 0, we don't need the cursor.
+ // If position != 0, then we need to move the cursor to position - 1 to account for the offset
+ // of the header.
+ if (position != 0 && !getCursor().moveToPosition(position - 1)) {
+ Assert.fail("couldn't move cursor to position " + (position - 1));
+ }
+ View view;
+ if (convertView == null) {
+ view = newView(context, getCursor(), parent);
+ } else {
+ view = convertView;
+ }
+ bindView(view, context, getCursor(), position);
+ return view;
+ }
+
+ private void bindView(View view, Context context, Cursor cursor, int position) {
+ if (position == 0) {
+ GalleryGridItemView gridView = (GalleryGridItemView) view;
+ gridView.showGallery(true);
+ } else {
+ bindView(view, context, cursor);
+ }
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ GalleryGridItemView gridView = (GalleryGridItemView) view;
+ gridView.bind(cursor);
+ gridView.setSelected(gridView.getData().equals(selectedData));
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ GalleryGridItemView view =
+ (GalleryGridItemView)
+ LayoutInflater.from(context).inflate(R.layout.gallery_grid_item_view, parent, false);
+ view.setOnClickListener(onClickListener);
+ views.add(view);
+ return view;
+ }
+
+ public void setSelected(GalleryGridItemData selectedData) {
+ this.selectedData = selectedData;
+ for (GalleryGridItemView view : views) {
+ view.setSelected(view.getData().equals(selectedData));
+ }
+ }
+
+ public GalleryGridItemData insertEntry(String filePath, String mimeType) {
+ LogUtil.i("GalleryGridAdapter.insertRow", mimeType + " " + filePath);
+
+ MatrixCursor extraRow = new MatrixCursor(GalleryGridItemData.IMAGE_PROJECTION);
+ extraRow.addRow(new Object[] {0L, filePath, mimeType, ""});
+ extraRow.moveToFirst();
+ Cursor extendedCursor = new MergeCursor(new Cursor[] {extraRow, getCursor()});
+ swapCursor(extendedCursor);
+
+ return new GalleryGridItemData(extraRow);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryGridItemData.java b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
new file mode 100644
index 0000000..402c6ce
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
@@ -0,0 +1,91 @@
+/*
+ * 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.callcomposer;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Images.Media;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import java.io.File;
+import java.util.Objects;
+
+/** Provides data for GalleryGridItemView */
+public final class GalleryGridItemData {
+ public static final String[] IMAGE_PROJECTION =
+ new String[] {Media._ID, Media.DATA, Media.MIME_TYPE, Media.DATE_MODIFIED};
+
+ private static final int INDEX_DATA_PATH = 1;
+ private static final int INDEX_MIME_TYPE = 2;
+ private static final int INDEX_DATE_MODIFIED = 3;
+
+ private String filePath;
+ private String mimeType;
+ private long dateModifiedSeconds;
+
+ public GalleryGridItemData() {}
+
+ public GalleryGridItemData(GalleryGridItemData copyData) {
+ filePath = Assert.isNotNull(copyData.getFilePath());
+ mimeType = Assert.isNotNull(copyData.getMimeType());
+ dateModifiedSeconds = Assert.isNotNull(copyData.getDateModifiedSeconds());
+ }
+
+ public GalleryGridItemData(Cursor cursor) {
+ bind(cursor);
+ }
+
+ public void bind(Cursor cursor) {
+ mimeType = Assert.isNotNull(cursor.getString(INDEX_MIME_TYPE));
+ String dateModified = Assert.isNotNull(cursor.getString(INDEX_DATE_MODIFIED));
+ dateModifiedSeconds = !TextUtils.isEmpty(dateModified) ? Long.parseLong(dateModified) : -1;
+ filePath = Assert.isNotNull(cursor.getString(INDEX_DATA_PATH));
+ }
+
+ @Nullable
+ public String getFilePath() {
+ return filePath;
+ }
+
+ @Nullable
+ public Uri getFileUri() {
+ return TextUtils.isEmpty(filePath) ? null : Uri.fromFile(new File(filePath));
+ }
+
+ /** @return The date in seconds. This can be negative if we could not retrieve date info */
+ public long getDateModifiedSeconds() {
+ return dateModifiedSeconds;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof GalleryGridItemData
+ && Objects.equals(mimeType, ((GalleryGridItemData) obj).mimeType)
+ && Objects.equals(filePath, ((GalleryGridItemData) obj).filePath)
+ && ((GalleryGridItemData) obj).dateModifiedSeconds == dateModifiedSeconds;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(filePath, mimeType, dateModifiedSeconds);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryGridItemView.java b/java/com/android/dialer/callcomposer/GalleryGridItemView.java
new file mode 100644
index 0000000..d70fd57
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryGridItemView.java
@@ -0,0 +1,126 @@
+/*
+ * 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.callcomposer;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
+import com.bumptech.glide.request.RequestOptions;
+import java.util.concurrent.TimeUnit;
+
+/** Shows an item in the gallery picker grid view. Hosts an FileImageView with a checkbox. */
+public class GalleryGridItemView extends FrameLayout {
+
+ private final GalleryGridItemData data = new GalleryGridItemData();
+
+ private ImageView image;
+ private View checkbox;
+ private View gallery;
+ private String currentFilePath;
+ private boolean isGallery;
+
+ public GalleryGridItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ image = (ImageView) findViewById(R.id.image);
+ checkbox = findViewById(R.id.checkbox);
+ gallery = findViewById(R.id.gallery);
+
+ image.setClipToOutline(true);
+ checkbox.setClipToOutline(true);
+ gallery.setClipToOutline(true);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // The grid view auto-fit the columns, so we want to let the height match the width
+ // to make the image square.
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+ public GalleryGridItemData getData() {
+ return data;
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ if (selected) {
+ checkbox.setVisibility(VISIBLE);
+ int paddingPx = getResources().getDimensionPixelSize(R.dimen.gallery_item_selected_padding);
+ setPadding(paddingPx, paddingPx, paddingPx, paddingPx);
+ } else {
+ checkbox.setVisibility(GONE);
+ int paddingPx = getResources().getDimensionPixelOffset(R.dimen.gallery_item_padding);
+ setPadding(paddingPx, paddingPx, paddingPx, paddingPx);
+ }
+ }
+
+ public boolean isGallery() {
+ return isGallery;
+ }
+
+ public void showGallery(boolean show) {
+ isGallery = show;
+ gallery.setVisibility(show ? VISIBLE : INVISIBLE);
+ }
+
+ public void bind(Cursor cursor) {
+ data.bind(cursor);
+ showGallery(false);
+ updateImageView();
+ }
+
+ private void updateImageView() {
+ image.setScaleType(ScaleType.CENTER_CROP);
+
+ if (currentFilePath == null || !currentFilePath.equals(data.getFilePath())) {
+ currentFilePath = data.getFilePath();
+
+ // Downloads/loads an image from the given URI so that the image's largest dimension is
+ // between 1/2 the given dimensions and the given dimensions, with no restrictions on the
+ // image's smallest dimension. We skip the memory cache, but glide still applies it's disk
+ // cache to optimize loads.
+ Glide.with(getContext())
+ .load(data.getFileUri())
+ .apply(RequestOptions.downsampleOf(DownsampleStrategy.AT_MOST).skipMemoryCache(true))
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .into(image);
+ }
+ long dateModifiedSeconds = data.getDateModifiedSeconds();
+ if (dateModifiedSeconds > 0) {
+ image.setContentDescription(
+ getResources()
+ .getString(
+ R.string.gallery_item_description,
+ TimeUnit.SECONDS.toMillis(dateModifiedSeconds)));
+ } else {
+ image.setContentDescription(
+ getResources().getString(R.string.gallery_item_description_no_date));
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/MessageComposerFragment.java b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
new file mode 100644
index 0000000..521b714
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
@@ -0,0 +1,143 @@
+/*
+ * 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.callcomposer;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/** Fragment used to compose call with message fragment. */
+public class MessageComposerFragment extends CallComposerFragment
+ implements OnClickListener, TextWatcher, OnTouchListener, OnLongClickListener {
+ private static final String CHAR_LIMIT_KEY = "char_limit";
+
+ public static final int NO_CHAR_LIMIT = -1;
+
+ private EditText customMessage;
+ private boolean isLongClick = false;
+ private int charLimit;
+
+ public static MessageComposerFragment newInstance(int charLimit) {
+ MessageComposerFragment fragment = new MessageComposerFragment();
+ Bundle args = new Bundle();
+ args.putInt(CHAR_LIMIT_KEY, charLimit);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ public String getMessage() {
+ return customMessage == null ? null : customMessage.getText().toString();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ charLimit = getArguments().getInt(CHAR_LIMIT_KEY, NO_CHAR_LIMIT);
+
+ View view = inflater.inflate(R.layout.fragment_message_composer, container, false);
+ TextView urgent = (TextView) view.findViewById(R.id.message_urgent);
+ customMessage = (EditText) view.findViewById(R.id.custom_message);
+
+ urgent.setOnClickListener(this);
+ customMessage.setOnTouchListener(this);
+ customMessage.setOnLongClickListener(this);
+ customMessage.addTextChangedListener(this);
+ if (charLimit != NO_CHAR_LIMIT) {
+ TextView remainingChar = (TextView) view.findViewById(R.id.remaining_characters);
+ remainingChar.setText("" + charLimit);
+ customMessage.setFilters(new InputFilter[] {new InputFilter.LengthFilter(charLimit)});
+ customMessage.addTextChangedListener(
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ remainingChar.setText("" + (charLimit - editable.length()));
+ }
+ });
+ }
+ view.findViewById(R.id.message_chat).setOnClickListener(this);
+ view.findViewById(R.id.message_question).setOnClickListener(this);
+
+ setTopView(urgent);
+ return view;
+ }
+
+ @Override
+ public void onClick(View view) {
+ customMessage.setText(((TextView) view).getText());
+ customMessage.setSelection(customMessage.getText().length());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ getListener().composeCall(this);
+ }
+
+ /**
+ * EditTexts take two clicks to dispatch an onClick() event, so instead we add an onTouchListener
+ * to listen for them. The caveat to this is that it also requires listening for onLongClicks to
+ * distinguish whether a MotionEvent came from a click or a long click.
+ */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ if (isLongClick) {
+ isLongClick = false;
+ } else {
+ getListener().showFullscreen(true);
+ }
+ }
+ view.performClick();
+ return false;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ isLongClick = true;
+ return false;
+ }
+
+ @Override
+ public boolean shouldHide() {
+ return TextUtils.isEmpty(getMessage());
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml b/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml
new file mode 100644
index 0000000..82f1412
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ 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 package="com.android.dialer.callcomposer.camera"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/CameraManager.java b/java/com/android/dialer/callcomposer/camera/CameraManager.java
new file mode 100644
index 0000000..87cd16a
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/CameraManager.java
@@ -0,0 +1,822 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import com.android.dialer.callcomposer.camera.camerafocus.FocusOverlayManager;
+import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Class which manages interactions with the camera, but does not do any UI. This class is designed
+ * to be a singleton to ensure there is one component managing the camera and releasing the native
+ * resources. In order to acquire a camera, a caller must:
+ *
+ * <ul>
+ * <li>Call selectCamera to select front or back camera
+ * <li>Call setSurface to control where the preview is shown
+ * <li>Call openCamera to request the camera start preview
+ * </ul>
+ *
+ * Callers should call onPause and onResume to ensure that the camera is release while the activity
+ * is not active. This class is not thread safe. It should only be called from one thread (the UI
+ * thread or test thread)
+ */
+public class CameraManager implements FocusOverlayManager.Listener {
+ /** Callbacks for the camera manager listener */
+ public interface CameraManagerListener {
+ void onCameraError(int errorCode, Exception e);
+
+ void onCameraChanged();
+ }
+
+ /** Callback when taking image or video */
+ public interface MediaCallback {
+ int MEDIA_CAMERA_CHANGED = 1;
+ int MEDIA_NO_DATA = 2;
+
+ void onMediaReady(Uri uriToMedia, String contentType, int width, int height);
+
+ void onMediaFailed(Exception exception);
+
+ void onMediaInfo(int what);
+ }
+
+ // Error codes
+ private static final int ERROR_OPENING_CAMERA = 1;
+ private static final int ERROR_SHOWING_PREVIEW = 2;
+ private static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 3;
+ private static final int ERROR_TAKING_PICTURE = 4;
+
+ private static final int NO_CAMERA_SELECTED = -1;
+
+ private static final Camera.ShutterCallback DUMMY_SHUTTER_CALLBACK =
+ new Camera.ShutterCallback() {
+ @Override
+ public void onShutter() {
+ // Do nothing
+ }
+ };
+
+ private static CameraManager sInstance;
+
+ /** The CameraInfo for the currently selected camera */
+ private final CameraInfo mCameraInfo;
+
+ /** The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet */
+ private int mCameraIndex;
+
+ /** True if the device has front and back cameras */
+ private final boolean mHasFrontAndBackCamera;
+
+ /** True if the camera should be open (may not yet be actually open) */
+ private boolean mOpenRequested;
+
+ /** The preview view to show the preview on */
+ private CameraPreview mCameraPreview;
+
+ /** The helper classs to handle orientation changes */
+ private OrientationHandler mOrientationHandler;
+
+ /** Tracks whether the preview has hardware acceleration */
+ private boolean mIsHardwareAccelerationSupported;
+
+ /**
+ * The task for opening the camera, so it doesn't block the UI thread Using AsyncTask rather than
+ * SafeAsyncTask because the tasks need to be serialized, but don't need to be on the UI thread
+ * TODO: If we have other AyncTasks (not SafeAsyncTasks) this may contend and we may need
+ * to create a dedicated thread, or synchronize the threads in the thread pool
+ */
+ private AsyncTask<Integer, Void, Camera> mOpenCameraTask;
+
+ /**
+ * The camera index that is queued to be opened, but not completed yet, or NO_CAMERA_SELECTED if
+ * no open task is pending
+ */
+ private int mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+
+ /** The instance of the currently opened camera */
+ private Camera mCamera;
+
+ /** The rotation of the screen relative to the camera's natural orientation */
+ private int mRotation;
+
+ /** The callback to notify when errors or other events occur */
+ private CameraManagerListener mListener;
+
+ /** True if the camera is currently in the process of taking an image */
+ private boolean mTakingPicture;
+
+ /** Manages auto focus visual and behavior */
+ private final FocusOverlayManager mFocusOverlayManager;
+
+ private CameraManager() {
+ mCameraInfo = new CameraInfo();
+ mCameraIndex = NO_CAMERA_SELECTED;
+
+ // Check to see if a front and back camera exist
+ boolean hasFrontCamera = false;
+ boolean hasBackCamera = false;
+ final CameraInfo cameraInfo = new CameraInfo();
+ final int cameraCount = Camera.getNumberOfCameras();
+ try {
+ for (int i = 0; i < cameraCount; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
+ hasFrontCamera = true;
+ } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
+ hasBackCamera = true;
+ }
+ if (hasFrontCamera && hasBackCamera) {
+ break;
+ }
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.CameraManager", "Unable to load camera info", e);
+ }
+ mHasFrontAndBackCamera = hasFrontCamera && hasBackCamera;
+ mFocusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper());
+
+ // Assume the best until we are proven otherwise
+ mIsHardwareAccelerationSupported = true;
+ }
+
+ /** Gets the singleton instance */
+ public static CameraManager get() {
+ if (sInstance == null) {
+ sInstance = new CameraManager();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Sets the surface to use to display the preview This must only be called AFTER the CameraPreview
+ * has a texture ready
+ *
+ * @param preview The preview surface view
+ */
+ void setSurface(final CameraPreview preview) {
+ if (preview == mCameraPreview) {
+ return;
+ }
+
+ if (preview != null) {
+ Assert.checkArgument(preview.isValid());
+ preview.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(final View view, final MotionEvent motionEvent) {
+ if ((motionEvent.getActionMasked() & MotionEvent.ACTION_UP)
+ == MotionEvent.ACTION_UP) {
+ mFocusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight());
+ mFocusOverlayManager.onSingleTapUp(
+ (int) motionEvent.getX() + view.getLeft(),
+ (int) motionEvent.getY() + view.getTop());
+ }
+ view.performClick();
+ return true;
+ }
+ });
+ }
+ mCameraPreview = preview;
+ tryShowPreview();
+ }
+
+ public void setRenderOverlay(final RenderOverlay renderOverlay) {
+ mFocusOverlayManager.setFocusRenderer(
+ renderOverlay != null ? renderOverlay.getPieRenderer() : null);
+ }
+
+ /** Convenience function to swap between front and back facing cameras */
+ public void swapCamera() {
+ Assert.checkState(mCameraIndex >= 0);
+ selectCamera(
+ mCameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT
+ ? CameraInfo.CAMERA_FACING_BACK
+ : CameraInfo.CAMERA_FACING_FRONT);
+ }
+
+ /**
+ * Selects the first camera facing the desired direction, or the first camera if there is no
+ * camera in the desired direction
+ *
+ * @param desiredFacing One of the CameraInfo.CAMERA_FACING_* constants
+ * @return True if a camera was selected, or false if selecting a camera failed
+ */
+ public boolean selectCamera(final int desiredFacing) {
+ try {
+ // We already selected a camera facing that direction
+ if (mCameraIndex >= 0 && mCameraInfo.facing == desiredFacing) {
+ return true;
+ }
+
+ final int cameraCount = Camera.getNumberOfCameras();
+ Assert.checkState(cameraCount > 0);
+
+ mCameraIndex = NO_CAMERA_SELECTED;
+ setCamera(null);
+ final CameraInfo cameraInfo = new CameraInfo();
+ for (int i = 0; i < cameraCount; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == desiredFacing) {
+ mCameraIndex = i;
+ Camera.getCameraInfo(i, mCameraInfo);
+ break;
+ }
+ }
+
+ // There's no camera in the desired facing direction, just select the first camera
+ // regardless of direction
+ if (mCameraIndex < 0) {
+ mCameraIndex = 0;
+ Camera.getCameraInfo(0, mCameraInfo);
+ }
+
+ if (mOpenRequested) {
+ // The camera is open, so reopen with the newly selected camera
+ openCamera();
+ }
+ return true;
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.selectCamera", "RuntimeException in CameraManager.selectCamera", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ return false;
+ }
+ }
+
+ public int getCameraIndex() {
+ return mCameraIndex;
+ }
+
+ public void selectCameraByIndex(final int cameraIndex) {
+ if (mCameraIndex == cameraIndex) {
+ return;
+ }
+
+ try {
+ mCameraIndex = cameraIndex;
+ Camera.getCameraInfo(mCameraIndex, mCameraInfo);
+ if (mOpenRequested) {
+ openCamera();
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.selectCameraByIndex",
+ "RuntimeException in CameraManager.selectCameraByIndex",
+ e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public CameraInfo getCameraInfo() {
+ if (mCameraIndex == NO_CAMERA_SELECTED) {
+ return null;
+ }
+ return mCameraInfo;
+ }
+
+ /** @return True if the device has both a front and back camera */
+ public boolean hasFrontAndBackCamera() {
+ return mHasFrontAndBackCamera;
+ }
+
+ /** Opens the camera on a separate thread and initiates the preview if one is available */
+ void openCamera() {
+ if (mCameraIndex == NO_CAMERA_SELECTED) {
+ // Ensure a selected camera if none is currently selected. This may happen if the
+ // camera chooser is not the default media chooser.
+ selectCamera(CameraInfo.CAMERA_FACING_BACK);
+ }
+ mOpenRequested = true;
+ // We're already opening the camera or already have the camera handle, nothing more to do
+ if (mPendingOpenCameraIndex == mCameraIndex || mCamera != null) {
+ return;
+ }
+
+ // True if the task to open the camera has to be delayed until the current one completes
+ boolean delayTask = false;
+
+ // Cancel any previous open camera tasks
+ if (mOpenCameraTask != null) {
+ mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+ delayTask = true;
+ }
+
+ mPendingOpenCameraIndex = mCameraIndex;
+ mOpenCameraTask =
+ new AsyncTask<Integer, Void, Camera>() {
+ private Exception mException;
+
+ @Override
+ protected Camera doInBackground(final Integer... params) {
+ try {
+ final int cameraIndex = params[0];
+ LogUtil.v("CameraManager.doInBackground", "Opening camera " + mCameraIndex);
+ return Camera.open(cameraIndex);
+ } catch (final Exception e) {
+ LogUtil.e("CameraManager.doInBackground", "Exception while opening camera", e);
+ mException = e;
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(final Camera camera) {
+ // If we completed, but no longer want this camera, then release the camera
+ if (mOpenCameraTask != this || !mOpenRequested) {
+ releaseCamera(camera);
+ cleanup();
+ return;
+ }
+
+ cleanup();
+
+ LogUtil.v(
+ "CameraManager.onPostExecute",
+ "Opened camera " + mCameraIndex + " " + (camera != null));
+ setCamera(camera);
+ if (camera == null) {
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, mException);
+ }
+ LogUtil.e("CameraManager.onPostExecute", "Error opening camera");
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ cleanup();
+ }
+
+ private void cleanup() {
+ mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+ if (mOpenCameraTask != null && mOpenCameraTask.getStatus() == Status.PENDING) {
+ // If there's another task waiting on this one to complete, start it now
+ mOpenCameraTask.execute(mCameraIndex);
+ } else {
+ mOpenCameraTask = null;
+ }
+ }
+ };
+ LogUtil.v("CameraManager.openCamera", "Start opening camera " + mCameraIndex);
+ if (!delayTask) {
+ mOpenCameraTask.execute(mCameraIndex);
+ }
+ }
+
+ /** Closes the camera releasing the resources it uses */
+ void closeCamera() {
+ mOpenRequested = false;
+ setCamera(null);
+ }
+
+ /**
+ * Sets the listener which will be notified of errors or other events in the camera
+ *
+ * @param listener The listener to notify
+ */
+ public void setListener(final CameraManagerListener listener) {
+ Assert.isMainThread();
+ mListener = listener;
+ if (!mIsHardwareAccelerationSupported && mListener != null) {
+ mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null);
+ }
+ }
+
+ public void takePicture(final float heightPercent, @NonNull final MediaCallback callback) {
+ Assert.checkState(!mTakingPicture);
+ Assert.isNotNull(callback);
+ mCameraPreview.setFocusable(false);
+ mFocusOverlayManager.cancelAutoFocus();
+ if (mCamera == null) {
+ // The caller should have checked isCameraAvailable first, but just in case, protect
+ // against a null camera by notifying the callback that taking the picture didn't work
+ callback.onMediaFailed(null);
+ return;
+ }
+ final Camera.PictureCallback jpegCallback =
+ new Camera.PictureCallback() {
+ @Override
+ public void onPictureTaken(final byte[] bytes, final Camera camera) {
+ mTakingPicture = false;
+ if (mCamera != camera) {
+ // This may happen if the camera was changed between front/back while the
+ // picture is being taken.
+ callback.onMediaInfo(MediaCallback.MEDIA_CAMERA_CHANGED);
+ return;
+ }
+
+ if (bytes == null) {
+ callback.onMediaInfo(MediaCallback.MEDIA_NO_DATA);
+ return;
+ }
+
+ final Camera.Size size = camera.getParameters().getPictureSize();
+ int width;
+ int height;
+ if (mRotation == 90 || mRotation == 270) {
+ // Is rotated, so swapping dimensions is desired
+ //noinspection SuspiciousNameCombination
+ width = size.height;
+ //noinspection SuspiciousNameCombination
+ height = size.width;
+ } else {
+ width = size.width;
+ height = size.height;
+ }
+ LogUtil.i(
+ "CameraManager.onPictureTaken", "taken picture size: " + bytes.length + " bytes");
+ new ImagePersistTask(
+ width, height, heightPercent, bytes, mCameraPreview.getContext(), callback)
+ .execute();
+ }
+ };
+
+ mTakingPicture = true;
+ try {
+ mCamera.takePicture(
+ // A shutter callback is required to enable shutter sound
+ DUMMY_SHUTTER_CALLBACK, null /* raw */, null /* postView */, jpegCallback);
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.takePicture", "RuntimeException in CameraManager.takePicture", e);
+ mTakingPicture = false;
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_TAKING_PICTURE, e);
+ }
+ }
+ }
+
+ /**
+ * Asynchronously releases a camera
+ *
+ * @param camera The camera to release
+ */
+ private void releaseCamera(final Camera camera) {
+ if (camera == null) {
+ return;
+ }
+
+ mFocusOverlayManager.onCameraReleased();
+
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... params) {
+ LogUtil.v("CameraManager.doInBackground", "Releasing camera " + mCameraIndex);
+ camera.release();
+ return null;
+ }
+ }.execute();
+ }
+
+ /** Updates the orientation of the camera to match the orientation of the device */
+ private void updateCameraOrientation() {
+ if (mCamera == null || mCameraPreview == null || mTakingPicture) {
+ return;
+ }
+
+ final WindowManager windowManager =
+ (WindowManager) mCameraPreview.getContext().getSystemService(Context.WINDOW_SERVICE);
+
+ int degrees = 0;
+ switch (windowManager.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ }
+
+ // The display orientation of the camera (this controls the preview image).
+ int orientation;
+
+ // The clockwise rotation angle relative to the orientation of the camera. This affects
+ // pictures returned by the camera in Camera.PictureCallback.
+ int rotation;
+ if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ orientation = (mCameraInfo.orientation + degrees) % 360;
+ rotation = orientation;
+ // compensate the mirror but only for orientation
+ orientation = (360 - orientation) % 360;
+ } else { // back-facing
+ orientation = (mCameraInfo.orientation - degrees + 360) % 360;
+ rotation = orientation;
+ }
+ mRotation = rotation;
+ try {
+ mCamera.setDisplayOrientation(orientation);
+ final Camera.Parameters params = mCamera.getParameters();
+ params.setRotation(rotation);
+ mCamera.setParameters(params);
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.updateCameraOrientation",
+ "RuntimeException in CameraManager.updateCameraOrientation",
+ e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ }
+ }
+
+ /** Sets the current camera, releasing any previously opened camera */
+ private void setCamera(final Camera camera) {
+ if (mCamera == camera) {
+ return;
+ }
+
+ releaseCamera(mCamera);
+ mCamera = camera;
+ tryShowPreview();
+ if (mListener != null) {
+ mListener.onCameraChanged();
+ }
+ }
+
+ /** Shows the preview if the camera is open and the preview is loaded */
+ private void tryShowPreview() {
+ if (mCameraPreview == null || mCamera == null) {
+ if (mOrientationHandler != null) {
+ mOrientationHandler.disable();
+ mOrientationHandler = null;
+ }
+ // releaseMediaRecorder(true /* cleanupFile */);
+ mFocusOverlayManager.onPreviewStopped();
+ return;
+ }
+ try {
+ mCamera.stopPreview();
+ updateCameraOrientation();
+
+ final Camera.Parameters params = mCamera.getParameters();
+ final Camera.Size pictureSize = chooseBestPictureSize();
+ final Camera.Size previewSize = chooseBestPreviewSize(pictureSize);
+ params.setPreviewSize(previewSize.width, previewSize.height);
+ params.setPictureSize(pictureSize.width, pictureSize.height);
+ logCameraSize("Setting preview size: ", previewSize);
+ logCameraSize("Setting picture size: ", pictureSize);
+ mCameraPreview.setSize(previewSize, mCameraInfo.orientation);
+ for (final String focusMode : params.getSupportedFocusModes()) {
+ if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ // Use continuous focus if available
+ params.setFocusMode(focusMode);
+ break;
+ }
+ }
+
+ mCamera.setParameters(params);
+ mCameraPreview.startPreview(mCamera);
+ mCamera.startPreview();
+ mCamera.setAutoFocusMoveCallback(
+ new Camera.AutoFocusMoveCallback() {
+ @Override
+ public void onAutoFocusMoving(final boolean start, final Camera camera) {
+ mFocusOverlayManager.onAutoFocusMoving(start);
+ }
+ });
+ mFocusOverlayManager.setParameters(mCamera.getParameters());
+ mFocusOverlayManager.setMirror(mCameraInfo.facing == CameraInfo.CAMERA_FACING_BACK);
+ mFocusOverlayManager.onPreviewStarted();
+ if (mOrientationHandler == null) {
+ mOrientationHandler = new OrientationHandler(mCameraPreview.getContext());
+ mOrientationHandler.enable();
+ }
+ } catch (final IOException e) {
+ LogUtil.e("CameraManager.tryShowPreview", "IOException in CameraManager.tryShowPreview", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_SHOWING_PREVIEW, e);
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.tryShowPreview", "RuntimeException in CameraManager.tryShowPreview", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_SHOWING_PREVIEW, e);
+ }
+ }
+ }
+
+ public boolean isCameraAvailable() {
+ return mCamera != null && !mTakingPicture && mIsHardwareAccelerationSupported;
+ }
+
+ /**
+ * Choose the best picture size by trying to find a size close to the MmsConfig's max size, which
+ * is closest to the screen aspect ratio. In case of RCS conversation returns default size.
+ */
+ private Camera.Size chooseBestPictureSize() {
+ return mCamera.getParameters().getPictureSize();
+ }
+
+ /**
+ * Chose the best preview size based on the picture size. Try to find a size with the same aspect
+ * ratio and size as the picture if possible
+ */
+ private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) {
+ final List<Camera.Size> sizes =
+ new ArrayList<Camera.Size>(mCamera.getParameters().getSupportedPreviewSizes());
+ final float aspectRatio = pictureSize.width / (float) pictureSize.height;
+ final int capturePixels = pictureSize.width * pictureSize.height;
+
+ // Sort the sizes so the best size is first
+ Collections.sort(
+ sizes,
+ new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, aspectRatio, capturePixels));
+
+ return sizes.get(0);
+ }
+
+ private class OrientationHandler extends OrientationEventListener {
+ OrientationHandler(final Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onOrientationChanged(final int orientation) {
+ updateCameraOrientation();
+ }
+ }
+
+ private static class SizeComparator implements Comparator<Camera.Size> {
+ private static final int PREFER_LEFT = -1;
+ private static final int PREFER_RIGHT = 1;
+
+ // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit
+ private final int mMaxWidth;
+ private final int mMaxHeight;
+
+ // The desired aspect ratio
+ private final float mTargetAspectRatio;
+
+ // The desired size (width x height) to try to match
+ private final int mTargetPixels;
+
+ public SizeComparator(
+ final int maxWidth,
+ final int maxHeight,
+ final float targetAspectRatio,
+ final int targetPixels) {
+ mMaxWidth = maxWidth;
+ mMaxHeight = maxHeight;
+ mTargetAspectRatio = targetAspectRatio;
+ mTargetPixels = targetPixels;
+ }
+
+ /**
+ * Returns a negative value if left is a better choice than right, or a positive value if right
+ * is a better choice is better than left. 0 if they are equal
+ */
+ @Override
+ public int compare(final Camera.Size left, final Camera.Size right) {
+ // If one size is less than the max size prefer it over the other
+ if ((left.width <= mMaxWidth && left.height <= mMaxHeight)
+ != (right.width <= mMaxWidth && right.height <= mMaxHeight)) {
+ return left.width <= mMaxWidth ? PREFER_LEFT : PREFER_RIGHT;
+ }
+
+ // If one is closer to the target aspect ratio, prefer it.
+ final float leftAspectRatio = left.width / (float) left.height;
+ final float rightAspectRatio = right.width / (float) right.height;
+ final float leftAspectRatioDiff = Math.abs(leftAspectRatio - mTargetAspectRatio);
+ final float rightAspectRatioDiff = Math.abs(rightAspectRatio - mTargetAspectRatio);
+ if (leftAspectRatioDiff != rightAspectRatioDiff) {
+ return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? PREFER_LEFT : PREFER_RIGHT;
+ }
+
+ // At this point they have the same aspect ratio diff and are either both bigger
+ // than the max size or both smaller than the max size, so prefer the one closest
+ // to target size
+ final int leftDiff = Math.abs((left.width * left.height) - mTargetPixels);
+ final int rightDiff = Math.abs((right.width * right.height) - mTargetPixels);
+ return leftDiff - rightDiff;
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void autoFocus() {
+ if (mCamera == null) {
+ return;
+ }
+
+ try {
+ mCamera.autoFocus(
+ new Camera.AutoFocusCallback() {
+ @Override
+ public void onAutoFocus(final boolean success, final Camera camera) {
+ mFocusOverlayManager.onAutoFocus(success, false /* shutterDown */);
+ }
+ });
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.autoFocus", "RuntimeException in CameraManager.autoFocus", e);
+ // If autofocus fails, the camera should have called the callback with success=false,
+ // but some throw an exception here
+ mFocusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/);
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void cancelAutoFocus() {
+ if (mCamera == null) {
+ return;
+ }
+ try {
+ mCamera.cancelAutoFocus();
+ } catch (final RuntimeException e) {
+ // Ignore
+ LogUtil.e(
+ "CameraManager.cancelAutoFocus", "RuntimeException in CameraManager.cancelAutoFocus", e);
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public boolean capture() {
+ return false;
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void setFocusParameters() {
+ if (mCamera == null) {
+ return;
+ }
+ try {
+ final Camera.Parameters parameters = mCamera.getParameters();
+ parameters.setFocusMode(mFocusOverlayManager.getFocusMode());
+ if (parameters.getMaxNumFocusAreas() > 0) {
+ // Don't set focus areas (even to null) if focus areas aren't supported, camera may
+ // crash
+ parameters.setFocusAreas(mFocusOverlayManager.getFocusAreas());
+ }
+ parameters.setMeteringAreas(mFocusOverlayManager.getMeteringAreas());
+ mCamera.setParameters(parameters);
+ } catch (final RuntimeException e) {
+ // This occurs when the device is out of space or when the camera is locked
+ LogUtil.e(
+ "CameraManager.setFocusParameters",
+ "RuntimeException in CameraManager setFocusParameters");
+ }
+ }
+
+ public void resetPreview() {
+ mCamera.startPreview();
+ if (mCameraPreview != null) {
+ mCameraPreview.setFocusable(true);
+ }
+ }
+
+ private void logCameraSize(final String prefix, final Camera.Size size) {
+ // Log the camera size and aspect ratio for help when examining bug reports for camera
+ // failures
+ LogUtil.i(
+ "CameraManager.logCameraSize",
+ prefix + size.width + "x" + size.height + " (" + (size.width / (float) size.height) + ")");
+ }
+
+ @VisibleForTesting
+ public void resetCameraManager() {
+ sInstance = null;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/CameraPreview.java b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
new file mode 100644
index 0000000..6581ad6
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
@@ -0,0 +1,177 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.hardware.Camera;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnTouchListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.PermissionsUtil;
+import java.io.IOException;
+
+/**
+ * Contains shared code for SoftwareCameraPreview and HardwareCameraPreview, cannot use inheritance
+ * because those classes must inherit from separate Views, so those classes delegate calls to this
+ * helper class. Specifics for each implementation are in CameraPreviewHost
+ */
+public class CameraPreview {
+ /** Implemented by the camera for rendering. */
+ public interface CameraPreviewHost {
+ View getView();
+
+ boolean isValid();
+
+ void startPreview(final Camera camera) throws IOException;
+
+ void onCameraPermissionGranted();
+
+ void setShown();
+ }
+
+ private int mCameraWidth = -1;
+ private int mCameraHeight = -1;
+ private boolean mTabHasBeenShown = false;
+ private OnTouchListener mListener;
+
+ private final CameraPreviewHost mHost;
+
+ public CameraPreview(final CameraPreviewHost host) {
+ Assert.isNotNull(host);
+ Assert.isNotNull(host.getView());
+ mHost = host;
+ }
+
+ // This is set when the tab is actually selected.
+ public void setShown() {
+ mTabHasBeenShown = true;
+ maybeOpenCamera();
+ }
+
+ // Opening camera is very expensive. Most of the ANR reports seem to be related to the camera.
+ // So we delay until the camera is actually needed. See b/23287938
+ private void maybeOpenCamera() {
+ boolean visible = mHost.getView().getVisibility() == View.VISIBLE;
+ if (mTabHasBeenShown && visible && PermissionsUtil.hasCameraPermissions(getContext())) {
+ CameraManager.get().openCamera();
+ }
+ }
+
+ public void setSize(final Camera.Size size, final int orientation) {
+ switch (orientation) {
+ case 0:
+ case 180:
+ mCameraWidth = size.width;
+ mCameraHeight = size.height;
+ break;
+ case 90:
+ case 270:
+ default:
+ mCameraWidth = size.height;
+ mCameraHeight = size.width;
+ }
+ mHost.getView().requestLayout();
+ }
+
+ public int getWidthMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
+ if (mCameraHeight >= 0) {
+ final int width = View.MeasureSpec.getSize(widthMeasureSpec);
+ return MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
+ } else {
+ return widthMeasureSpec;
+ }
+ }
+
+ public int getHeightMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
+ if (mCameraHeight >= 0) {
+ final int orientation = getContext().getResources().getConfiguration().orientation;
+ final int width = View.MeasureSpec.getSize(widthMeasureSpec);
+ final float aspectRatio = (float) mCameraWidth / (float) mCameraHeight;
+ int height;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ height = (int) (width * aspectRatio);
+ } else {
+ height = (int) (width / aspectRatio);
+ }
+ return View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
+ } else {
+ return heightMeasureSpec;
+ }
+ }
+
+ // onVisibilityChanged is set to Visible when the tab is _created_,
+ // which may be when the user is viewing a different tab.
+ public void onVisibilityChanged(final int visibility) {
+ if (PermissionsUtil.hasCameraPermissions(getContext())) {
+ if (visibility == View.VISIBLE) {
+ maybeOpenCamera();
+ } else {
+ CameraManager.get().closeCamera();
+ }
+ }
+ }
+
+ public Context getContext() {
+ return mHost.getView().getContext();
+ }
+
+ public void setOnTouchListener(final View.OnTouchListener listener) {
+ mListener = listener;
+ mHost.getView().setOnTouchListener(listener);
+ }
+
+ public void setFocusable(boolean focusable) {
+ mHost.getView().setOnTouchListener(focusable ? mListener : null);
+ }
+
+ public int getHeight() {
+ return mHost.getView().getHeight();
+ }
+
+ public void onAttachedToWindow() {
+ maybeOpenCamera();
+ }
+
+ public void onDetachedFromWindow() {
+ CameraManager.get().closeCamera();
+ }
+
+ public void onRestoreInstanceState() {
+ maybeOpenCamera();
+ }
+
+ public void onCameraPermissionGranted() {
+ maybeOpenCamera();
+ }
+
+ /** @return True if the view is valid and prepared for the camera to start showing the preview */
+ public boolean isValid() {
+ return mHost.isValid();
+ }
+
+ /**
+ * Starts the camera preview on the current surface. Abstracts out the differences in API from the
+ * CameraManager
+ *
+ * @throws IOException Which is caught by the CameraManager to display an error
+ */
+ public void startPreview(final Camera camera) throws IOException {
+ mHost.startPreview(camera);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java b/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java
new file mode 100644
index 0000000..c0d61f3
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java
@@ -0,0 +1,125 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.TextureView;
+import android.view.View;
+import java.io.IOException;
+
+/**
+ * A hardware accelerated preview texture for the camera. This is the preferred CameraPreview
+ * because it animates smoother. When hardware acceleration isn't available, SoftwareCameraPreview
+ * is used.
+ *
+ * <p>There is a significant amount of duplication between HardwareCameraPreview and
+ * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
+ * implementations of the shared methods are delegated to CameraPreview
+ */
+public class HardwareCameraPreview extends TextureView implements CameraPreview.CameraPreviewHost {
+ private CameraPreview mPreview;
+
+ public HardwareCameraPreview(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mPreview = new CameraPreview(this);
+ setSurfaceTextureListener(
+ new SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(
+ final SurfaceTexture surfaceTexture, final int i, final int i2) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ final SurfaceTexture surfaceTexture, final int i, final int i2) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) {
+ CameraManager.get().setSurface(null);
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) {
+ CameraManager.get().setSurface(mPreview);
+ }
+ });
+ }
+
+ @Override
+ public void setShown() {
+ mPreview.setShown();
+ }
+
+ @Override
+ protected void onVisibilityChanged(final View changedView, final int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mPreview.onVisibilityChanged(visibility);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPreview.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPreview.onAttachedToWindow();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ super.onRestoreInstanceState(state);
+ mPreview.onRestoreInstanceState();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public boolean isValid() {
+ return getSurfaceTexture() != null;
+ }
+
+ @Override
+ public void startPreview(final Camera camera) throws IOException {
+ camera.setPreviewTexture(getSurfaceTexture());
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ mPreview.onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
new file mode 100644
index 0000000..1500094
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
@@ -0,0 +1,143 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.v4.content.FileProvider;
+import com.android.dialer.callcomposer.camera.exif.ExifInterface;
+import com.android.dialer.callcomposer.camera.exif.ExifTag;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FallibleAsyncTask;
+import com.android.dialer.constants.Constants;
+import com.android.dialer.util.DialerUtils;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Persisting image routine. */
+@TargetApi(VERSION_CODES.M)
+public class ImagePersistTask extends FallibleAsyncTask<Void, Void, Uri> {
+ private int mWidth;
+ private int mHeight;
+ private final float mHeightPercent;
+ private final byte[] mBytes;
+ private final Context mContext;
+ private final CameraManager.MediaCallback mCallback;
+
+ ImagePersistTask(
+ final int width,
+ final int height,
+ final float heightPercent,
+ final byte[] bytes,
+ final Context context,
+ final CameraManager.MediaCallback callback) {
+ Assert.checkArgument(heightPercent >= 0 && heightPercent <= 1);
+ Assert.isNotNull(bytes);
+ Assert.isNotNull(context);
+ Assert.isNotNull(callback);
+ mWidth = width;
+ mHeight = height;
+ mHeightPercent = heightPercent;
+ mBytes = bytes;
+ mContext = context;
+ mCallback = callback;
+ }
+
+ @Override
+ protected Uri doInBackgroundFallible(final Void... params) throws Exception {
+ File outputFile = DialerUtils.createShareableFile(mContext);
+
+ try (OutputStream outputStream = new FileOutputStream(outputFile)) {
+ if (mHeightPercent != 1.0f) {
+ writeClippedBitmap(outputStream);
+ } else {
+ outputStream.write(mBytes, 0, mBytes.length);
+ }
+ }
+
+ return FileProvider.getUriForFile(
+ mContext, Constants.get().getFileProviderAuthority(), outputFile);
+ }
+
+ @Override
+ protected void onPostExecute(FallibleTaskResult<Uri> result) {
+ if (result.isFailure()) {
+ mCallback.onMediaFailed(new Exception("Persisting image failed", result.getThrowable()));
+ } else {
+ mCallback.onMediaReady(result.getResult(), "image/jpeg", mWidth, mHeight);
+ }
+ }
+
+ private void writeClippedBitmap(OutputStream outputStream) throws IOException {
+ int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
+ final ExifInterface exifInterface = new ExifInterface();
+ try {
+ exifInterface.readExif(mBytes);
+ final Integer orientationValue = exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (orientationValue != null) {
+ orientation = orientationValue.intValue();
+ }
+ } catch (final IOException e) {
+ // Couldn't get exif tags, not the end of the world
+ }
+ Bitmap bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length);
+ final int clippedWidth;
+ final int clippedHeight;
+ if (ExifInterface.getOrientationParams(orientation).invertDimensions) {
+ Assert.checkState(mWidth == bitmap.getHeight());
+ Assert.checkState(mHeight == bitmap.getWidth());
+ clippedWidth = (int) (mHeight * mHeightPercent);
+ clippedHeight = mWidth;
+ } else {
+ Assert.checkState(mWidth == bitmap.getWidth());
+ Assert.checkState(mHeight == bitmap.getHeight());
+ clippedWidth = mWidth;
+ clippedHeight = (int) (mHeight * mHeightPercent);
+ }
+ final int offsetTop = (bitmap.getHeight() - clippedHeight) / 2;
+ final int offsetLeft = (bitmap.getWidth() - clippedWidth) / 2;
+ mWidth = clippedWidth;
+ mHeight = clippedHeight;
+ Bitmap clippedBitmap =
+ Bitmap.createBitmap(clippedWidth, clippedHeight, Bitmap.Config.ARGB_8888);
+ clippedBitmap.setDensity(bitmap.getDensity());
+ final Canvas clippedBitmapCanvas = new Canvas(clippedBitmap);
+ final Matrix matrix = new Matrix();
+ matrix.postTranslate(-offsetLeft, -offsetTop);
+ clippedBitmapCanvas.drawBitmap(bitmap, matrix, null /* paint */);
+ clippedBitmapCanvas.save();
+ clippedBitmap = CopyAndResizeImageTask.resizeForEnrichedCalling(clippedBitmap);
+ // EXIF data can take a big chunk of the file size and is often cleared by the
+ // carrier, only store orientation since that's critical
+ final ExifTag orientationTag = exifInterface.getTag(ExifInterface.TAG_ORIENTATION);
+ exifInterface.clearExif();
+ exifInterface.setTag(orientationTag);
+ exifInterface.writeExif(clippedBitmap, outputStream);
+
+ clippedBitmap.recycle();
+ bitmap.recycle();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java b/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java
new file mode 100644
index 0000000..fe2c600
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java
@@ -0,0 +1,120 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.hardware.Camera;
+import android.os.Parcelable;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import java.io.IOException;
+
+/**
+ * A software rendered preview surface for the camera. This renders slower and causes more jank, so
+ * HardwareCameraPreview is preferred if possible.
+ *
+ * <p>There is a significant amount of duplication between HardwareCameraPreview and
+ * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
+ * implementations of the shared methods are delegated to CameraPreview
+ */
+public class SoftwareCameraPreview extends SurfaceView implements CameraPreview.CameraPreviewHost {
+ private final CameraPreview mPreview;
+
+ public SoftwareCameraPreview(final Context context) {
+ super(context);
+ mPreview = new CameraPreview(this);
+ getHolder()
+ .addCallback(
+ new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(final SurfaceHolder surfaceHolder) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void surfaceChanged(
+ final SurfaceHolder surfaceHolder,
+ final int format,
+ final int width,
+ final int height) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void surfaceDestroyed(final SurfaceHolder surfaceHolder) {
+ CameraManager.get().setSurface(null);
+ }
+ });
+ }
+
+ @Override
+ public void setShown() {
+ mPreview.setShown();
+ }
+
+ @Override
+ protected void onVisibilityChanged(final View changedView, final int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mPreview.onVisibilityChanged(visibility);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPreview.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPreview.onAttachedToWindow();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ super.onRestoreInstanceState(state);
+ mPreview.onRestoreInstanceState();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public boolean isValid() {
+ return getHolder() != null;
+ }
+
+ @Override
+ public void startPreview(final Camera camera) throws IOException {
+ camera.setPreviewDisplay(getHolder());
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ mPreview.onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml b/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml
new file mode 100644
index 0000000..77ef222
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ 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 package="com.android.dialer.callcomposer.camera.camerafocus"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java
new file mode 100644
index 0000000..234cf53
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java
@@ -0,0 +1,28 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+/** Methods needed by the CameraPreview in order communicate camera events. */
+public interface FocusIndicator {
+ void showStart();
+
+ void showSuccess(boolean timeout);
+
+ void showFail(boolean timeout);
+
+ void clear();
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java
new file mode 100644
index 0000000..1c5ac38
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java
@@ -0,0 +1,482 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera.Area;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that handles everything about focus in still picture mode. This also handles the metering
+ * area because it is the same as focus area.
+ *
+ * <p>The test cases: (1) The camera has continuous autofocus. Move the camera. Take a picture when
+ * CAF is not in progress. (2) The camera has continuous autofocus. Move the camera. Take a picture
+ * when CAF is in progress. (3) The camera has face detection. Point the camera at some faces. Hold
+ * the shutter. Release to take a picture. (4) The camera has face detection. Point the camera at
+ * some faces. Single tap the shutter to take a picture. (5) The camera has autofocus. Single tap
+ * the shutter to take a picture. (6) The camera has autofocus. Hold the shutter. Release to take a
+ * picture. (7) The camera has no autofocus. Single tap the shutter and take a picture. (8) The
+ * camera has autofocus and supports focus area. Touch the screen to trigger autofocus. Take a
+ * picture. (9) The camera has autofocus and supports focus area. Touch the screen to trigger
+ * autofocus. Wait until it times out. (10) The camera has no autofocus and supports metering area.
+ * Touch the screen to change metering area.
+ */
+public class FocusOverlayManager {
+ private static final String TRUE = "true";
+ private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
+ private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED =
+ "auto-whitebalance-lock-supported";
+
+ private static final int RESET_TOUCH_FOCUS = 0;
+ private static final int RESET_TOUCH_FOCUS_DELAY = 3000;
+
+ private int mState = STATE_IDLE;
+ private static final int STATE_IDLE = 0; // Focus is not active.
+ private static final int STATE_FOCUSING = 1; // Focus is in progress.
+ // Focus is in progress and the camera should take a picture after focus finishes.
+ private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
+ private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
+ private static final int STATE_FAIL = 4; // Focus finishes and fails.
+
+ private boolean mInitialized;
+ private boolean mFocusAreaSupported;
+ private boolean mMeteringAreaSupported;
+ private boolean mLockAeAwbNeeded;
+ private boolean mAeAwbLock;
+ private Matrix mMatrix;
+
+ private PieRenderer mPieRenderer;
+
+ private int mPreviewWidth; // The width of the preview frame layout.
+ private int mPreviewHeight; // The height of the preview frame layout.
+ private boolean mMirror; // true if the camera is front-facing.
+ private List<Area> mFocusArea; // focus area in driver format
+ private List<Area> mMeteringArea; // metering area in driver format
+ private String mFocusMode;
+ private Parameters mParameters;
+ private Handler mHandler;
+ private Listener mListener;
+
+ /** Listener used for the focus indicator to communicate back to the camera. */
+ public interface Listener {
+ void autoFocus();
+
+ void cancelAutoFocus();
+
+ boolean capture();
+
+ void setFocusParameters();
+ }
+
+ private class MainHandler extends Handler {
+ public MainHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case RESET_TOUCH_FOCUS:
+ {
+ cancelAutoFocus();
+ break;
+ }
+ }
+ }
+ }
+
+ public FocusOverlayManager(Listener listener, Looper looper) {
+ mHandler = new MainHandler(looper);
+ mMatrix = new Matrix();
+ mListener = listener;
+ }
+
+ public void setFocusRenderer(PieRenderer renderer) {
+ mPieRenderer = renderer;
+ mInitialized = (mMatrix != null);
+ }
+
+ public void setParameters(Parameters parameters) {
+ // parameters can only be null when onConfigurationChanged is called
+ // before camera is open. We will just return in this case, because
+ // parameters will be set again later with the right parameters after
+ // camera is open.
+ if (parameters == null) {
+ return;
+ }
+ mParameters = parameters;
+ mFocusAreaSupported = isFocusAreaSupported(parameters);
+ mMeteringAreaSupported = isMeteringAreaSupported(parameters);
+ mLockAeAwbNeeded =
+ (isAutoExposureLockSupported(mParameters) || isAutoWhiteBalanceLockSupported(mParameters));
+ }
+
+ public void setPreviewSize(int previewWidth, int previewHeight) {
+ if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) {
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+ setMatrix();
+ }
+ }
+
+ public void setMirror(boolean mirror) {
+ mMirror = mirror;
+ setMatrix();
+ }
+
+ private void setMatrix() {
+ if (mPreviewWidth != 0 && mPreviewHeight != 0) {
+ Matrix matrix = new Matrix();
+ prepareMatrix(matrix, mMirror, mPreviewWidth, mPreviewHeight);
+ // In face detection, the matrix converts the driver coordinates to UI
+ // coordinates. In tap focus, the inverted matrix converts the UI
+ // coordinates to driver coordinates.
+ matrix.invert(mMatrix);
+ mInitialized = (mPieRenderer != null);
+ }
+ }
+
+ private void lockAeAwbIfNeeded() {
+ if (mLockAeAwbNeeded && !mAeAwbLock) {
+ mAeAwbLock = true;
+ mListener.setFocusParameters();
+ }
+ }
+
+ public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
+ if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ // Take the picture no matter focus succeeds or fails. No need
+ // to play the AF sound if we're about to play the shutter
+ // sound.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ capture();
+ } else if (mState == STATE_FOCUSING) {
+ // This happens when (1) user is half-pressing the focus key or
+ // (2) touch focus is triggered. Play the focus tone. Do not
+ // take the picture now.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ // If this is triggered by touch focus, cancel focus after a
+ // while.
+ if (mFocusArea != null) {
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ if (shutterButtonPressed) {
+ // Lock AE & AWB so users can half-press shutter and recompose.
+ lockAeAwbIfNeeded();
+ }
+ } else if (mState == STATE_IDLE) {
+ // User has released the focus key before focus completes.
+ // Do nothing.
+ }
+ }
+
+ public void onAutoFocusMoving(boolean moving) {
+ if (!mInitialized) {
+ return;
+ }
+
+ // Ignore if we have requested autofocus. This method only handles
+ // continuous autofocus.
+ if (mState != STATE_IDLE) {
+ return;
+ }
+
+ if (moving) {
+ mPieRenderer.showStart();
+ } else {
+ mPieRenderer.showSuccess(true);
+ }
+ }
+
+ private void initializeFocusAreas(
+ int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) {
+ if (mFocusArea == null) {
+ mFocusArea = new ArrayList<>();
+ mFocusArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ calculateTapArea(
+ focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight, mFocusArea.get(0).rect);
+ }
+
+ private void initializeMeteringAreas(
+ int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) {
+ if (mMeteringArea == null) {
+ mMeteringArea = new ArrayList<>();
+ mMeteringArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ // AE area is bigger because exposure is sensitive and
+ // easy to over- or underexposure if area is too small.
+ calculateTapArea(
+ focusWidth,
+ focusHeight,
+ 1.5f,
+ x,
+ y,
+ previewWidth,
+ previewHeight,
+ mMeteringArea.get(0).rect);
+ }
+
+ public void onSingleTapUp(int x, int y) {
+ if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ return;
+ }
+
+ // Let users be able to cancel previous touch focus.
+ if ((mFocusArea != null)
+ && (mState == STATE_FOCUSING || mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+ cancelAutoFocus();
+ }
+ // Initialize variables.
+ int focusWidth = mPieRenderer.getSize();
+ int focusHeight = mPieRenderer.getSize();
+ if (focusWidth == 0 || mPieRenderer.getWidth() == 0 || mPieRenderer.getHeight() == 0) {
+ return;
+ }
+ int previewWidth = mPreviewWidth;
+ int previewHeight = mPreviewHeight;
+ // Initialize mFocusArea.
+ if (mFocusAreaSupported) {
+ initializeFocusAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+ }
+ // Initialize mMeteringArea.
+ if (mMeteringAreaSupported) {
+ initializeMeteringAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+ }
+
+ // Use margin to set the focus indicator to the touched area.
+ mPieRenderer.setFocus(x, y);
+
+ // Set the focus area and metering area.
+ mListener.setFocusParameters();
+ if (mFocusAreaSupported) {
+ autoFocus();
+ } else { // Just show the indicator in all other cases.
+ updateFocusUI();
+ // Reset the metering area in 3 seconds.
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ }
+
+ public void onPreviewStarted() {
+ mState = STATE_IDLE;
+ }
+
+ public void onPreviewStopped() {
+ // If auto focus was in progress, it would have been stopped.
+ mState = STATE_IDLE;
+ resetTouchFocus();
+ updateFocusUI();
+ }
+
+ public void onCameraReleased() {
+ onPreviewStopped();
+ }
+
+ private void autoFocus() {
+ LogUtil.v("FocusOverlayManager.autoFocus", "Start autofocus.");
+ mListener.autoFocus();
+ mState = STATE_FOCUSING;
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ public void cancelAutoFocus() {
+ LogUtil.v("FocusOverlayManager.cancelAutoFocus", "Cancel autofocus.");
+
+ // Reset the tap area before calling mListener.cancelAutofocus.
+ // Otherwise, focus mode stays at auto and the tap area passed to the
+ // driver is not reset.
+ resetTouchFocus();
+ mListener.cancelAutoFocus();
+ mState = STATE_IDLE;
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ private void capture() {
+ if (mListener.capture()) {
+ mState = STATE_IDLE;
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+ }
+
+ public String getFocusMode() {
+ List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
+
+ if (mFocusAreaSupported && mFocusArea != null) {
+ // Always use autofocus in tap-to-focus.
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ mFocusMode = Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
+ }
+
+ if (!isSupported(mFocusMode, supportedFocusModes)) {
+ // For some reasons, the driver does not support the current
+ // focus mode. Fall back to auto.
+ if (isSupported(Parameters.FOCUS_MODE_AUTO, mParameters.getSupportedFocusModes())) {
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ mFocusMode = mParameters.getFocusMode();
+ }
+ }
+ return mFocusMode;
+ }
+
+ public List<Area> getFocusAreas() {
+ return mFocusArea;
+ }
+
+ public List<Area> getMeteringAreas() {
+ return mMeteringArea;
+ }
+
+ private void updateFocusUI() {
+ if (!mInitialized) {
+ return;
+ }
+ FocusIndicator focusIndicator = mPieRenderer;
+
+ if (mState == STATE_IDLE) {
+ if (mFocusArea == null) {
+ focusIndicator.clear();
+ } else {
+ // Users touch on the preview and the indicator represents the
+ // metering area. Either focus area is not supported or
+ // autoFocus call is not required.
+ focusIndicator.showStart();
+ }
+ } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ focusIndicator.showStart();
+ } else {
+ if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
+ // TODO: check HAL behavior and decide if this can be removed.
+ focusIndicator.showSuccess(false);
+ } else if (mState == STATE_SUCCESS) {
+ focusIndicator.showSuccess(false);
+ } else if (mState == STATE_FAIL) {
+ focusIndicator.showFail(false);
+ }
+ }
+ }
+
+ private void resetTouchFocus() {
+ if (!mInitialized) {
+ return;
+ }
+
+ // Put focus indicator to the center. clear reset position
+ mPieRenderer.clear();
+
+ mFocusArea = null;
+ mMeteringArea = null;
+ }
+
+ private void calculateTapArea(
+ int focusWidth,
+ int focusHeight,
+ float areaMultiple,
+ int x,
+ int y,
+ int previewWidth,
+ int previewHeight,
+ Rect rect) {
+ int areaWidth = (int) (focusWidth * areaMultiple);
+ int areaHeight = (int) (focusHeight * areaMultiple);
+ final int maxW = previewWidth - areaWidth;
+ int left = maxW > 0 ? clamp(x - areaWidth / 2, 0, maxW) : 0;
+ final int maxH = previewHeight - areaHeight;
+ int top = maxH > 0 ? clamp(y - areaHeight / 2, 0, maxH) : 0;
+
+ RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight);
+ mMatrix.mapRect(rectF);
+ rectFToRect(rectF, rect);
+ }
+
+ private int clamp(int x, int min, int max) {
+ Assert.checkArgument(max >= min);
+ if (x > max) {
+ return max;
+ }
+ if (x < min) {
+ return min;
+ }
+ return x;
+ }
+
+ private boolean isAutoExposureLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED));
+ }
+
+ private boolean isAutoWhiteBalanceLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED));
+ }
+
+ private boolean isSupported(String value, List<String> supported) {
+ return supported != null && supported.indexOf(value) >= 0;
+ }
+
+ private boolean isMeteringAreaSupported(Parameters params) {
+ return params.getMaxNumMeteringAreas() > 0;
+ }
+
+ private boolean isFocusAreaSupported(Parameters params) {
+ return (params.getMaxNumFocusAreas() > 0
+ && isSupported(Parameters.FOCUS_MODE_AUTO, params.getSupportedFocusModes()));
+ }
+
+ private void prepareMatrix(Matrix matrix, boolean mirror, int viewWidth, int viewHeight) {
+ // Need mirror for front camera.
+ matrix.setScale(mirror ? -1 : 1, 1);
+ // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
+ // UI coordinates range from (0, 0) to (width, height).
+ matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);
+ matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
+ }
+
+ private void rectFToRect(RectF rectF, Rect rect) {
+ rect.left = Math.round(rectF.left);
+ rect.top = Math.round(rectF.top);
+ rect.right = Math.round(rectF.right);
+ rect.bottom = Math.round(rectF.bottom);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java b/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java
new file mode 100644
index 0000000..4a3b522
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java
@@ -0,0 +1,97 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+/** Abstract class that all Camera overlays should implement. */
+public abstract class OverlayRenderer implements RenderOverlay.Renderer {
+
+ protected RenderOverlay mOverlay;
+
+ private int mLeft;
+ private int mTop;
+ private int mRight;
+ private int mBottom;
+ private boolean mVisible;
+
+ public void setVisible(boolean vis) {
+ mVisible = vis;
+ update();
+ }
+
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ // default does not handle touch
+ @Override
+ public boolean handlesTouch() {
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ return false;
+ }
+
+ public abstract void onDraw(Canvas canvas);
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mVisible) {
+ onDraw(canvas);
+ }
+ }
+
+ @Override
+ public void setOverlay(RenderOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ @Override
+ public void layout(int left, int top, int right, int bottom) {
+ mLeft = left;
+ mRight = right;
+ mTop = top;
+ mBottom = bottom;
+ }
+
+ protected Context getContext() {
+ if (mOverlay != null) {
+ return mOverlay.getContext();
+ } else {
+ return null;
+ }
+ }
+
+ public int getWidth() {
+ return mRight - mLeft;
+ }
+
+ public int getHeight() {
+ return mBottom - mTop;
+ }
+
+ protected void update() {
+ if (mOverlay != null) {
+ mOverlay.update();
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java b/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java
new file mode 100644
index 0000000..86f69c0
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java
@@ -0,0 +1,179 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import java.util.List;
+
+/** Pie menu item. */
+public class PieItem {
+
+ /** Listener to detect pie item clicks. */
+ public interface OnClickListener {
+ void onClick(PieItem item);
+ }
+
+ private Drawable mDrawable;
+ private int level;
+ private float mCenter;
+ private float start;
+ private float sweep;
+ private float animate;
+ private int inner;
+ private int outer;
+ private boolean mSelected;
+ private boolean mEnabled;
+ private List<PieItem> mItems;
+ private Path mPath;
+ private OnClickListener mOnClickListener;
+ private float mAlpha;
+
+ // Gray out the view when disabled
+ private static final float ENABLED_ALPHA = 1;
+ private static final float DISABLED_ALPHA = (float) 0.3;
+
+ public PieItem(Drawable drawable, int level) {
+ mDrawable = drawable;
+ this.level = level;
+ setAlpha(1f);
+ mEnabled = true;
+ setAnimationAngle(getAnimationAngle());
+ start = -1;
+ mCenter = -1;
+ }
+
+ public boolean hasItems() {
+ return mItems != null;
+ }
+
+ public List<PieItem> getItems() {
+ return mItems;
+ }
+
+ public void setPath(Path p) {
+ mPath = p;
+ }
+
+ public Path getPath() {
+ return mPath;
+ }
+
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ mDrawable.setAlpha((int) (255 * alpha));
+ }
+
+ public void setAnimationAngle(float a) {
+ animate = a;
+ }
+
+ private float getAnimationAngle() {
+ return animate;
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ if (mEnabled) {
+ setAlpha(ENABLED_ALPHA);
+ } else {
+ setAlpha(DISABLED_ALPHA);
+ }
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setSelected(boolean s) {
+ mSelected = s;
+ }
+
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public void setGeometry(float st, float sw, int inside, int outside) {
+ start = st;
+ sweep = sw;
+ inner = inside;
+ outer = outside;
+ }
+
+ public float getCenter() {
+ return mCenter;
+ }
+
+ public float getStart() {
+ return start;
+ }
+
+ public float getStartAngle() {
+ return start + animate;
+ }
+
+ public float getSweep() {
+ return sweep;
+ }
+
+ public int getInnerRadius() {
+ return inner;
+ }
+
+ public int getOuterRadius() {
+ return outer;
+ }
+
+ public void setOnClickListener(OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void performClick() {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(this);
+ }
+ }
+
+ public int getIntrinsicWidth() {
+ return mDrawable.getIntrinsicWidth();
+ }
+
+ public int getIntrinsicHeight() {
+ return mDrawable.getIntrinsicHeight();
+ }
+
+ public void setBounds(int left, int top, int right, int bottom) {
+ mDrawable.setBounds(left, top, right, bottom);
+ }
+
+ public void draw(Canvas canvas) {
+ mDrawable.draw(canvas);
+ }
+
+ public void setImageResource(Context context, int resId) {
+ Drawable d = context.getResources().getDrawable(resId).mutate();
+ d.setBounds(mDrawable.getBounds());
+ mDrawable = d;
+ setAlpha(mAlpha);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java b/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java
new file mode 100644
index 0000000..59b57b0
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java
@@ -0,0 +1,816 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Used to draw and render the pie item focus indicator. */
+public class PieRenderer extends OverlayRenderer implements FocusIndicator {
+ // Sometimes continuous autofocus starts and stops several times quickly.
+ // These states are used to make sure the animation is run for at least some
+ // time.
+ private volatile int mState;
+ private ScaleAnimation mAnimation = new ScaleAnimation();
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_FOCUSING = 1;
+ private static final int STATE_FINISHING = 2;
+ private static final int STATE_PIE = 8;
+
+ private Runnable mDisappear = new Disappear();
+ private Animation.AnimationListener mEndAction = new EndAction();
+ private static final int SCALING_UP_TIME = 600;
+ private static final int SCALING_DOWN_TIME = 100;
+ private static final int DISAPPEAR_TIMEOUT = 200;
+ private static final int DIAL_HORIZONTAL = 157;
+
+ private static final long PIE_FADE_IN_DURATION = 200;
+ private static final long PIE_XFADE_DURATION = 200;
+ private static final long PIE_SELECT_FADE_DURATION = 300;
+
+ private static final int MSG_OPEN = 0;
+ private static final int MSG_CLOSE = 1;
+ private static final float PIE_SWEEP = (float) (Math.PI * 2 / 3);
+ // geometry
+ private Point mCenter;
+ private int mRadius;
+ private int mRadiusInc;
+
+ // the detection if touch is inside a slice is offset
+ // inbounds by this amount to allow the selection to show before the
+ // finger covers it
+ private int mTouchOffset;
+
+ private List<PieItem> mItems;
+
+ private PieItem mOpenItem;
+
+ private Paint mSelectedPaint;
+ private Paint mSubPaint;
+
+ // touch handling
+ private PieItem mCurrentItem;
+
+ private Paint mFocusPaint;
+ private int mSuccessColor;
+ private int mFailColor;
+ private int mCircleSize;
+ private int mFocusX;
+ private int mFocusY;
+ private int mCenterX;
+ private int mCenterY;
+
+ private int mDialAngle;
+ private RectF mCircle;
+ private RectF mDial;
+ private Point mPoint1;
+ private Point mPoint2;
+ private int mStartAnimationAngle;
+ private boolean mFocused;
+ private int mInnerOffset;
+ private int mOuterStroke;
+ private int mInnerStroke;
+ private boolean mTapMode;
+ private boolean mBlockFocus;
+ private int mTouchSlopSquared;
+ private Point mDown;
+ private boolean mOpening;
+ private LinearAnimation mXFade;
+ private LinearAnimation mFadeIn;
+ private volatile boolean mFocusCancelled;
+
+ private Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_OPEN:
+ if (mListener != null) {
+ mListener.onPieOpened(mCenter.x, mCenter.y);
+ }
+ break;
+ case MSG_CLOSE:
+ if (mListener != null) {
+ mListener.onPieClosed();
+ }
+ break;
+ }
+ }
+ };
+
+ private PieListener mListener;
+
+ /** Listener for the pie item to communicate back to the renderer. */
+ public interface PieListener {
+ void onPieOpened(int centerX, int centerY);
+
+ void onPieClosed();
+ }
+
+ public void setPieListener(PieListener pl) {
+ mListener = pl;
+ }
+
+ public PieRenderer(Context context) {
+ init(context);
+ }
+
+ private void init(Context ctx) {
+ setVisible(false);
+ mItems = new ArrayList<PieItem>();
+ Resources res = ctx.getResources();
+ mRadius = res.getDimensionPixelSize(R.dimen.pie_radius_start);
+ mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
+ mRadiusInc = res.getDimensionPixelSize(R.dimen.pie_radius_increment);
+ mTouchOffset = res.getDimensionPixelSize(R.dimen.pie_touch_offset);
+ mCenter = new Point(0, 0);
+ mSelectedPaint = new Paint();
+ mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
+ mSelectedPaint.setAntiAlias(true);
+ mSubPaint = new Paint();
+ mSubPaint.setAntiAlias(true);
+ mSubPaint.setColor(Color.argb(200, 250, 230, 128));
+ mFocusPaint = new Paint();
+ mFocusPaint.setAntiAlias(true);
+ mFocusPaint.setColor(Color.WHITE);
+ mFocusPaint.setStyle(Paint.Style.STROKE);
+ mSuccessColor = Color.GREEN;
+ mFailColor = Color.RED;
+ mCircle = new RectF();
+ mDial = new RectF();
+ mPoint1 = new Point();
+ mPoint2 = new Point();
+ mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
+ mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
+ mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
+ mState = STATE_IDLE;
+ mBlockFocus = false;
+ mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
+ mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
+ mDown = new Point();
+ }
+
+ public boolean showsItems() {
+ return mTapMode;
+ }
+
+ public void addItem(PieItem item) {
+ // add the item to the pie itself
+ mItems.add(item);
+ }
+
+ public void removeItem(PieItem item) {
+ mItems.remove(item);
+ }
+
+ public void clearItems() {
+ mItems.clear();
+ }
+
+ public void showInCenter() {
+ if ((mState == STATE_PIE) && isVisible()) {
+ mTapMode = false;
+ show(false);
+ } else {
+ if (mState != STATE_IDLE) {
+ cancelFocus();
+ }
+ mState = STATE_PIE;
+ setCenter(mCenterX, mCenterY);
+ mTapMode = true;
+ show(true);
+ }
+ }
+
+ public void hide() {
+ show(false);
+ }
+
+ /**
+ * guaranteed has center set
+ *
+ * @param show
+ */
+ private void show(boolean show) {
+ if (show) {
+ mState = STATE_PIE;
+ // ensure clean state
+ mCurrentItem = null;
+ mOpenItem = null;
+ for (PieItem item : mItems) {
+ item.setSelected(false);
+ }
+ layoutPie();
+ fadeIn();
+ } else {
+ mState = STATE_IDLE;
+ mTapMode = false;
+ if (mXFade != null) {
+ mXFade.cancel();
+ }
+ }
+ setVisible(show);
+ mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
+ }
+
+ private void fadeIn() {
+ mFadeIn = new LinearAnimation(0, 1);
+ mFadeIn.setDuration(PIE_FADE_IN_DURATION);
+ mFadeIn.setAnimationListener(
+ new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mFadeIn = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mFadeIn.startNow();
+ mOverlay.startAnimation(mFadeIn);
+ }
+
+ public void setCenter(int x, int y) {
+ mCenter.x = x;
+ mCenter.y = y;
+ // when using the pie menu, align the focus ring
+ alignFocus(x, y);
+ }
+
+ private void layoutPie() {
+ int rgap = 2;
+ int inner = mRadius + rgap;
+ int outer = mRadius + mRadiusInc - rgap;
+ int gap = 1;
+ layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
+ }
+
+ private void layoutItems(List<PieItem> items, float centerAngle, int inner, int outer, int gap) {
+ float emptyangle = PIE_SWEEP / 16;
+ float sweep = (PIE_SWEEP - 2 * emptyangle) / items.size();
+ float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
+ // check if we have custom geometry
+ // first item we find triggers custom sweep for all
+ // this allows us to re-use the path
+ for (PieItem item : items) {
+ if (item.getCenter() >= 0) {
+ sweep = item.getSweep();
+ break;
+ }
+ }
+ Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter);
+ for (PieItem item : items) {
+ // shared between items
+ item.setPath(path);
+ if (item.getCenter() >= 0) {
+ angle = item.getCenter();
+ }
+ int w = item.getIntrinsicWidth();
+ int h = item.getIntrinsicHeight();
+ // move views to outer border
+ int r = inner + (outer - inner) * 2 / 3;
+ int x = (int) (r * Math.cos(angle));
+ int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
+ x = mCenter.x + x - w / 2;
+ item.setBounds(x, y, x + w, y + h);
+ float itemstart = angle - sweep / 2;
+ item.setGeometry(itemstart, sweep, inner, outer);
+ if (item.hasItems()) {
+ layoutItems(item.getItems(), angle, inner, outer + mRadiusInc / 2, gap);
+ }
+ angle += sweep;
+ }
+ }
+
+ private Path makeSlice(float start, float end, int outer, int inner, Point center) {
+ RectF bb = new RectF(center.x - outer, center.y - outer, center.x + outer, center.y + outer);
+ RectF bbi = new RectF(center.x - inner, center.y - inner, center.x + inner, center.y + inner);
+ Path path = new Path();
+ path.arcTo(bb, start, end - start, true);
+ path.arcTo(bbi, end, start - end);
+ path.close();
+ return path;
+ }
+
+ /**
+ * converts a
+ *
+ * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
+ * @return skia angle
+ */
+ private float getDegrees(double angle) {
+ return (float) (360 - 180 * angle / Math.PI);
+ }
+
+ private void startFadeOut() {
+ mOverlay
+ .animate()
+ .alpha(0)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ deselect();
+ show(false);
+ mOverlay.setAlpha(1);
+ super.onAnimationEnd(animation);
+ }
+ })
+ .setDuration(PIE_SELECT_FADE_DURATION);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ float alpha = 1;
+ if (mXFade != null) {
+ alpha = mXFade.getValue();
+ } else if (mFadeIn != null) {
+ alpha = mFadeIn.getValue();
+ }
+ int state = canvas.save();
+ if (mFadeIn != null) {
+ float sf = 0.9f + alpha * 0.1f;
+ canvas.scale(sf, sf, mCenter.x, mCenter.y);
+ }
+ drawFocus(canvas);
+ if (mState == STATE_FINISHING) {
+ canvas.restoreToCount(state);
+ return;
+ }
+ if ((mOpenItem == null) || (mXFade != null)) {
+ // draw base menu
+ for (PieItem item : mItems) {
+ drawItem(canvas, item, alpha);
+ }
+ }
+ if (mOpenItem != null) {
+ for (PieItem inner : mOpenItem.getItems()) {
+ drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+ }
+ }
+ canvas.restoreToCount(state);
+ }
+
+ private void drawItem(Canvas canvas, PieItem item, float alpha) {
+ if (mState == STATE_PIE) {
+ if (item.getPath() != null) {
+ if (item.isSelected()) {
+ Paint p = mSelectedPaint;
+ int state = canvas.save();
+ float r = getDegrees(item.getStartAngle());
+ canvas.rotate(r, mCenter.x, mCenter.y);
+ canvas.drawPath(item.getPath(), p);
+ canvas.restoreToCount(state);
+ }
+ alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
+ // draw the item view
+ item.setAlpha(alpha);
+ item.draw(canvas);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ float x = evt.getX();
+ float y = evt.getY();
+ int action = evt.getActionMasked();
+ PointF polar = getPolar(x, y, !(mTapMode));
+ if (MotionEvent.ACTION_DOWN == action) {
+ mDown.x = (int) evt.getX();
+ mDown.y = (int) evt.getY();
+ mOpening = false;
+ if (mTapMode) {
+ PieItem item = findItem(polar);
+ if ((item != null) && (mCurrentItem != item)) {
+ mState = STATE_PIE;
+ onEnter(item);
+ }
+ } else {
+ setCenter((int) x, (int) y);
+ show(true);
+ }
+ return true;
+ } else if (MotionEvent.ACTION_UP == action) {
+ if (isVisible()) {
+ PieItem item = mCurrentItem;
+ if (mTapMode) {
+ item = findItem(polar);
+ if (item != null && mOpening) {
+ mOpening = false;
+ return true;
+ }
+ }
+ if (item == null) {
+ mTapMode = false;
+ show(false);
+ } else if (!mOpening && !item.hasItems()) {
+ item.performClick();
+ startFadeOut();
+ mTapMode = false;
+ }
+ return true;
+ }
+ } else if (MotionEvent.ACTION_CANCEL == action) {
+ if (isVisible() || mTapMode) {
+ show(false);
+ }
+ deselect();
+ return false;
+ } else if (MotionEvent.ACTION_MOVE == action) {
+ if (polar.y < mRadius) {
+ if (mOpenItem != null) {
+ mOpenItem = null;
+ } else {
+ deselect();
+ }
+ return false;
+ }
+ PieItem item = findItem(polar);
+ boolean moved = hasMoved(evt);
+ if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
+ // only select if we didn't just open or have moved past slop
+ mOpening = false;
+ if (moved) {
+ // switch back to swipe mode
+ mTapMode = false;
+ }
+ onEnter(item);
+ }
+ }
+ return false;
+ }
+
+ private boolean hasMoved(MotionEvent e) {
+ return mTouchSlopSquared
+ < (e.getX() - mDown.x) * (e.getX() - mDown.x) + (e.getY() - mDown.y) * (e.getY() - mDown.y);
+ }
+
+ /**
+ * enter a slice for a view updates model only
+ *
+ * @param item
+ */
+ private void onEnter(PieItem item) {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (item != null && item.isEnabled()) {
+ item.setSelected(true);
+ mCurrentItem = item;
+ if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
+ openCurrentItem();
+ }
+ } else {
+ mCurrentItem = null;
+ }
+ }
+
+ private void deselect() {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (mOpenItem != null) {
+ mOpenItem = null;
+ }
+ mCurrentItem = null;
+ }
+
+ private void openCurrentItem() {
+ if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
+ mCurrentItem.setSelected(false);
+ mOpenItem = mCurrentItem;
+ mOpening = true;
+ mXFade = new LinearAnimation(1, 0);
+ mXFade.setDuration(PIE_XFADE_DURATION);
+ mXFade.setAnimationListener(
+ new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mXFade = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mXFade.startNow();
+ mOverlay.startAnimation(mXFade);
+ }
+ }
+
+ private PointF getPolar(float x, float y, boolean useOffset) {
+ PointF res = new PointF();
+ // get angle and radius from x/y
+ res.x = (float) Math.PI / 2;
+ x = x - mCenter.x;
+ y = mCenter.y - y;
+ res.y = (float) Math.sqrt(x * x + y * y);
+ if (x != 0) {
+ res.x = (float) Math.atan2(y, x);
+ if (res.x < 0) {
+ res.x = (float) (2 * Math.PI + res.x);
+ }
+ }
+ res.y = res.y + (useOffset ? mTouchOffset : 0);
+ return res;
+ }
+
+ /**
+ * @param polar x: angle, y: dist
+ * @return the item at angle/dist or null
+ */
+ private PieItem findItem(PointF polar) {
+ // find the matching item:
+ List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
+ for (PieItem item : items) {
+ if (inside(polar, item)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private boolean inside(PointF polar, PieItem item) {
+ return (item.getInnerRadius() < polar.y)
+ && (item.getStartAngle() < polar.x)
+ && (item.getStartAngle() + item.getSweep() > polar.x)
+ && (!mTapMode || (item.getOuterRadius() > polar.y));
+ }
+
+ @Override
+ public boolean handlesTouch() {
+ return true;
+ }
+
+ // focus specific code
+
+ public void setBlockFocus(boolean blocked) {
+ mBlockFocus = blocked;
+ if (blocked) {
+ clear();
+ }
+ }
+
+ public void setFocus(int x, int y) {
+ mFocusX = x;
+ mFocusY = y;
+ setCircle(mFocusX, mFocusY);
+ }
+
+ public void alignFocus(int x, int y) {
+ mOverlay.removeCallbacks(mDisappear);
+ mAnimation.cancel();
+ mAnimation.reset();
+ mFocusX = x;
+ mFocusY = y;
+ mDialAngle = DIAL_HORIZONTAL;
+ setCircle(x, y);
+ mFocused = false;
+ }
+
+ public int getSize() {
+ return 2 * mCircleSize;
+ }
+
+ private int getRandomRange() {
+ return (int) (-60 + 120 * Math.random());
+ }
+
+ @Override
+ public void layout(int l, int t, int r, int b) {
+ super.layout(l, t, r, b);
+ mCenterX = (r - l) / 2;
+ mCenterY = (b - t) / 2;
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ setCircle(mFocusX, mFocusY);
+ if (isVisible() && mState == STATE_PIE) {
+ setCenter(mCenterX, mCenterY);
+ layoutPie();
+ }
+ }
+
+ private void setCircle(int cx, int cy) {
+ mCircle.set(cx - mCircleSize, cy - mCircleSize, cx + mCircleSize, cy + mCircleSize);
+ mDial.set(
+ cx - mCircleSize + mInnerOffset,
+ cy - mCircleSize + mInnerOffset,
+ cx + mCircleSize - mInnerOffset,
+ cy + mCircleSize - mInnerOffset);
+ }
+
+ public void drawFocus(Canvas canvas) {
+ if (mBlockFocus) {
+ return;
+ }
+ mFocusPaint.setStrokeWidth(mOuterStroke);
+ canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
+ if (mState == STATE_PIE) {
+ return;
+ }
+ int color = mFocusPaint.getColor();
+ if (mState == STATE_FINISHING) {
+ mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
+ }
+ mFocusPaint.setStrokeWidth(mInnerStroke);
+ drawLine(canvas, mDialAngle, mFocusPaint);
+ drawLine(canvas, mDialAngle + 45, mFocusPaint);
+ drawLine(canvas, mDialAngle + 180, mFocusPaint);
+ drawLine(canvas, mDialAngle + 225, mFocusPaint);
+ canvas.save();
+ // rotate the arc instead of its offset to better use framework's shape caching
+ canvas.rotate(mDialAngle, mFocusX, mFocusY);
+ canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
+ canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
+ canvas.restore();
+ mFocusPaint.setColor(color);
+ }
+
+ private void drawLine(Canvas canvas, int angle, Paint p) {
+ convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
+ convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
+ canvas.drawLine(
+ mPoint1.x + mFocusX, mPoint1.y + mFocusY, mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
+ }
+
+ private static void convertCart(int angle, int radius, Point out) {
+ double a = 2 * Math.PI * (angle % 360) / 360;
+ out.x = (int) (radius * Math.cos(a) + 0.5);
+ out.y = (int) (radius * Math.sin(a) + 0.5);
+ }
+
+ @Override
+ public void showStart() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ cancelFocus();
+ mStartAnimationAngle = 67;
+ int range = getRandomRange();
+ startAnimation(SCALING_UP_TIME, false, mStartAnimationAngle, mStartAnimationAngle + range);
+ mState = STATE_FOCUSING;
+ }
+
+ @Override
+ public void showSuccess(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME, timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = true;
+ }
+ }
+
+ @Override
+ public void showFail(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME, timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = false;
+ }
+ }
+
+ private void cancelFocus() {
+ mFocusCancelled = true;
+ mOverlay.removeCallbacks(mDisappear);
+ if (mAnimation != null) {
+ mAnimation.cancel();
+ }
+ mFocusCancelled = false;
+ mFocused = false;
+ mState = STATE_IDLE;
+ }
+
+ @Override
+ public void clear() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ cancelFocus();
+ mOverlay.post(mDisappear);
+ }
+
+ private void startAnimation(long duration, boolean timeout, float toScale) {
+ startAnimation(duration, timeout, mDialAngle, toScale);
+ }
+
+ private void startAnimation(long duration, boolean timeout, float fromScale, float toScale) {
+ setVisible(true);
+ mAnimation.reset();
+ mAnimation.setDuration(duration);
+ mAnimation.setScale(fromScale, toScale);
+ mAnimation.setAnimationListener(timeout ? mEndAction : null);
+ mOverlay.startAnimation(mAnimation);
+ update();
+ }
+
+ private class EndAction implements Animation.AnimationListener {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // Keep the focus indicator for some time.
+ if (!mFocusCancelled) {
+ mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+
+ @Override
+ public void onAnimationStart(Animation animation) {}
+ }
+
+ private class Disappear implements Runnable {
+ @Override
+ public void run() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ setVisible(false);
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ mState = STATE_IDLE;
+ setCircle(mFocusX, mFocusY);
+ mFocused = false;
+ }
+ }
+
+ private class ScaleAnimation extends Animation {
+ private float mFrom = 1f;
+ private float mTo = 1f;
+
+ public ScaleAnimation() {
+ setFillAfter(true);
+ }
+
+ public void setScale(float from, float to) {
+ mFrom = from;
+ mTo = to;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mDialAngle = (int) (mFrom + (mTo - mFrom) * interpolatedTime);
+ }
+ }
+
+ private static class LinearAnimation extends Animation {
+ private float mFrom;
+ private float mTo;
+ private float mValue;
+
+ public LinearAnimation(float from, float to) {
+ setFillAfter(true);
+ setInterpolator(new LinearInterpolator());
+ mFrom = from;
+ mTo = to;
+ }
+
+ public float getValue() {
+ return mValue;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java b/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java
new file mode 100644
index 0000000..c38ae6c
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java
@@ -0,0 +1,153 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Focusing overlay. */
+public class RenderOverlay extends FrameLayout {
+
+ /** Render interface. */
+ interface Renderer {
+ boolean handlesTouch();
+
+ boolean onTouchEvent(MotionEvent evt);
+
+ void setOverlay(RenderOverlay overlay);
+
+ void layout(int left, int top, int right, int bottom);
+
+ void draw(Canvas canvas);
+ }
+
+ private RenderView mRenderView;
+ private List<Renderer> mClients;
+
+ // reverse list of touch clients
+ private List<Renderer> mTouchClients;
+ private int[] mPosition = new int[2];
+
+ public RenderOverlay(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRenderView = new RenderView(context);
+ addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ mClients = new ArrayList<>(10);
+ mTouchClients = new ArrayList<>(10);
+ setWillNotDraw(false);
+
+ addRenderer(new PieRenderer(context));
+ }
+
+ public PieRenderer getPieRenderer() {
+ for (Renderer renderer : mClients) {
+ if (renderer instanceof PieRenderer) {
+ return (PieRenderer) renderer;
+ }
+ }
+ return null;
+ }
+
+ public void addRenderer(Renderer renderer) {
+ mClients.add(renderer);
+ renderer.setOverlay(this);
+ if (renderer.handlesTouch()) {
+ mTouchClients.add(0, renderer);
+ }
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void addRenderer(int pos, Renderer renderer) {
+ mClients.add(pos, renderer);
+ renderer.setOverlay(this);
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void remove(Renderer renderer) {
+ mClients.remove(renderer);
+ renderer.setOverlay(null);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent m) {
+ return false;
+ }
+
+ private void adjustPosition() {
+ getLocationInWindow(mPosition);
+ }
+
+ public void update() {
+ mRenderView.invalidate();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private class RenderView extends View {
+
+ public RenderView(Context context) {
+ super(context);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ if (mTouchClients != null) {
+ boolean res = false;
+ for (Renderer client : mTouchClients) {
+ res |= client.onTouchEvent(evt);
+ }
+ return res;
+ }
+ return false;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ adjustPosition();
+ super.onLayout(changed, left, top, right, bottom);
+ if (mClients == null) {
+ return;
+ }
+ for (Renderer renderer : mClients) {
+ renderer.layout(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mClients == null) {
+ return;
+ }
+ boolean redraw = false;
+ for (Renderer renderer : mClients) {
+ renderer.draw(canvas);
+ redraw = redraw || ((OverlayRenderer) renderer).isVisible();
+ }
+ if (redraw) {
+ invalidate();
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml b/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml
new file mode 100644
index 0000000..fba631b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml
@@ -0,0 +1,26 @@
+<?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>
+ <!-- Camera focus indicator values -->
+ <dimen name="pie_radius_start">40dp</dimen>
+ <dimen name="pie_radius_increment">30dp</dimen>
+ <dimen name="pie_touch_offset">20dp</dimen>
+ <dimen name="focus_radius_offset">8dp</dimen>
+ <dimen name="focus_inner_offset">12dp</dimen>
+ <dimen name="focus_outer_stroke">3dp</dimen>
+ <dimen name="focus_inner_stroke">2dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java b/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java
new file mode 100644
index 0000000..e2c8185
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012 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.callcomposer.camera.exif;
+
+import com.android.dialer.common.Assert;
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+
+class CountedDataInputStream extends FilterInputStream {
+
+ private int mCount = 0;
+
+ // allocate a byte buffer for a long value;
+ private final byte[] mByteArray = new byte[8];
+ private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
+
+ CountedDataInputStream(InputStream in) {
+ super(in);
+ }
+
+ int getReadByteCount() {
+ return mCount;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int r = in.read(b);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int r = in.read(b, off, len);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int r = in.read();
+ mCount += (r >= 0) ? 1 : 0;
+ return r;
+ }
+
+ @Override
+ public long skip(long length) throws IOException {
+ long skip = in.skip(length);
+ mCount += skip;
+ return skip;
+ }
+
+ private void skipOrThrow(long length) throws IOException {
+ if (skip(length) != length) {
+ throw new EOFException();
+ }
+ }
+
+ void skipTo(long target) throws IOException {
+ long cur = mCount;
+ long diff = target - cur;
+ Assert.checkArgument(diff >= 0);
+ skipOrThrow(diff);
+ }
+
+ private void readOrThrow(byte[] b, int off, int len) throws IOException {
+ int r = read(b, off, len);
+ if (r != len) {
+ throw new EOFException();
+ }
+ }
+
+ private void readOrThrow(byte[] b) throws IOException {
+ readOrThrow(b, 0, b.length);
+ }
+
+ void setByteOrder(ByteOrder order) {
+ mByteBuffer.order(order);
+ }
+
+ ByteOrder getByteOrder() {
+ return mByteBuffer.order();
+ }
+
+ short readShort() throws IOException {
+ readOrThrow(mByteArray, 0, 2);
+ mByteBuffer.rewind();
+ return mByteBuffer.getShort();
+ }
+
+ int readUnsignedShort() throws IOException {
+ return readShort() & 0xffff;
+ }
+
+ int readInt() throws IOException {
+ readOrThrow(mByteArray, 0, 4);
+ mByteBuffer.rewind();
+ return mByteBuffer.getInt();
+ }
+
+ long readUnsignedInt() throws IOException {
+ return readInt() & 0xffffffffL;
+ }
+
+ String readString(int n, Charset charset) throws IOException {
+ byte[] buf = new byte[n];
+ readOrThrow(buf);
+ return new String(buf, charset);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifData.java b/java/com/android/dialer/callcomposer/camera/exif/ExifData.java
new file mode 100644
index 0000000..27936ae
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifData.java
@@ -0,0 +1,89 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+/**
+ * This class stores the EXIF header in IFDs according to the JPEG specification. It is the result
+ * produced by {@link ExifReader}.
+ *
+ * @see ExifReader
+ * @see IfdData
+ */
+public class ExifData {
+
+ private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
+
+ /**
+ * Adds IFD data. If IFD data of the same type already exists, it will be replaced by the new
+ * data.
+ */
+ void addIfdData(IfdData data) {
+ mIfdDatas[data.getId()] = data;
+ }
+
+ /** Returns the {@link IfdData} object corresponding to a given IFD if it exists or null. */
+ IfdData getIfdData(int ifdId) {
+ if (ExifTag.isValidIfd(ifdId)) {
+ return mIfdDatas[ifdId];
+ }
+ return null;
+ }
+
+ /**
+ * Returns the tag with a given TID in the given IFD if the tag exists. Otherwise returns null.
+ */
+ protected ExifTag getTag(short tag, int ifd) {
+ IfdData ifdData = mIfdDatas[ifd];
+ return (ifdData == null) ? null : ifdData.getTag(tag);
+ }
+
+ /**
+ * Adds the given ExifTag to its default IFD and returns an existing ExifTag with the same TID or
+ * null if none exist.
+ */
+ ExifTag addTag(ExifTag tag) {
+ if (tag != null) {
+ int ifd = tag.getIfd();
+ return addTag(tag, ifd);
+ }
+ return null;
+ }
+
+ /**
+ * Adds the given ExifTag to the given IFD and returns an existing ExifTag with the same TID or
+ * null if none exist.
+ */
+ private ExifTag addTag(ExifTag tag, int ifdId) {
+ if (tag != null && ExifTag.isValidIfd(ifdId)) {
+ IfdData ifdData = getOrCreateIfdData(ifdId);
+ return ifdData.setTag(tag);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link IfdData} object corresponding to a given IFD or generates one if none exist.
+ */
+ private IfdData getOrCreateIfdData(int ifdId) {
+ IfdData ifdData = mIfdDatas[ifdId];
+ if (ifdData == null) {
+ ifdData = new IfdData(ifdId);
+ mIfdDatas[ifdId] = ifdData;
+ }
+ return ifdData;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java b/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java
new file mode 100644
index 0000000..92dee1c
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java
@@ -0,0 +1,374 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.util.SparseIntArray;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.HashSet;
+import java.util.TimeZone;
+
+/**
+ * This class provides methods and constants for reading and writing jpeg file metadata. It contains
+ * a collection of ExifTags, and a collection of definitions for creating valid ExifTags. The
+ * collection of ExifTags can be updated by: reading new ones from a file, deleting or adding
+ * existing ones, or building new ExifTags from a tag definition. These ExifTags can be written to a
+ * valid jpeg image as exif metadata.
+ *
+ * <p>Each ExifTag has a tag ID (TID) and is stored in a specific image file directory (IFD) as
+ * specified by the exif standard. A tag definition can be looked up with a constant that is a
+ * combination of TID and IFD. This definition has information about the type, number of components,
+ * and valid IFDs for a tag.
+ *
+ * @see ExifTag
+ */
+public class ExifInterface {
+ private static final int IFD_NULL = -1;
+ static final int DEFINITION_NULL = 0;
+
+ /** Tag constants for Jeita EXIF 2.2 */
+ // IFD 0
+ public static final int TAG_ORIENTATION = defineTag(IfdId.TYPE_IFD_0, (short) 0x0112);
+
+ static final int TAG_EXIF_IFD = defineTag(IfdId.TYPE_IFD_0, (short) 0x8769);
+ static final int TAG_GPS_IFD = defineTag(IfdId.TYPE_IFD_0, (short) 0x8825);
+ static final int TAG_STRIP_OFFSETS = defineTag(IfdId.TYPE_IFD_0, (short) 0x0111);
+ static final int TAG_STRIP_BYTE_COUNTS = defineTag(IfdId.TYPE_IFD_0, (short) 0x0117);
+ // IFD 1
+ static final int TAG_JPEG_INTERCHANGE_FORMAT = defineTag(IfdId.TYPE_IFD_1, (short) 0x0201);
+ static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = defineTag(IfdId.TYPE_IFD_1, (short) 0x0202);
+ // IFD Exif Tags
+ static final int TAG_INTEROPERABILITY_IFD = defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005);
+
+ /** Tags that contain offset markers. These are included in the banned defines. */
+ private static HashSet<Short> sOffsetTags = new HashSet<>();
+
+ static {
+ sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT));
+ sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS));
+ }
+
+ private static final String NULL_ARGUMENT_STRING = "Argument is null";
+
+ private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
+
+ private ExifData mData = new ExifData();
+
+ @SuppressLint("SimpleDateFormat")
+ public ExifInterface() {
+ DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
+ mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ /**
+ * Reads the exif tags from a byte array, clearing this ExifInterface object's existing exif tags.
+ *
+ * @param jpeg a byte array containing a jpeg compressed image.
+ * @throws java.io.IOException
+ */
+ public void readExif(byte[] jpeg) throws IOException {
+ readExif(new ByteArrayInputStream(jpeg));
+ }
+
+ /**
+ * Reads the exif tags from an InputStream, clearing this ExifInterface object's existing exif
+ * tags.
+ *
+ * @param inStream an InputStream containing a jpeg compressed image.
+ * @throws java.io.IOException
+ */
+ private void readExif(InputStream inStream) throws IOException {
+ if (inStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ ExifData d;
+ try {
+ d = new ExifReader(this).read(inStream);
+ } catch (ExifInvalidFormatException e) {
+ throw new IOException("Invalid exif format : " + e);
+ }
+ mData = d;
+ }
+
+ /** Returns the TID for a tag constant. */
+ static short getTrueTagKey(int tag) {
+ // Truncate
+ return (short) tag;
+ }
+
+ /** Returns the constant representing a tag with a given TID and default IFD. */
+ private static int defineTag(int ifdId, short tagId) {
+ return (tagId & 0x0000ffff) | (ifdId << 16);
+ }
+
+ static boolean isIfdAllowed(int info, int ifd) {
+ int[] ifds = IfdData.getIfds();
+ int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+ for (int i = 0; i < ifds.length; i++) {
+ if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int getAllowedIfdFlagsFromInfo(int info) {
+ return info >>> 24;
+ }
+
+ /**
+ * Returns true if tag TID is one of the following: {@code TAG_EXIF_IFD}, {@code TAG_GPS_IFD},
+ * {@code TAG_JPEG_INTERCHANGE_FORMAT}, {@code TAG_STRIP_OFFSETS}, {@code
+ * TAG_INTEROPERABILITY_IFD}
+ *
+ * <p>Note: defining tags with these TID's is disallowed.
+ *
+ * @param tag a tag's TID (can be obtained from a defined tag constant with {@link
+ * #getTrueTagKey}).
+ * @return true if the TID is that of an offset tag.
+ */
+ static boolean isOffsetTag(short tag) {
+ return sOffsetTags.contains(tag);
+ }
+
+ private SparseIntArray mTagInfo = null;
+
+ SparseIntArray getTagInfo() {
+ if (mTagInfo == null) {
+ mTagInfo = new SparseIntArray();
+ initTagInfo();
+ }
+ return mTagInfo;
+ }
+
+ private void initTagInfo() {
+ /**
+ * We put tag information in a 4-bytes integer. The first byte a bitmask representing the
+ * allowed IFDs of the tag, the second byte is the data type, and the last two byte are a short
+ * value indicating the default component count of this tag.
+ */
+ // IFD0 tags
+ int[] ifdAllowedIfds = {IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1};
+ int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24;
+ mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16);
+ mTagInfo.put(ExifInterface.TAG_EXIF_IFD, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_IFD, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16);
+ // IFD1 tags
+ int[] ifd1AllowedIfds = {IfdId.TYPE_IFD_1};
+ int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24;
+ mTagInfo.put(
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ // Exif tags
+ int[] exifAllowedIfds = {IfdId.TYPE_IFD_EXIF};
+ int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24;
+ mTagInfo.put(
+ ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ }
+
+ private static int getFlagsFromAllowedIfds(int[] allowedIfds) {
+ if (allowedIfds == null || allowedIfds.length == 0) {
+ return 0;
+ }
+ int flags = 0;
+ int[] ifds = IfdData.getIfds();
+ for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+ for (int j : allowedIfds) {
+ if (ifds[i] == j) {
+ flags |= 1 << i;
+ break;
+ }
+ }
+ }
+ return flags;
+ }
+
+ private Integer getTagIntValue(int tagId, int ifdId) {
+ int[] l = getTagIntValues(tagId, ifdId);
+ if (l == null || l.length <= 0) {
+ return null;
+ }
+ return l[0];
+ }
+
+ private int[] getTagIntValues(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsInts();
+ }
+
+ /** Gets an ExifTag for an IFD other than the tag's default. */
+ public ExifTag getTag(int tagId, int ifdId) {
+ if (!ExifTag.isValidIfd(ifdId)) {
+ return null;
+ }
+ return mData.getTag(getTrueTagKey(tagId), ifdId);
+ }
+
+ public Integer getTagIntValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagIntValue(tagId, ifdId);
+ }
+
+ /**
+ * Gets the default IFD for a tag.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_EXIF_IFD}.
+ * @return the default IFD for a tag definition or {@link #IFD_NULL} if no definition exists.
+ */
+ private int getDefinedTagDefaultIfd(int tagId) {
+ int info = getTagInfo().get(tagId);
+ if (info == DEFINITION_NULL) {
+ return IFD_NULL;
+ }
+ return getTrueIfd(tagId);
+ }
+
+ /** Returns the default IFD for a tag constant. */
+ private static int getTrueIfd(int tag) {
+ return tag >>> 16;
+ }
+
+ /**
+ * Constants for {@code TAG_ORIENTATION}. They can be interpreted as follows:
+ *
+ * <ul>
+ * <li>TOP_LEFT is the normal orientation.
+ * <li>TOP_RIGHT is a left-right mirror.
+ * <li>BOTTOM_LEFT is a 180 degree rotation.
+ * <li>BOTTOM_RIGHT is a top-bottom mirror.
+ * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.
+ * <li>RIGHT_TOP is a 90 degree clockwise rotation.
+ * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.
+ * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.
+ * </ul>
+ */
+ interface Orientation {
+ short TOP_LEFT = 1;
+ short TOP_RIGHT = 2;
+ short BOTTOM_LEFT = 3;
+ short BOTTOM_RIGHT = 4;
+ short LEFT_TOP = 5;
+ short RIGHT_TOP = 6;
+ short LEFT_BOTTOM = 7;
+ short RIGHT_BOTTOM = 8;
+ }
+
+ /** Wrapper class to define some orientation parameters. */
+ public static class OrientationParams {
+ int rotation = 0;
+ int scaleX = 1;
+ int scaleY = 1;
+ public boolean invertDimensions = false;
+ }
+
+ public static OrientationParams getOrientationParams(int orientation) {
+ OrientationParams params = new OrientationParams();
+ switch (orientation) {
+ case Orientation.TOP_RIGHT: // Flip horizontal
+ params.scaleX = -1;
+ break;
+ case Orientation.BOTTOM_RIGHT: // Flip vertical
+ params.scaleY = -1;
+ break;
+ case Orientation.BOTTOM_LEFT: // Rotate 180
+ params.rotation = 180;
+ break;
+ case Orientation.RIGHT_BOTTOM: // Rotate 270
+ params.rotation = 270;
+ params.invertDimensions = true;
+ break;
+ case Orientation.RIGHT_TOP: // Rotate 90
+ params.rotation = 90;
+ params.invertDimensions = true;
+ break;
+ case Orientation.LEFT_TOP: // Transpose
+ params.rotation = 90;
+ params.scaleX = -1;
+ params.invertDimensions = true;
+ break;
+ case Orientation.LEFT_BOTTOM: // Transverse
+ params.rotation = 270;
+ params.scaleX = -1;
+ params.invertDimensions = true;
+ break;
+ }
+ return params;
+ }
+
+ /** Clears this ExifInterface object's existing exif tags. */
+ public void clearExif() {
+ mData = new ExifData();
+ }
+
+ /**
+ * Puts an ExifTag into this ExifInterface object's tags, removing a previous ExifTag with the
+ * same TID and IFD. The IFD it is put into will be the one the tag was created with in {@link
+ * #buildTag}.
+ *
+ * @param tag an ExifTag to put into this ExifInterface's tags.
+ * @return the previous ExifTag with the same TID and IFD or null if none exists.
+ */
+ public ExifTag setTag(ExifTag tag) {
+ return mData.addTag(tag);
+ }
+
+ /**
+ * Returns the ExifTag in that tag's default IFD for a defined tag constant or null if none
+ * exists.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_EXIF_IFD}.
+ * @return an {@link ExifTag} or null if none exists.
+ */
+ public ExifTag getTag(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTag(tagId, ifdId);
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg compressed bitmap, removing prior
+ * exif tags.
+ *
+ * @param bmap a bitmap to compress and write exif into.
+ * @param exifOutStream the OutputStream to which the jpeg image with added exif tags will be
+ * written.
+ * @throws java.io.IOException
+ */
+ public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException {
+ if (bmap == null || exifOutStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ bmap.compress(Bitmap.CompressFormat.JPEG, 90, exifOutStream);
+ exifOutStream.flush();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java b/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java
new file mode 100644
index 0000000..92449d7
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2012 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.callcomposer.camera.exif;
+
+/** Exception for invalid exif formats. */
+public class ExifInvalidFormatException extends Exception {
+ ExifInvalidFormatException(String meg) {
+ super(meg);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java b/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java
new file mode 100644
index 0000000..23d748c
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java
@@ -0,0 +1,846 @@
+/*
+ * Copyright (C) 2012 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.callcomposer.camera.exif;
+
+import android.annotation.SuppressLint;
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * This class provides a low-level EXIF parsing API. Given a JPEG format InputStream, the caller can
+ * request which IFD's to read via {@link #parse(java.io.InputStream, int)} with given options.
+ *
+ * <p>Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the parser.
+ *
+ * <pre>
+ * void parse() {
+ * ExifParser parser = ExifParser.parse(mImageInputStream,
+ * ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
+ * int event = parser.next();
+ * while (event != ExifParser.EVENT_END) {
+ * switch (event) {
+ * case ExifParser.EVENT_START_OF_IFD:
+ * break;
+ * case ExifParser.EVENT_NEW_TAG:
+ * ExifTag tag = parser.getTag();
+ * if (!tag.hasValue()) {
+ * parser.registerForTagValue(tag);
+ * } else {
+ * processTag(tag);
+ * }
+ * break;
+ * case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ * tag = parser.getTag();
+ * if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ * processTag(tag);
+ * }
+ * break;
+ * }
+ * event = parser.next();
+ * }
+ * }
+ *
+ * void processTag(ExifTag tag) {
+ * // process the tag as you like.
+ * }
+ * </pre>
+ */
+public class ExifParser {
+ private static final boolean LOGV = false;
+ /**
+ * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to know which IFD we are
+ * in.
+ */
+ static final int EVENT_START_OF_IFD = 0;
+ /** When the parser reaches a new tag. Call {@link #getTag()}to get the corresponding tag. */
+ static final int EVENT_NEW_TAG = 1;
+ /**
+ * When the parser reaches the value area of tag that is registered by {@link
+ * #registerForTagValue(ExifTag)} previously. Call {@link #getTag()} to get the corresponding tag.
+ */
+ static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
+
+ /** When the parser reaches the compressed image area. */
+ static final int EVENT_COMPRESSED_IMAGE = 3;
+ /**
+ * When the parser reaches the uncompressed image strip. Call {@link #getStripIndex()} to get the
+ * index of the strip.
+ *
+ * @see #getStripIndex()
+ */
+ static final int EVENT_UNCOMPRESSED_STRIP = 4;
+ /** When there is nothing more to parse. */
+ static final int EVENT_END = 5;
+
+ /** Option bit to request to parse IFD0. */
+ private static final int OPTION_IFD_0 = 1;
+ /** Option bit to request to parse IFD1. */
+ private static final int OPTION_IFD_1 = 1 << 1;
+ /** Option bit to request to parse Exif-IFD. */
+ private static final int OPTION_IFD_EXIF = 1 << 2;
+ /** Option bit to request to parse GPS-IFD. */
+ private static final int OPTION_IFD_GPS = 1 << 3;
+ /** Option bit to request to parse Interoperability-IFD. */
+ private static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
+ /** Option bit to request to parse thumbnail. */
+ private static final int OPTION_THUMBNAIL = 1 << 5;
+
+ private static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
+ private static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
+
+ // TIFF header
+ private static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
+ private static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
+ private static final short TIFF_HEADER_TAIL = 0x002A;
+
+ private static final int TAG_SIZE = 12;
+ private static final int OFFSET_SIZE = 2;
+
+ private static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+ private static final int DEFAULT_IFD0_OFFSET = 8;
+
+ private final CountedDataInputStream mTiffStream;
+ private final int mOptions;
+ private int mIfdStartOffset = 0;
+ private int mNumOfTagInIfd = 0;
+ private int mIfdType;
+ private ExifTag mTag;
+ private ImageEvent mImageEvent;
+ private ExifTag mStripSizeTag;
+ private ExifTag mJpegSizeTag;
+ private boolean mNeedToParseOffsetsInCurrentIfd;
+ private boolean mContainExifData = false;
+ private int mApp1End;
+ private byte[] mDataAboveIfd0;
+ private int mIfd0Position;
+ private final ExifInterface mInterface;
+
+ private static final short TAG_EXIF_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
+ private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
+ private static final short TAG_INTEROPERABILITY_IFD =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+ private static final short TAG_STRIP_OFFSETS =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
+ private static final short TAG_STRIP_BYTE_COUNTS =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+
+ private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<>();
+
+ private boolean isIfdRequested(int ifdType) {
+ switch (ifdType) {
+ case IfdId.TYPE_IFD_0:
+ return (mOptions & OPTION_IFD_0) != 0;
+ case IfdId.TYPE_IFD_1:
+ return (mOptions & OPTION_IFD_1) != 0;
+ case IfdId.TYPE_IFD_EXIF:
+ return (mOptions & OPTION_IFD_EXIF) != 0;
+ case IfdId.TYPE_IFD_GPS:
+ return (mOptions & OPTION_IFD_GPS) != 0;
+ case IfdId.TYPE_IFD_INTEROPERABILITY:
+ return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
+ }
+ return false;
+ }
+
+ private boolean isThumbnailRequested() {
+ return (mOptions & OPTION_THUMBNAIL) != 0;
+ }
+
+ private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ if (inputStream == null) {
+ throw new IOException("Null argument inputStream to ExifParser");
+ }
+ if (LOGV) {
+ LogUtil.v("ExifParser.ExifParser", "Reading exif...");
+ }
+ mInterface = iRef;
+ mContainExifData = seekTiffData(inputStream);
+ mTiffStream = new CountedDataInputStream(inputStream);
+ mOptions = options;
+ if (!mContainExifData) {
+ return;
+ }
+
+ parseTiffHeader();
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("Invalid offset " + offset);
+ }
+ mIfd0Position = (int) offset;
+ mIfdType = IfdId.TYPE_IFD_0;
+ if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
+ registerIfd(IfdId.TYPE_IFD_0, offset);
+ if (offset != DEFAULT_IFD0_OFFSET) {
+ mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
+ read(mDataAboveIfd0);
+ }
+ }
+ }
+
+ /**
+ * Parses the the given InputStream with the given options
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ */
+ protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(inputStream, options, iRef);
+ }
+
+ /**
+ * Parses the the given InputStream with default options; that is, every IFD and thumbnaill will
+ * be parsed.
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ * @see #parse(java.io.InputStream, int, ExifInterface)
+ */
+ protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(
+ inputStream,
+ OPTION_IFD_0
+ | OPTION_IFD_1
+ | OPTION_IFD_EXIF
+ | OPTION_IFD_GPS
+ | OPTION_IFD_INTEROPERABILITY
+ | OPTION_THUMBNAIL,
+ iRef);
+ }
+
+ /**
+ * Moves the parser forward and returns the next parsing event
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ * @see #EVENT_START_OF_IFD
+ * @see #EVENT_NEW_TAG
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ * @see #EVENT_COMPRESSED_IMAGE
+ * @see #EVENT_UNCOMPRESSED_STRIP
+ * @see #EVENT_END
+ */
+ protected int next() throws IOException, ExifInvalidFormatException {
+ if (!mContainExifData) {
+ return EVENT_END;
+ }
+ int offset = mTiffStream.getReadByteCount();
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ if (offset < endOfTags) {
+ mTag = readTag();
+ if (mTag == null) {
+ return next();
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ checkOffsetOrImageTag(mTag);
+ }
+ return EVENT_NEW_TAG;
+ } else if (offset == endOfTags) {
+ // There is a link to ifd1 at the end of ifd0
+ if (mIfdType == IfdId.TYPE_IFD_0) {
+ long ifdOffset = readUnsignedLong();
+ if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
+ if (ifdOffset != 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ } else {
+ int offsetSize = 4;
+ // Some camera models use invalid length of the offset
+ if (mCorrespondingEvent.size() > 0) {
+ offsetSize = mCorrespondingEvent.firstEntry().getKey() - mTiffStream.getReadByteCount();
+ }
+ if (offsetSize < 4) {
+ LogUtil.i("ExifParser.next", "Invalid size of link to next IFD: " + offsetSize);
+ } else {
+ long ifdOffset = readUnsignedLong();
+ if (ifdOffset != 0) {
+ LogUtil.i("ExifParser.next", "Invalid link to next IFD: " + ifdOffset);
+ }
+ }
+ }
+ }
+ while (mCorrespondingEvent.size() != 0) {
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ Object event = entry.getValue();
+ try {
+ skipTo(entry.getKey());
+ } catch (IOException e) {
+ LogUtil.i(
+ "ExifParser.next",
+ "Failed to skip to data at: "
+ + entry.getKey()
+ + " for "
+ + event.getClass().getName()
+ + ", the file may be broken.");
+ continue;
+ }
+ if (event instanceof IfdEvent) {
+ mIfdType = ((IfdEvent) event).ifd;
+ mNumOfTagInIfd = mTiffStream.readUnsignedShort();
+ mIfdStartOffset = entry.getKey();
+
+ if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
+ LogUtil.i("ExifParser.next", "Invalid size of IFD " + mIfdType);
+ return EVENT_END;
+ }
+
+ mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
+ if (((IfdEvent) event).isRequested) {
+ return EVENT_START_OF_IFD;
+ } else {
+ skipRemainingTagsInCurrentIfd();
+ }
+ } else if (event instanceof ImageEvent) {
+ mImageEvent = (ImageEvent) event;
+ return mImageEvent.type;
+ } else {
+ ExifTagEvent tagEvent = (ExifTagEvent) event;
+ mTag = tagEvent.tag;
+ if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ readFullTagValue(mTag);
+ checkOffsetOrImageTag(mTag);
+ }
+ if (tagEvent.isRequested) {
+ return EVENT_VALUE_OF_REGISTERED_TAG;
+ }
+ }
+ }
+ return EVENT_END;
+ }
+
+ /**
+ * Skips the tags area of current IFD, if the parser is not in the tag area, nothing will happen.
+ *
+ * @throws java.io.IOException
+ * @throws ExifInvalidFormatException
+ */
+ private void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ int offset = mTiffStream.getReadByteCount();
+ if (offset > endOfTags) {
+ return;
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ while (offset < endOfTags) {
+ mTag = readTag();
+ offset += TAG_SIZE;
+ if (mTag == null) {
+ continue;
+ }
+ checkOffsetOrImageTag(mTag);
+ }
+ } else {
+ skipTo(endOfTags);
+ }
+ long ifdOffset = readUnsignedLong();
+ // For ifd0, there is a link to ifd1 in the end of all tags
+ if (mIfdType == IfdId.TYPE_IFD_0
+ && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
+ if (ifdOffset > 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ }
+
+ private boolean needToParseOffsetsInCurrentIfd() {
+ switch (mIfdType) {
+ case IfdId.TYPE_IFD_0:
+ return isIfdRequested(IfdId.TYPE_IFD_EXIF)
+ || isIfdRequested(IfdId.TYPE_IFD_GPS)
+ || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
+ || isIfdRequested(IfdId.TYPE_IFD_1);
+ case IfdId.TYPE_IFD_1:
+ return isThumbnailRequested();
+ case IfdId.TYPE_IFD_EXIF:
+ // The offset to interoperability IFD is located in Exif IFD
+ return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * If {@link #next()} return {@link #EVENT_NEW_TAG} or {@link #EVENT_VALUE_OF_REGISTERED_TAG},
+ * call this function to get the corresponding tag.
+ *
+ * <p>For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size of the value is
+ * greater than 4 bytes. One should call {@link ExifTag#hasValue()} to check if the tag contains
+ * value. If there is no value,call {@link #registerForTagValue(ExifTag)} to have the parser emit
+ * {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area pointed by the offset.
+ *
+ * <p>When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the tag will have
+ * already been read except for tags of undefined type. For tags of undefined type, call one of
+ * the read methods to get the value.
+ *
+ * @see #registerForTagValue(ExifTag)
+ * @see #read(byte[])
+ * @see #read(byte[], int, int)
+ * @see #readLong()
+ * @see #readRational()
+ * @see #readString(int)
+ * @see #readString(int, java.nio.charset.Charset)
+ */
+ protected ExifTag getTag() {
+ return mTag;
+ }
+
+ /**
+ * Gets the ID of current IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ * @see IfdId#TYPE_IFD_EXIF
+ */
+ int getCurrentIfd() {
+ return mIfdType;
+ }
+
+ /**
+ * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the index of this
+ * strip.
+ */
+ int getStripIndex() {
+ return mImageEvent.stripIndex;
+ }
+
+ /** When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the strip size. */
+ int getStripSize() {
+ if (mStripSizeTag == null) {
+ return 0;
+ }
+ return (int) mStripSizeTag.getValueAt(0);
+ }
+
+ /**
+ * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get the image data size.
+ */
+ int getCompressedImageSize() {
+ if (mJpegSizeTag == null) {
+ return 0;
+ }
+ return (int) mJpegSizeTag.getValueAt(0);
+ }
+
+ private void skipTo(int offset) throws IOException {
+ mTiffStream.skipTo(offset);
+ while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
+ mCorrespondingEvent.pollFirstEntry();
+ }
+ }
+
+ /**
+ * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may not contain the value
+ * if the size of the value is greater than 4 bytes. When the value is not available here, call
+ * this method so that the parser will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches
+ * the area where the value is located.
+ *
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ */
+ void registerForTagValue(ExifTag tag) {
+ if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
+ }
+ }
+
+ private void registerIfd(int ifdType, long offset) {
+ // Cast unsigned int to int since the offset is always smaller
+ // than the size of APP1 (65536)
+ mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
+ }
+
+ private void registerCompressedImage(long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
+ }
+
+ private void registerUncompressedStrip(int stripIndex, long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP, stripIndex));
+ }
+
+ @SuppressLint("DefaultLocale")
+ private ExifTag readTag() throws IOException, ExifInvalidFormatException {
+ short tagId = mTiffStream.readShort();
+ short dataFormat = mTiffStream.readShort();
+ long numOfComp = mTiffStream.readUnsignedInt();
+ if (numOfComp > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("Number of component is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid image file contains invalid data type. Ignore those tags
+ if (!ExifTag.isValidType(dataFormat)) {
+ LogUtil.i("ExifParser.readTag", "Tag %04x: Invalid data type %d", tagId, dataFormat);
+ mTiffStream.skip(4);
+ return null;
+ }
+ // TODO: handle numOfComp overflow
+ ExifTag tag =
+ new ExifTag(
+ tagId,
+ dataFormat,
+ (int) numOfComp,
+ mIfdType,
+ ((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
+ int dataSize = tag.getDataSize();
+ if (dataSize > 4) {
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("offset is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid images put some undefined data before IFD0.
+ // Read the data here.
+ if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
+ byte[] buf = new byte[(int) numOfComp];
+ System.arraycopy(
+ mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET, buf, 0, (int) numOfComp);
+ tag.setValue(buf);
+ } else {
+ tag.setOffset((int) offset);
+ }
+ } else {
+ boolean defCount = tag.hasDefinedCount();
+ // Set defined count to 0 so we can add \0 to non-terminated strings
+ tag.setHasDefinedCount(false);
+ // Read value
+ readFullTagValue(tag);
+ tag.setHasDefinedCount(defCount);
+ mTiffStream.skip(4 - dataSize);
+ // Set the offset to the position of value.
+ tag.setOffset(mTiffStream.getReadByteCount() - 4);
+ }
+ return tag;
+ }
+
+ /**
+ * Check the if the tag is one of the offset tag that points to the IFD or image the caller is
+ * interested in, register the IFD or image.
+ */
+ private void checkOffsetOrImageTag(ExifTag tag) {
+ // Some invalid formattd image contains tag with 0 size.
+ if (tag.getComponentCount() == 0) {
+ return;
+ }
+ short tid = tag.getTagId();
+ int ifd = tag.getIfd();
+ if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
+ registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_INTEROPERABILITY_IFD
+ && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
+ if (isThumbnailRequested()) {
+ registerCompressedImage(tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
+ if (isThumbnailRequested()) {
+ mJpegSizeTag = tag;
+ }
+ } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
+ if (isThumbnailRequested()) {
+ if (tag.hasValue()) {
+ for (int i = 0; i < tag.getComponentCount(); i++) {
+ if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ } else {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ }
+ }
+ } else {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
+ }
+ }
+ } else if (tid == TAG_STRIP_BYTE_COUNTS
+ && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
+ && isThumbnailRequested()
+ && tag.hasValue()) {
+ mStripSizeTag = tag;
+ }
+ }
+
+ private boolean checkAllowed(int ifd, int tagId) {
+ int info = mInterface.getTagInfo().get(tagId);
+ return info != ExifInterface.DEFINITION_NULL && ExifInterface.isIfdAllowed(info, ifd);
+ }
+
+ void readFullTagValue(ExifTag tag) throws IOException {
+ // Some invalid images contains tags with wrong size, check it here
+ short type = tag.getDataType();
+ if (type == ExifTag.TYPE_ASCII
+ || type == ExifTag.TYPE_UNDEFINED
+ || type == ExifTag.TYPE_UNSIGNED_BYTE) {
+ int size = tag.getComponentCount();
+ if (mCorrespondingEvent.size() > 0) {
+ if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount() + size) {
+ Object event = mCorrespondingEvent.firstEntry().getValue();
+ if (event instanceof ImageEvent) {
+ // Tag value overlaps thumbnail, ignore thumbnail.
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Thumbnail overlaps value for tag: \n" + tag.toString());
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ LogUtil.i("ExifParser.readFullTagValue", "Invalid thumbnail offset: " + entry.getKey());
+ } else {
+ // Tag value overlaps another shorten count
+ if (event instanceof IfdEvent) {
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Ifd " + ((IfdEvent) event).ifd + " overlaps value for tag: \n" + tag.toString());
+ } else if (event instanceof ExifTagEvent) {
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Tag value for tag: \n"
+ + ((ExifTagEvent) event).tag.toString()
+ + " overlaps value for tag: \n"
+ + tag.toString());
+ }
+ size = mCorrespondingEvent.firstEntry().getKey() - mTiffStream.getReadByteCount();
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Invalid size of tag: \n" + tag.toString() + " setting count to: " + size);
+ tag.forceSetComponentCount(size);
+ }
+ }
+ }
+ }
+ switch (tag.getDataType()) {
+ case ExifTag.TYPE_UNSIGNED_BYTE:
+ case ExifTag.TYPE_UNDEFINED:
+ {
+ byte[] buf = new byte[tag.getComponentCount()];
+ read(buf);
+ tag.setValue(buf);
+ }
+ break;
+ case ExifTag.TYPE_ASCII:
+ tag.setValue(readString(tag.getComponentCount()));
+ break;
+ case ExifTag.TYPE_UNSIGNED_LONG:
+ {
+ long[] value = new long[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_RATIONAL:
+ {
+ Rational[] value = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_SHORT:
+ {
+ int[] value = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedShort();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_LONG:
+ {
+ int[] value = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_RATIONAL:
+ {
+ Rational[] value = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ }
+ if (LOGV) {
+ LogUtil.v("ExifParser.readFullTagValue", "\n" + tag.toString());
+ }
+ }
+
+ private void parseTiffHeader() throws IOException, ExifInvalidFormatException {
+ short byteOrder = mTiffStream.readShort();
+ if (LITTLE_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ } else if (BIG_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ } else {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+
+ if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+ }
+
+ private boolean seekTiffData(InputStream inputStream)
+ throws IOException, ExifInvalidFormatException {
+ CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
+ if (dataStream.readShort() != JpegHeader.SOI) {
+ throw new ExifInvalidFormatException("Invalid JPEG format");
+ }
+
+ short marker = dataStream.readShort();
+ while (marker != JpegHeader.EOI && !JpegHeader.isSofMarker(marker)) {
+ int length = dataStream.readUnsignedShort();
+ // Some invalid formatted image contains multiple APP1,
+ // try to find the one with Exif data.
+ if (marker == JpegHeader.APP1) {
+ int header;
+ short headerTail;
+ if (length >= 8) {
+ header = dataStream.readInt();
+ headerTail = dataStream.readShort();
+ length -= 6;
+ if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
+ mApp1End = length;
+ return true;
+ }
+ }
+ }
+ if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
+ LogUtil.i("ExifParser.seekTiffData", "Invalid JPEG format.");
+ return false;
+ }
+ marker = dataStream.readShort();
+ }
+ return false;
+ }
+
+ /** Reads bytes from the InputStream. */
+ protected int read(byte[] buffer, int offset, int length) throws IOException {
+ return mTiffStream.read(buffer, offset, length);
+ }
+
+ /** Equivalent to read(buffer, 0, buffer.length). */
+ protected int read(byte[] buffer) throws IOException {
+ return mTiffStream.read(buffer);
+ }
+
+ /**
+ * Reads a String from the InputStream with US-ASCII charset. The parser will read n bytes and
+ * convert it to ascii string. This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ private String readString(int n) throws IOException {
+ return readString(n, US_ASCII);
+ }
+
+ /**
+ * Reads a String from the InputStream with the given charset. The parser will read n bytes and
+ * convert it to string. This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ private String readString(int n, Charset charset) throws IOException {
+ if (n > 0) {
+ return mTiffStream.readString(n, charset);
+ } else {
+ return "";
+ }
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the InputStream. */
+ private int readUnsignedShort() throws IOException {
+ return mTiffStream.readShort() & 0xffff;
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the InputStream. */
+ private long readUnsignedLong() throws IOException {
+ return readLong() & 0xffffffffL;
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the InputStream. */
+ private Rational readUnsignedRational() throws IOException {
+ long nomi = readUnsignedLong();
+ long denomi = readUnsignedLong();
+ return new Rational(nomi, denomi);
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream. */
+ private int readLong() throws IOException {
+ return mTiffStream.readInt();
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream. */
+ private Rational readRational() throws IOException {
+ int nomi = readLong();
+ int denomi = readLong();
+ return new Rational(nomi, denomi);
+ }
+
+ private static class ImageEvent {
+ int stripIndex;
+ int type;
+
+ ImageEvent(int type) {
+ this.stripIndex = 0;
+ this.type = type;
+ }
+
+ ImageEvent(int type, int stripIndex) {
+ this.type = type;
+ this.stripIndex = stripIndex;
+ }
+ }
+
+ private static class IfdEvent {
+ int ifd;
+ boolean isRequested;
+
+ IfdEvent(int ifd, boolean isInterestedIfd) {
+ this.ifd = ifd;
+ this.isRequested = isInterestedIfd;
+ }
+ }
+
+ private static class ExifTagEvent {
+ ExifTag tag;
+ boolean isRequested;
+
+ ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
+ this.tag = tag;
+ this.isRequested = isRequireByUser;
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java b/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java
new file mode 100644
index 0000000..89d2126
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java
@@ -0,0 +1,81 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** This class reads the EXIF header of a JPEG file and stores it in {@link ExifData}. */
+class ExifReader {
+
+ private final ExifInterface mInterface;
+
+ ExifReader(ExifInterface iRef) {
+ mInterface = iRef;
+ }
+
+ /**
+ * Parses the inputStream and and returns the EXIF data in an {@link ExifData}.
+ *
+ * @throws ExifInvalidFormatException
+ * @throws java.io.IOException
+ */
+ protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException, IOException {
+ ExifParser parser = ExifParser.parse(inputStream, mInterface);
+ ExifData exifData = new ExifData();
+ ExifTag tag;
+
+ int event = parser.next();
+ while (event != ExifParser.EVENT_END) {
+ switch (event) {
+ case ExifParser.EVENT_START_OF_IFD:
+ exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
+ break;
+ case ExifParser.EVENT_NEW_TAG:
+ tag = parser.getTag();
+ if (!tag.hasValue()) {
+ parser.registerForTagValue(tag);
+ } else {
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ }
+ break;
+ case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ tag = parser.getTag();
+ if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
+ parser.readFullTagValue(tag);
+ }
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ break;
+ case ExifParser.EVENT_COMPRESSED_IMAGE:
+ byte[] buf = new byte[parser.getCompressedImageSize()];
+ if (buf.length != parser.read(buf)) {
+ LogUtil.i("ExifReader.read", "Failed to read the compressed thumbnail");
+ }
+ break;
+ case ExifParser.EVENT_UNCOMPRESSED_STRIP:
+ buf = new byte[parser.getStripSize()];
+ if (buf.length != parser.read(buf)) {
+ LogUtil.i("ExifReader.read", "Failed to read the strip bytes");
+ }
+ break;
+ }
+ event = parser.next();
+ }
+ return exifData;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java b/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java
new file mode 100644
index 0000000..a254ae9
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java
@@ -0,0 +1,619 @@
+/*
+ * Copyright (C) 2012 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.callcomposer.camera.exif;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This class stores information of an EXIF tag. For more information about defined EXIF tags,
+ * please read the Jeita EXIF 2.2 standard. Tags should be instantiated using {@link
+ * ExifInterface#buildTag}.
+ *
+ * @see ExifInterface
+ */
+public class ExifTag {
+ /** The BYTE type in the EXIF standard. An 8-bit unsigned integer. */
+ static final short TYPE_UNSIGNED_BYTE = 1;
+ /**
+ * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit ASCII code. The final
+ * byte is terminated with NULL.
+ */
+ static final short TYPE_ASCII = 2;
+ /** The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer */
+ static final short TYPE_UNSIGNED_SHORT = 3;
+ /** The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer */
+ static final short TYPE_UNSIGNED_LONG = 4;
+ /**
+ * The RATIONAL type of EXIF standard. It consists of two LONGs. The first one is the numerator
+ * and the second one expresses the denominator.
+ */
+ static final short TYPE_UNSIGNED_RATIONAL = 5;
+ /**
+ * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any value depending on the
+ * field definition.
+ */
+ static final short TYPE_UNDEFINED = 7;
+ /**
+ * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer (2's complement
+ * notation).
+ */
+ static final short TYPE_LONG = 9;
+ /**
+ * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first one is the numerator
+ * and the second one is the denominator.
+ */
+ static final short TYPE_RATIONAL = 10;
+
+ private static final Charset US_ASCII = Charset.forName("US-ASCII");
+ private static final int[] TYPE_TO_SIZE_MAP = new int[11];
+ private static final int UNSIGNED_SHORT_MAX = 65535;
+ private static final long UNSIGNED_LONG_MAX = 4294967295L;
+ private static final long LONG_MAX = Integer.MAX_VALUE;
+ private static final long LONG_MIN = Integer.MIN_VALUE;
+
+ static {
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8;
+ TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8;
+ }
+
+ static final int SIZE_UNDEFINED = 0;
+
+ // Exif TagId
+ private final short mTagId;
+ // Exif Tag Type
+ private final short mDataType;
+ // If tag has defined count
+ private boolean mHasDefinedDefaultComponentCount;
+ // Actual data count in tag (should be number of elements in value array)
+ private int mComponentCountActual;
+ // The ifd that this tag should be put in
+ private int mIfd;
+ // The value (array of elements of type Tag Type)
+ private Object mValue;
+ // Value offset in exif header.
+ private int mOffset;
+
+ /** Returns true if the given IFD is a valid IFD. */
+ static boolean isValidIfd(int ifdId) {
+ return ifdId == IfdId.TYPE_IFD_0
+ || ifdId == IfdId.TYPE_IFD_1
+ || ifdId == IfdId.TYPE_IFD_EXIF
+ || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY
+ || ifdId == IfdId.TYPE_IFD_GPS;
+ }
+
+ /** Returns true if a given type is a valid tag type. */
+ static boolean isValidType(short type) {
+ return type == TYPE_UNSIGNED_BYTE
+ || type == TYPE_ASCII
+ || type == TYPE_UNSIGNED_SHORT
+ || type == TYPE_UNSIGNED_LONG
+ || type == TYPE_UNSIGNED_RATIONAL
+ || type == TYPE_UNDEFINED
+ || type == TYPE_LONG
+ || type == TYPE_RATIONAL;
+ }
+
+ // Use builtTag in ExifInterface instead of constructor.
+ ExifTag(short tagId, short type, int componentCount, int ifd, boolean hasDefinedComponentCount) {
+ mTagId = tagId;
+ mDataType = type;
+ mComponentCountActual = componentCount;
+ mHasDefinedDefaultComponentCount = hasDefinedComponentCount;
+ mIfd = ifd;
+ mValue = null;
+ }
+
+ /**
+ * Gets the element size of the given data type in bytes.
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ private static int getElementSize(short type) {
+ return TYPE_TO_SIZE_MAP[type];
+ }
+
+ /**
+ * Returns the ID of the IFD this tag belongs to.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ int getIfd() {
+ return mIfd;
+ }
+
+ void setIfd(int ifdId) {
+ mIfd = ifdId;
+ }
+
+ /** Gets the TID of this tag. */
+ short getTagId() {
+ return mTagId;
+ }
+
+ /**
+ * Gets the data type of this tag
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ short getDataType() {
+ return mDataType;
+ }
+
+ /** Gets the total data size in bytes of the value of this tag. */
+ int getDataSize() {
+ return getComponentCount() * getElementSize(getDataType());
+ }
+
+ /** Gets the component count of this tag. */
+
+ // TODO: fix integer overflows with this
+ int getComponentCount() {
+ return mComponentCountActual;
+ }
+
+ /**
+ * Sets the component count of this tag. Call this function before setValue() if the length of
+ * value does not match the component count.
+ */
+ void forceSetComponentCount(int count) {
+ mComponentCountActual = count;
+ }
+
+ /**
+ * Returns true if this ExifTag contains value; otherwise, this tag will contain an offset value
+ * that is determined when the tag is written.
+ */
+ boolean hasValue() {
+ return mValue != null;
+ }
+
+ /**
+ * Sets integer values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_SHORT}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, {@link
+ * #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ boolean setValue(int[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_SHORT
+ && mDataType != TYPE_LONG
+ && mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
+ return false;
+ } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+
+ long[] data = new long[value.length];
+ for (int i = 0; i < value.length; i++) {
+ data[i] = value[i];
+ }
+ mValue = data;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets long values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_LONG}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ boolean setValue(long[] value) {
+ if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets a string value into this tag. This method should be used for tags of type {@link
+ * #TYPE_ASCII}. The string is converted to an ASCII string. Characters that cannot be converted
+ * are replaced with '?'. The length of the string must be equal to either (component count -1) or
+ * (component count). The final byte will be set to the string null terminator '\0', overwriting
+ * the last character in the string if the value.length is equal to the component count. This
+ * method will fail if:
+ *
+ * <ul>
+ * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.
+ * <li>The length of the string is not equal to (component count -1) or (component count) in the
+ * definition for this tag.
+ * </ul>
+ */
+ boolean setValue(String value) {
+ if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+
+ byte[] buf = value.getBytes(US_ASCII);
+ byte[] finalBuf = buf;
+ if (buf.length > 0) {
+ finalBuf =
+ (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED)
+ ? buf
+ : Arrays.copyOf(buf, buf.length + 1);
+ } else if (mDataType == TYPE_ASCII && mComponentCountActual == 1) {
+ finalBuf = new byte[] {0};
+ }
+ int count = finalBuf.length;
+ if (checkBadComponentCount(count)) {
+ return false;
+ }
+ mComponentCountActual = count;
+ mValue = finalBuf;
+ return true;
+ }
+
+ /**
+ * Sets Rational values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} or {@link
+ * #TYPE_RATIONAL}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ *
+ * @see Rational
+ */
+ boolean setValue(Rational[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
+ return false;
+ } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
+ return false;
+ }
+
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets byte values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or {@link
+ * #TYPE_UNDEFINED} .
+ * <li>The length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ private boolean setValue(byte[] value, int offset, int length) {
+ if (checkBadComponentCount(length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+ mValue = new byte[length];
+ System.arraycopy(value, offset, mValue, 0, length);
+ mComponentCountActual = length;
+ return true;
+ }
+
+ /** Equivalent to setValue(value, 0, value.length). */
+ boolean setValue(byte[] value) {
+ return setValue(value, 0, value.length);
+ }
+
+ /**
+ * Gets the value as an array of ints. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+ *
+ * @return the value as as an array of ints, or null if the tag's value does not exist or cannot
+ * be converted to an array of ints.
+ */
+ int[] getValueAsInts() {
+ if (mValue == null) {
+ return null;
+ } else if (mValue instanceof long[]) {
+ long[] val = (long[]) mValue;
+ int[] arr = new int[val.length];
+ for (int i = 0; i < val.length; i++) {
+ arr[i] = (int) val[i]; // Truncates
+ }
+ return arr;
+ }
+ return null;
+ }
+
+ /** Gets the tag's value or null if none exists. */
+ public Object getValue() {
+ return mValue;
+ }
+
+ /** Gets a string representation of the value. */
+ private String forceGetValueAsString() {
+ if (mValue == null) {
+ return "";
+ } else if (mValue instanceof byte[]) {
+ if (mDataType == TYPE_ASCII) {
+ return new String((byte[]) mValue, US_ASCII);
+ } else {
+ return Arrays.toString((byte[]) mValue);
+ }
+ } else if (mValue instanceof long[]) {
+ if (((long[]) mValue).length == 1) {
+ return String.valueOf(((long[]) mValue)[0]);
+ } else {
+ return Arrays.toString((long[]) mValue);
+ }
+ } else if (mValue instanceof Object[]) {
+ if (((Object[]) mValue).length == 1) {
+ Object val = ((Object[]) mValue)[0];
+ if (val == null) {
+ return "";
+ } else {
+ return val.toString();
+ }
+ } else {
+ return Arrays.toString((Object[]) mValue);
+ }
+ } else {
+ return mValue.toString();
+ }
+ }
+
+ /**
+ * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG}, {@link #TYPE_UNDEFINED},
+ * {@link #TYPE_UNSIGNED_BYTE}, {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}.
+ *
+ * @exception IllegalArgumentException if the data type is {@link #TYPE_RATIONAL} or {@link
+ * #TYPE_UNSIGNED_RATIONAL}.
+ */
+ long getValueAt(int index) {
+ if (mValue instanceof long[]) {
+ return ((long[]) mValue)[index];
+ } else if (mValue instanceof byte[]) {
+ return ((byte[]) mValue)[index];
+ }
+ throw new IllegalArgumentException(
+ "Cannot get integer value from " + convertTypeToString(mDataType));
+ }
+
+ /**
+ * Gets the {@link #TYPE_ASCII} data.
+ *
+ * @exception IllegalArgumentException If the type is NOT {@link #TYPE_ASCII}.
+ */
+ protected String getString() {
+ if (mDataType != TYPE_ASCII) {
+ throw new IllegalArgumentException(
+ "Cannot get ASCII value from " + convertTypeToString(mDataType));
+ }
+ return new String((byte[]) mValue, US_ASCII);
+ }
+
+ /**
+ * Gets the offset of this tag. This is only valid if this data size > 4 and contains an offset to
+ * the location of the actual value.
+ */
+ protected int getOffset() {
+ return mOffset;
+ }
+
+ /** Sets the offset of this tag. */
+ protected void setOffset(int offset) {
+ mOffset = offset;
+ }
+
+ void setHasDefinedCount(boolean d) {
+ mHasDefinedDefaultComponentCount = d;
+ }
+
+ boolean hasDefinedCount() {
+ return mHasDefinedDefaultComponentCount;
+ }
+
+ private boolean checkBadComponentCount(int count) {
+ return mHasDefinedDefaultComponentCount && (mComponentCountActual != count);
+ }
+
+ private static String convertTypeToString(short type) {
+ switch (type) {
+ case TYPE_UNSIGNED_BYTE:
+ return "UNSIGNED_BYTE";
+ case TYPE_ASCII:
+ return "ASCII";
+ case TYPE_UNSIGNED_SHORT:
+ return "UNSIGNED_SHORT";
+ case TYPE_UNSIGNED_LONG:
+ return "UNSIGNED_LONG";
+ case TYPE_UNSIGNED_RATIONAL:
+ return "UNSIGNED_RATIONAL";
+ case TYPE_UNDEFINED:
+ return "UNDEFINED";
+ case TYPE_LONG:
+ return "LONG";
+ case TYPE_RATIONAL:
+ return "RATIONAL";
+ default:
+ return "";
+ }
+ }
+
+ private boolean checkOverflowForUnsignedShort(int[] value) {
+ for (int v : value) {
+ if (v > UNSIGNED_SHORT_MAX || v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(long[] value) {
+ for (long v : value) {
+ if (v < 0 || v > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(int[] value) {
+ for (int v : value) {
+ if (v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < 0
+ || v.getDenominator() < 0
+ || v.getNumerator() > UNSIGNED_LONG_MAX
+ || v.getDenominator() > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < LONG_MIN
+ || v.getDenominator() < LONG_MIN
+ || v.getNumerator() > LONG_MAX
+ || v.getDenominator() > LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof ExifTag) {
+ ExifTag tag = (ExifTag) obj;
+ if (tag.mTagId != this.mTagId
+ || tag.mComponentCountActual != this.mComponentCountActual
+ || tag.mDataType != this.mDataType) {
+ return false;
+ }
+ if (mValue != null) {
+ if (tag.mValue == null) {
+ return false;
+ } else if (mValue instanceof long[]) {
+ if (!(tag.mValue instanceof long[])) {
+ return false;
+ }
+ return Arrays.equals((long[]) mValue, (long[]) tag.mValue);
+ } else if (mValue instanceof Rational[]) {
+ if (!(tag.mValue instanceof Rational[])) {
+ return false;
+ }
+ return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue);
+ } else if (mValue instanceof byte[]) {
+ if (!(tag.mValue instanceof byte[])) {
+ return false;
+ }
+ return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue);
+ } else {
+ return mValue.equals(tag.mValue);
+ }
+ } else {
+ return tag.mValue == null;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTagId,
+ mDataType,
+ mHasDefinedDefaultComponentCount,
+ mComponentCountActual,
+ mIfd,
+ mValue,
+ mOffset);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tag id: %04X\n", mTagId)
+ + "ifd id: "
+ + mIfd
+ + "\ntype: "
+ + convertTypeToString(mDataType)
+ + "\ncount: "
+ + mComponentCountActual
+ + "\noffset: "
+ + mOffset
+ + "\nvalue: "
+ + forceGetValueAsString()
+ + "\n";
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/IfdData.java b/java/com/android/dialer/callcomposer/camera/exif/IfdData.java
new file mode 100644
index 0000000..b808def
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/IfdData.java
@@ -0,0 +1,126 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This class stores all the tags in an IFD.
+ *
+ * @see ExifData
+ * @see ExifTag
+ */
+class IfdData {
+
+ private final int mIfdId;
+ private final Map<Short, ExifTag> mExifTags = new HashMap<>();
+ private static final int[] sIfds = {
+ IfdId.TYPE_IFD_0,
+ IfdId.TYPE_IFD_1,
+ IfdId.TYPE_IFD_EXIF,
+ IfdId.TYPE_IFD_INTEROPERABILITY,
+ IfdId.TYPE_IFD_GPS
+ };
+ /**
+ * Creates an IfdData with given IFD ID.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ IfdData(int ifdId) {
+ mIfdId = ifdId;
+ }
+
+ static int[] getIfds() {
+ return sIfds;
+ }
+
+ /** Get a array the contains all {@link ExifTag} in this IFD. */
+ private ExifTag[] getAllTags() {
+ return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
+ }
+
+ /**
+ * Gets the ID of this IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ protected int getId() {
+ return mIfdId;
+ }
+
+ /** Gets the {@link ExifTag} with given tag id. Return null if there is no such tag. */
+ protected ExifTag getTag(short tagId) {
+ return mExifTags.get(tagId);
+ }
+
+ /** Adds or replaces a {@link ExifTag}. */
+ protected ExifTag setTag(ExifTag tag) {
+ tag.setIfd(mIfdId);
+ return mExifTags.put(tag.getTagId(), tag);
+ }
+
+ /** Gets the tags count in the IFD. */
+ private int getTagCount() {
+ return mExifTags.size();
+ }
+
+ /**
+ * Returns true if all tags in this two IFDs are equal. Note that tags of IFDs offset or thumbnail
+ * offset will be ignored.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof IfdData) {
+ IfdData data = (IfdData) obj;
+ if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
+ ExifTag[] tags = data.getAllTags();
+ for (ExifTag tag : tags) {
+ if (ExifInterface.isOffsetTag(tag.getTagId())) {
+ continue;
+ }
+ ExifTag tag2 = mExifTags.get(tag.getTagId());
+ if (!tag.equals(tag2)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIfdId, mExifTags);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/IfdId.java b/java/com/android/dialer/callcomposer/camera/exif/IfdId.java
new file mode 100644
index 0000000..c615457
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/IfdId.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2012 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.callcomposer.camera.exif;
+
+/** The constants of the IFD ID defined in EXIF spec. */
+public interface IfdId {
+ int TYPE_IFD_0 = 0;
+ int TYPE_IFD_1 = 1;
+ int TYPE_IFD_EXIF = 2;
+ int TYPE_IFD_INTEROPERABILITY = 3;
+ int TYPE_IFD_GPS = 4;
+ /* This is used in ExifData to allocate enough IfdData */
+ int TYPE_IFD_COUNT = 5;
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java b/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java
new file mode 100644
index 0000000..3d98fcc
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java
@@ -0,0 +1,38 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+class JpegHeader {
+ static final short SOI = (short) 0xFFD8;
+ static final short APP1 = (short) 0xFFE1;
+ static final short EOI = (short) 0xFFD9;
+
+ /**
+ * SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG, and
+ * DAC marker.
+ */
+ private static final short SOF0 = (short) 0xFFC0;
+
+ private static final short SOF15 = (short) 0xFFCF;
+ private static final short DHT = (short) 0xFFC4;
+ private static final short JPG = (short) 0xFFC8;
+ private static final short DAC = (short) 0xFFCC;
+
+ static boolean isSofMarker(short marker) {
+ return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG && marker != DAC;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/Rational.java b/java/com/android/dialer/callcomposer/camera/exif/Rational.java
new file mode 100644
index 0000000..9afca84
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/Rational.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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.callcomposer.camera.exif;
+
+import java.util.Objects;
+
+/**
+ * The rational data type of EXIF tag. Contains a pair of longs representing the numerator and
+ * denominator of a Rational number.
+ */
+public class Rational {
+
+ private final long mNumerator;
+ private final long mDenominator;
+
+ /** Create a Rational with a given numerator and denominator. */
+ Rational(long nominator, long denominator) {
+ mNumerator = nominator;
+ mDenominator = denominator;
+ }
+
+ /** Gets the numerator of the rational. */
+ long getNumerator() {
+ return mNumerator;
+ }
+
+ /** Gets the denominator of the rational */
+ long getDenominator() {
+ return mDenominator;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Rational) {
+ Rational data = (Rational) obj;
+ return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNumerator, mDenominator);
+ }
+
+ @Override
+ public String toString() {
+ return mNumerator + "/" + mDenominator;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml b/java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml
new file mode 100644
index 0000000..12694ee
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ 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 package="com.android.dialer.callcomposer.cameraui"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java b/java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java
new file mode 100644
index 0000000..85c64e4
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java
@@ -0,0 +1,107 @@
+/*
+ * 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.callcomposer.cameraui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.hardware.Camera;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import com.android.dialer.callcomposer.camera.CameraManager;
+import com.android.dialer.callcomposer.camera.HardwareCameraPreview;
+import com.android.dialer.callcomposer.camera.SoftwareCameraPreview;
+import com.android.dialer.common.LogUtil;
+
+/** Used to display the view of the camera. */
+public class CameraMediaChooserView extends FrameLayout {
+ private static final String STATE_CAMERA_INDEX = "camera_index";
+ private static final String STATE_SUPER = "super";
+
+ // True if we have at least queued an update to the view tree to support software rendering
+ // fallback
+ private boolean mIsSoftwareFallbackActive;
+
+ public CameraMediaChooserView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Bundle bundle = new Bundle();
+ bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState());
+ final int cameraIndex = CameraManager.get().getCameraIndex();
+ LogUtil.i("CameraMediaChooserView.onSaveInstanceState", "saving camera index:" + cameraIndex);
+ bundle.putInt(STATE_CAMERA_INDEX, cameraIndex);
+ return bundle;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ if (!(state instanceof Bundle)) {
+ return;
+ }
+
+ final Bundle bundle = (Bundle) state;
+ final int cameraIndex = bundle.getInt(STATE_CAMERA_INDEX);
+ super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER));
+
+ LogUtil.i(
+ "CameraMediaChooserView.onRestoreInstanceState", "restoring camera index:" + cameraIndex);
+ if (cameraIndex != -1) {
+ CameraManager.get().selectCameraByIndex(cameraIndex);
+ } else {
+ resetState();
+ }
+ }
+
+ public void resetState() {
+ CameraManager.get().selectCamera(Camera.CameraInfo.CAMERA_FACING_BACK);
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+ // If the canvas isn't hardware accelerated, we have to replace the HardwareCameraPreview
+ // with a SoftwareCameraPreview which supports software rendering
+ if (!canvas.isHardwareAccelerated() && !mIsSoftwareFallbackActive) {
+ mIsSoftwareFallbackActive = true;
+ // Post modifying the tree since we can't modify the view tree during a draw pass
+ post(
+ new Runnable() {
+ @Override
+ public void run() {
+ final HardwareCameraPreview cameraPreview =
+ (HardwareCameraPreview) findViewById(R.id.camera_preview);
+ if (cameraPreview == null) {
+ return;
+ }
+ final ViewGroup parent = ((ViewGroup) cameraPreview.getParent());
+ final int index = parent.indexOfChild(cameraPreview);
+ final SoftwareCameraPreview softwareCameraPreview =
+ new SoftwareCameraPreview(getContext());
+ // Be sure to remove the hardware view before adding the software view to
+ // prevent having 2 camera previews active at the same time
+ parent.removeView(cameraPreview);
+ parent.addView(softwareCameraPreview, index);
+ }
+ });
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png
new file mode 100644
index 0000000..b974c9f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png
new file mode 100644
index 0000000..9842758
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png
new file mode 100644
index 0000000..4ec9f75
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png
new file mode 100644
index 0000000..e2345dc
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png
new file mode 100644
index 0000000..3bab009
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml b/java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml
new file mode 100644
index 0000000..fda52c9
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml
@@ -0,0 +1,26 @@
+<?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
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:drawable="@color/background_item_grey_pressed"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@color/background_item_grey_pressed"
+ android:state_activated="true"/>
+ <item
+ android:drawable="@android:color/transparent"/>
+</selector>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
new file mode 100644
index 0000000..75401b1
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
@@ -0,0 +1,121 @@
+<?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
+ -->
+<com.android.dialer.callcomposer.cameraui.CameraMediaChooserView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/camera_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+
+ <FrameLayout
+ android:id="@+id/mediapicker_enabled"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <!-- Default to using the hardware rendered camera preview, we will fall back to
+ SoftwareCameraPreview in CameraMediaChooserView if needed -->
+ <com.android.dialer.callcomposer.camera.HardwareCameraPreview
+ android:id="@+id/camera_preview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center" />
+
+ <com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay
+ android:id="@+id/focus_visual"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <View
+ android:id="@+id/camera_shutter_visual"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/white"
+ android:visibility="gone" />
+
+ <!-- Need a background on this view in order for the ripple effect to have a place to draw -->
+ <FrameLayout
+ android:id="@+id/camera_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:padding="16dp"
+ android:layout_gravity="bottom">
+
+ <ImageButton
+ android:id="@+id/camera_fullscreen"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="bottom|end"
+ android:layout_marginEnd="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:src="@drawable/quantum_ic_fullscreen_white_48"
+ android:background="?android:selectableItemBackgroundBorderless"/>
+
+ <ImageButton
+ android:id="@+id/camera_exit_fullscreen"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="bottom|end"
+ android:layout_marginEnd="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:src="@drawable/quantum_ic_fullscreen_exit_white_48"
+ android:visibility="gone"
+ android:background="?android:selectableItemBackgroundBorderless"/>
+
+ <ImageButton
+ android:id="@+id/camera_capture_button"
+ android:layout_width="@dimen/capture_button_size"
+ android:layout_height="@dimen/capture_button_size"
+ android:layout_gravity="bottom|center_horizontal"
+ android:layout_marginBottom="@dimen/capture_button_bottom_margin"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:src="@drawable/ic_capture"
+ android:scaleType="fitXY"
+ android:contentDescription="@string/camera_take_picture"/>
+
+ <ImageButton
+ android:id="@+id/swap_camera_button"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="start|bottom"
+ android:layout_marginStart="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:src="@drawable/front_back_switch_button_animation"
+ android:background="@drawable/transparent_button_background"
+ android:contentDescription="@string/camera_switch_camera_rear"/>
+
+ <ImageButton
+ android:id="@+id/camera_cancel_button"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="start|bottom"
+ android:layout_marginStart="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:visibility="gone"
+ android:background="@drawable/transparent_button_background"
+ android:src="@drawable/quantum_ic_undo_white_48"
+ android:contentDescription="@string/camera_cancel_recording" />
+ </FrameLayout>
+ </FrameLayout>
+
+ <ProgressBar
+ android:id="@+id/loading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+</com.android.dialer.callcomposer.cameraui.CameraMediaChooserView>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml b/java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml
new file mode 100644
index 0000000..d5a839a
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="background_item_grey_pressed">#E0E0E0</color>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml b/java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml
new file mode 100644
index 0000000..09d4a58
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<?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="camera_view_button_margin">22dp</dimen>
+ <dimen name="camera_view_button_size">46dp</dimen>
+ <dimen name="capture_button_size">84dp</dimen>
+ <dimen name="capture_button_bottom_margin">4dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml b/java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml
new file mode 100644
index 0000000..999fe8f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description of button to switch to full screen camera -->
+ <string name="camera_switch_full_screen">Switch to full screen camera</string>
+ <!-- Content description of button when after swapped to front -->
+ <string name="camera_switch_camera_facing">Button is now front camera</string>
+ <!-- Content description of button when after swapped to back -->
+ <string name="camera_switch_camera_rear">Button is now back camera</string>
+ <!-- Content description of button to cancel recording video -->
+ <string name="camera_cancel_recording">Stop recording video</string>
+ <!-- Accessibility announcement for when we are using the front facing camera -->
+ <string name="using_front_camera">Using front camera</string>
+ <!-- Accessibility announcement for when we are using the back camera -->
+ <string name="using_back_camera">Using back camera</string>
+ <!-- Content description of button to take a photo -->
+ <string name="camera_take_picture">Take photo</string>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/nano/CallComposerContact.java b/java/com/android/dialer/callcomposer/nano/CallComposerContact.java
new file mode 100644
index 0000000..acb71a0
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/nano/CallComposerContact.java
@@ -0,0 +1,220 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.callcomposer.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class CallComposerContact
+ extends com.google.protobuf.nano.ExtendableMessageNano<CallComposerContact> {
+
+ private static volatile CallComposerContact[] _emptyArray;
+ public static CallComposerContact[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallComposerContact[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional fixed64 photo_id = 1;
+ public long photoId;
+
+ // optional string photo_uri = 2;
+ public java.lang.String photoUri;
+
+ // optional string contact_uri = 3;
+ public java.lang.String contactUri;
+
+ // optional string name_or_number = 4;
+ public java.lang.String nameOrNumber;
+
+ // optional bool is_business = 5;
+ public boolean isBusiness;
+
+ // optional string number = 6;
+ public java.lang.String number;
+
+ // optional string display_number = 7;
+ public java.lang.String displayNumber;
+
+ // optional string number_label = 8;
+ public java.lang.String numberLabel;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.callcomposer.CallComposerContact)
+
+ public CallComposerContact() {
+ clear();
+ }
+
+ public CallComposerContact clear() {
+ photoId = 0L;
+ photoUri = "";
+ contactUri = "";
+ nameOrNumber = "";
+ isBusiness = false;
+ number = "";
+ displayNumber = "";
+ numberLabel = "";
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.photoId != 0L) {
+ output.writeFixed64(1, this.photoId);
+ }
+ if (this.photoUri != null && !this.photoUri.equals("")) {
+ output.writeString(2, this.photoUri);
+ }
+ if (this.contactUri != null && !this.contactUri.equals("")) {
+ output.writeString(3, this.contactUri);
+ }
+ if (this.nameOrNumber != null && !this.nameOrNumber.equals("")) {
+ output.writeString(4, this.nameOrNumber);
+ }
+ if (this.isBusiness != false) {
+ output.writeBool(5, this.isBusiness);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ output.writeString(6, this.number);
+ }
+ if (this.displayNumber != null && !this.displayNumber.equals("")) {
+ output.writeString(7, this.displayNumber);
+ }
+ if (this.numberLabel != null && !this.numberLabel.equals("")) {
+ output.writeString(8, this.numberLabel);
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.photoId != 0L) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeFixed64Size(1, this.photoId);
+ }
+ if (this.photoUri != null && !this.photoUri.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.photoUri);
+ }
+ if (this.contactUri != null && !this.contactUri.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(3, this.contactUri);
+ }
+ if (this.nameOrNumber != null && !this.nameOrNumber.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 4, this.nameOrNumber);
+ }
+ if (this.isBusiness != false) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeBoolSize(5, this.isBusiness);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(6, this.number);
+ }
+ if (this.displayNumber != null && !this.displayNumber.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 7, this.displayNumber);
+ }
+ if (this.numberLabel != null && !this.numberLabel.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(8, this.numberLabel);
+ }
+ return size;
+ }
+
+ @Override
+ public CallComposerContact mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 9:
+ {
+ this.photoId = input.readFixed64();
+ break;
+ }
+ case 18:
+ {
+ this.photoUri = input.readString();
+ break;
+ }
+ case 26:
+ {
+ this.contactUri = input.readString();
+ break;
+ }
+ case 34:
+ {
+ this.nameOrNumber = input.readString();
+ break;
+ }
+ case 40:
+ {
+ this.isBusiness = input.readBool();
+ break;
+ }
+ case 50:
+ {
+ this.number = input.readString();
+ break;
+ }
+ case 58:
+ {
+ this.displayNumber = input.readString();
+ break;
+ }
+ case 66:
+ {
+ this.numberLabel = input.readString();
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallComposerContact parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallComposerContact(), data);
+ }
+
+ public static CallComposerContact parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+ return new CallComposerContact().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml b/java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml
new file mode 100644
index 0000000..b3c36e9
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml
@@ -0,0 +1,30 @@
+<?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
+ -->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <stroke
+ android:width="@dimen/call_composer_contact_photo_border_thickness"
+ android:color="@color/background_dialer_white"/>
+
+ <padding
+ android:bottom="@dimen/call_composer_contact_photo_border_thickness"
+ android:left="@dimen/call_composer_contact_photo_border_thickness"
+ android:right="@dimen/call_composer_contact_photo_border_thickness"
+ android:top="@dimen/call_composer_contact_photo_border_thickness"/>
+</shape>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml
new file mode 100644
index 0000000..57dce97
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml
@@ -0,0 +1,22 @@
+<?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
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/gallery_item_corner_radius"/>
+ <solid android:color="@color/gallery_item_image_color"/>
+</shape>
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml
new file mode 100644
index 0000000..b6b91b5
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml
@@ -0,0 +1,22 @@
+<?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
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/gallery_item_corner_radius"/>
+ <solid android:color="#80000000"/>
+</shape>
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml
new file mode 100644
index 0000000..bbae1a8
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml
@@ -0,0 +1,22 @@
+<?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
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/gallery_item_corner_radius"/>
+ <solid android:color="@color/background_dialer_white"/>
+</shape>
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml
new file mode 100644
index 0000000..5050407
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.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
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="oval">
+ <stroke
+ android:width="1dp"
+ android:color="@color/dialer_theme_color"/>
+ <solid
+ android:color="@color/background_dialer_white"/>
+ <size
+ android:height="@dimen/gallery_check_size"
+ android:width="@dimen/gallery_check_size"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@drawable/quantum_ic_check_black_24"
+ android:tint="@color/dialer_theme_color"/>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
new file mode 100644
index 0000000..518b53f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
@@ -0,0 +1,147 @@
+<?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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/call_composer_background_color">
+
+ <LinearLayout
+ android:id="@+id/call_composer_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="bottom"
+ android:background="@android:color/transparent">
+
+ <RelativeLayout
+ android:id="@+id/contact_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:elevation="@dimen/call_composer_contact_container_elevation"
+ android:background="?android:attr/selectableItemBackground">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginTop="@dimen/call_composer_contact_container_margin_top"
+ android:paddingTop="@dimen/call_composer_contact_container_padding_top"
+ android:paddingBottom="@dimen/call_composer_contact_container_padding_bottom"
+ android:background="@color/dialer_theme_color">
+
+ <TextView
+ android:id="@+id/contact_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textColor="@color/background_dialer_white"
+ android:textSize="@dimen/call_composer_name_text_size"/>
+
+ <TextView
+ android:id="@+id/phone_number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textColor="@color/background_dialer_white"
+ android:textSize="@dimen/call_composer_number_text_size"/>
+ </LinearLayout>
+
+ <QuickContactBadge
+ android:id="@+id/contact_photo"
+ android:layout_width="@dimen/call_composer_contact_photo_size"
+ android:layout_height="@dimen/call_composer_contact_photo_size"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/call_composer_contact_border"/>
+ </RelativeLayout>
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/call_composer_view_pager"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_view_pager_height"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/media_actions"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_media_bar_height"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal"
+ android:background="@color/dialer_secondary_color"
+ android:clickable="true">
+
+ <ImageView
+ android:id="@+id/call_composer_camera"
+ android:layout_width="@dimen/call_composer_media_actions_width"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_camera_alt_white_24"
+ android:background="?android:attr/selectableItemBackgroundBorderless"/>
+
+ <ImageView
+ android:id="@+id/call_composer_photo"
+ android:layout_width="@dimen/call_composer_media_actions_width"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_photo_white_24"
+ android:background="?android:attr/selectableItemBackgroundBorderless"/>
+
+ <ImageView
+ android:id="@+id/call_composer_message"
+ android:layout_width="@dimen/call_composer_media_actions_width"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:src="@drawable/ic_message_24dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/send_and_call_button"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_media_bar_height"
+ android:visibility="invisible"
+ android:background="@color/compose_and_call_background">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:drawableStart="@drawable/quantum_ic_call_white_18"
+ android:drawablePadding="@dimen/send_and_call_drawable_padding"
+ android:textAllCaps="true"
+ android:text="@string/send_and_call"
+ android:textSize="@dimen/send_and_call_text_size"
+ android:fontFamily="sans-serif-medium"
+ android:textColor="@color/background_dialer_white"/>
+ </FrameLayout>
+ </FrameLayout>
+ </LinearLayout>
+
+ <Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
+ android:visibility="invisible"
+ android:titleTextAppearance="@style/call_composer_toolbar_title_text"
+ android:subtitleTextAppearance="@style/call_composer_toolbar_subtitle_text"
+ android:navigationIcon="@drawable/quantum_ic_close_white_24"
+ android:background="@color/dialer_theme_color"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml
new file mode 100644
index 0000000..200a3dc
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml
@@ -0,0 +1,33 @@
+<?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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ layout="@layout/camera_view"/>
+
+ <include
+ android:id="@+id/permission_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ layout="@layout/permission_view"/>
+</FrameLayout>
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
new file mode 100644
index 0000000..58893ba
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
@@ -0,0 +1,38 @@
+<?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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_white">
+
+ <GridView
+ android:id="@+id/gallery_grid_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/gallery_item_padding"
+ android:paddingRight="@dimen/gallery_item_padding"
+ android:paddingTop="@dimen/gallery_item_padding"
+ android:numColumns="3"/>
+
+ <include
+ android:id="@+id/permission_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ layout="@layout/permission_view"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
new file mode 100644
index 0000000..97f232b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
@@ -0,0 +1,79 @@
+<?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
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_view_pager_height"
+ android:orientation="vertical"
+ android:gravity="bottom"
+ android:background="@color/background_dialer_white">
+
+ <TextView
+ android:id="@+id/message_urgent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/urgent"
+ style="@style/message_composer_textview"/>
+
+ <TextView
+ android:id="@+id/message_chat"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/want_to_chat"
+ style="@style/message_composer_textview"/>
+
+ <TextView
+ android:id="@+id/message_question"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/quick_question"
+ style="@style/message_composer_textview"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/message_composer_divider_height"
+ android:background="@color/call_composer_divider"/>
+
+ <RelativeLayout
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/custom_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/message_composer_item_padding"
+ android:textSize="@dimen/message_compose_item_text_size"
+ android:hint="@string/custom_message_hint"
+ android:textColor="@color/dialer_primary_text_color"
+ android:textColorHint="@color/dialer_edit_text_hint_color"
+ android:background="@color/background_dialer_white"
+ android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
+ android:layout_toLeftOf="@+id/remaining_characters"/>
+
+ <TextView
+ android:id="@+id/remaining_characters"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/message_composer_item_padding"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:textSize="@dimen/message_compose_remaining_char_text_size"
+ android:textColor="@color/dialer_edit_text_hint_color"/>
+ </RelativeLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml b/java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml
new file mode 100644
index 0000000..6c68517
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml
@@ -0,0 +1,57 @@
+<?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
+ -->
+<com.android.dialer.callcomposer.GalleryGridItemView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/gallery_item_padding"
+ android:clickable="true">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/gallery_grid_item_view_background"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+
+ <FrameLayout
+ android:id="@+id/checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/gallery_grid_checkbox_background"
+ android:outlineProvider="background"
+ android:visibility="gone">
+
+ <ImageView
+ android:layout_width="@dimen/gallery_check_size"
+ android:layout_height="@dimen/gallery_check_size"
+ android:layout_gravity="center"
+ android:src="@drawable/gallery_item_selected_drawable"/>
+ </FrameLayout>
+
+ <ImageView
+ android:id="@+id/gallery"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:src="@drawable/quantum_ic_photo_library_white_24"
+ android:scaleType="center"
+ android:background="@drawable/gallery_background"
+ android:outlineProvider="background"
+ android:visibility="gone"/>
+</com.android.dialer.callcomposer.GalleryGridItemView>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/permission_view.xml b/java/com/android/dialer/callcomposer/res/layout/permission_view.xml
new file mode 100644
index 0000000..4daa11d
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/permission_view.xml
@@ -0,0 +1,52 @@
+<?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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:clickable="true"
+ android:background="@color/background_dialer_white">
+
+ <ImageView
+ android:id="@+id/permission_icon"
+ android:layout_width="@dimen/permission_image_size"
+ android:layout_height="@dimen/permission_image_size"
+ android:layout_margin="@dimen/permission_item_margin"/>
+
+ <TextView
+ android:id="@+id/permission_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/permission_item_margin"
+ style="@style/TextAppearanceMedium"/>
+
+ <TextView
+ android:id="@+id/allow"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/min_touch_target_size"
+ android:minWidth="@dimen/min_touch_target_size"
+ android:gravity="center"
+ android:text="@string/allow"
+ android:textAllCaps="true"
+ android:textSize="@dimen/allow_permission_text_size"
+ android:textColor="@color/dialer_theme_color"
+ android:background="?android:attr/selectableItemBackground"
+ android:padding="@dimen/permission_allow_padding"
+ android:theme="@style/Theme.AppCompat.Light"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/colors.xml b/java/com/android/dialer/callcomposer/res/values/colors.xml
new file mode 100644
index 0000000..89e55b7
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?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>
+ <!-- 50% black -->
+ <color name="call_composer_background_color">#7F000000</color>
+ <color name="call_composer_divider">#12000000</color>
+ <color name="compose_and_call_background">#00BC35</color>
+ <color name="gallery_item_image_color">#607D8B</color>
+ <color name="gallery_item_background_color">#ECEFF1</color>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/dimens.xml b/java/com/android/dialer/callcomposer/res/values/dimens.xml
new file mode 100644
index 0000000..3ebda7a
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/dimens.xml
@@ -0,0 +1,63 @@
+<?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="call_composer_view_pager_height">258dp</dimen>
+
+ <!-- Toolbar -->
+ <dimen name="toolbar_title_text_size">16sp</dimen>
+ <dimen name="toolbar_subtitle_text_size">14sp</dimen>
+
+ <!-- Contact bar -->
+ <dimen name="call_composer_contact_photo_border_thickness">2dp</dimen>
+ <dimen name="call_composer_contact_photo_size">116dp</dimen>
+ <dimen name="call_composer_contact_container_margin_top">58dp</dimen>
+ <dimen name="call_composer_contact_container_padding_top">58dp</dimen>
+ <dimen name="call_composer_contact_container_padding_bottom">18dp</dimen>
+ <dimen name="call_composer_name_text_size">32sp</dimen>
+ <dimen name="call_composer_number_text_size">16sp</dimen>
+ <dimen name="call_composer_contact_container_elevation">2dp</dimen>
+
+ <!-- Media bar -->
+ <dimen name="call_composer_media_actions_width">80dp</dimen>
+ <dimen name="call_composer_media_bar_height">48dp</dimen>
+
+ <!-- Send and Call button -->
+ <dimen name="send_and_call_icon_size">18dp</dimen>
+ <dimen name="send_and_call_text_size">16sp</dimen>
+ <dimen name="send_and_call_padding">8dp</dimen>
+ <dimen name="send_and_call_drawable_padding">4dp</dimen>
+
+ <!-- Message Composer -->
+ <dimen name="message_composer_item_padding">16dp</dimen>
+ <dimen name="message_compose_item_text_size">16sp</dimen>
+ <dimen name="message_compose_remaining_char_text_size">12sp</dimen>
+ <dimen name="message_composer_divider_height">1dp</dimen>
+ <integer name="call_composer_message_limit">60</integer>
+
+ <!-- Gallery Composer -->
+ <dimen name="gallery_item_selected_padding">6dp</dimen>
+ <dimen name="gallery_item_padding">3dp</dimen>
+ <dimen name="gallery_check_size">48dp</dimen>
+ <dimen name="gallery_item_corner_radius">2dp</dimen>
+
+ <!-- Permissions view -->
+ <dimen name="permission_image_size">72dp</dimen>
+ <dimen name="allow_permission_text_size">16sp</dimen>
+ <dimen name="permission_item_margin">8dp</dimen>
+ <dimen name="permission_allow_padding">16dp</dimen>
+ <dimen name="min_touch_target_size">48dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/strings.xml b/java/com/android/dialer/callcomposer/res/values/strings.xml
new file mode 100644
index 0000000..35a8cf9
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
+ <string name="urgent">Urgent! Please pick up!</string>
+ <!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
+ <string name="want_to_chat">Want to chat?</string>
+ <!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
+ <string name="quick_question">Quick question…</string>
+ <!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] -->
+ <string name="custom_message_hint">Write a custom message</string>
+ <!-- Text for a button to make a phone call combined with a picture or text message [CHAR LIMIT=26] -->
+ <string name="send_and_call">Send and call</string>
+ <!-- Accessibility description for each image in the gallery. For example, "image January 17 2015 1 59 pm". -->
+ <string name="gallery_item_description">image <xliff:g id="date">%1$tB %1$te %1$tY %1$tl %1$tM %1$tp</xliff:g></string>
+ <!-- Accessibility description for each image in the gallery when no date is present. -->
+ <string name="gallery_item_description_no_date">image</string>
+ <!-- Content description of button to switch camera to picture more -->
+ <string name="camera_switch_to_still_mode">Take a photo</string>
+ <!-- Error toast message shown when a camera image failed to attach to the message -->
+ <string name="camera_media_failure">Couldn\'t load camera image</string>
+ <!-- Text for a button to ask for device permissions -->
+ <string name="allow">Allow</string>
+ <!-- Text presented to the user explaining that we need Camera permission to take photos -->
+ <string name="camera_permission_text">To take a photo, give access to Camera</string>
+ <!-- Text presented to the user explaining that we need device storage permission to view photos -->
+ <string name="gallery_permission_text">To share an image, give access to Media</string>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/styles.xml b/java/com/android/dialer/callcomposer/res/values/styles.xml
new file mode 100644
index 0000000..891f639
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/styles.xml
@@ -0,0 +1,50 @@
+<?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>
+ <style name="Theme.AppCompat.CallComposer" parent="Theme.AppCompat.NoActionBar">
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowFrame">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:listViewStyle">@style/ListViewStyle</item>
+ <!-- We need to use a light ripple behind ActionBar items in order for them to
+ be visible when using some of the darker ActionBar tints -->
+ <item name="android:actionBarItemBackground">@drawable/item_background_material_borderless_dark</item>
+ </style>
+
+ <style name="message_composer_textview">
+ <item name="android:textSize">@dimen/message_compose_item_text_size</item>
+ <item name="android:textColor">@color/dialer_primary_text_color</item>
+ <item name="android:padding">@dimen/message_composer_item_padding</item>
+ <item name="android:background">@drawable/item_background_material_light</item>
+ </style>
+
+ <style name="call_composer_toolbar_title_text">
+ <item name="android:textSize">@dimen/toolbar_title_text_size</item>
+ <item name="android:textColor">@color/background_dialer_white</item>
+ </style>
+
+ <style name="call_composer_toolbar_subtitle_text">
+ <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item>
+ <item name="android:textColor">@color/background_dialer_white</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java b/java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java
new file mode 100644
index 0000000..83580fd
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java
@@ -0,0 +1,124 @@
+/*
+ * 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.callcomposer.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FallibleAsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DialerUtils;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+
+/** Task for copying and resizing images to be shared with RCS process. */
+@TargetApi(VERSION_CODES.M)
+public class CopyAndResizeImageTask extends FallibleAsyncTask<Void, Void, File> {
+ public static final int MAX_OUTPUT_RESOLUTION = 1024;
+ private static final String MIME_TYPE = "image/jpeg";
+
+ private final Context context;
+ private final Uri uri;
+ private final Callback callback;
+
+ public CopyAndResizeImageTask(
+ @NonNull Context context, @NonNull Uri uri, @NonNull Callback callback) {
+ this.context = Assert.isNotNull(context);
+ this.uri = Assert.isNotNull(uri);
+ this.callback = Assert.isNotNull(callback);
+ }
+
+ @Nullable
+ @Override
+ protected File doInBackgroundFallible(Void... params) throws Throwable {
+ Bitmap bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri));
+ bitmap = resizeForEnrichedCalling(bitmap);
+
+ File outputFile = DialerUtils.createShareableFile(context);
+ try (OutputStream outputStream = new FileOutputStream(outputFile)) {
+ // Encode images to jpeg as it is better for camera pictures which we expect to be sending
+ bitmap.compress(CompressFormat.JPEG, 90, outputStream);
+ return outputFile;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(FallibleTaskResult<File> result) {
+ if (result.isFailure()) {
+ callback.onCopyFailed(result.getThrowable());
+ } else {
+ callback.onCopySuccessful(result.getResult(), MIME_TYPE);
+ }
+ }
+
+ public static Bitmap resizeForEnrichedCalling(Bitmap image) {
+ Assert.isWorkerThread();
+
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ LogUtil.i(
+ "CopyAndResizeImageTask.resizeForEnrichedCalling",
+ "starting height: %d, width: %d",
+ height,
+ width);
+
+ if (width <= MAX_OUTPUT_RESOLUTION && height <= MAX_OUTPUT_RESOLUTION) {
+ LogUtil.i("CopyAndResizeImageTask.resizeForEnrichedCalling", "no resizing needed");
+ return image;
+ }
+
+ if (width > height) {
+ // landscape
+ float ratio = width / (float) MAX_OUTPUT_RESOLUTION;
+ width = MAX_OUTPUT_RESOLUTION;
+ height = (int) (height / ratio);
+ } else if (height > width) {
+ // portrait
+ float ratio = height / (float) MAX_OUTPUT_RESOLUTION;
+ height = MAX_OUTPUT_RESOLUTION;
+ width = (int) (width / ratio);
+ } else {
+ // square
+ height = MAX_OUTPUT_RESOLUTION;
+ width = MAX_OUTPUT_RESOLUTION;
+ }
+
+ LogUtil.i(
+ "CopyAndResizeImageTask.resizeForEnrichedCalling",
+ "ending height: %d, width: %d",
+ height,
+ width);
+
+ return Bitmap.createScaledBitmap(image, width, height, true);
+ }
+
+ /** Callback for callers to know when the task has finished */
+ public interface Callback {
+ void onCopySuccessful(File file, String mimeType);
+
+ void onCopyFailed(Throwable throwable);
+ }
+}
diff --git a/java/com/android/dialer/callintent/CallIntentBuilder.java b/java/com/android/dialer/callintent/CallIntentBuilder.java
new file mode 100644
index 0000000..a2fb564
--- /dev/null
+++ b/java/com/android/dialer/callintent/CallIntentBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * 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.callintent;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.CallUtil;
+
+/** Creates an intent to start a new outgoing call. */
+public class CallIntentBuilder {
+ private final Uri uri;
+ private final CallSpecificAppData callSpecificAppData;
+ @Nullable private PhoneAccountHandle phoneAccountHandle;
+ private boolean isVideoCall;
+ private String callSubject;
+
+ public CallIntentBuilder(@NonNull Uri uri, @NonNull CallSpecificAppData callSpecificAppData) {
+ this.uri = Assert.isNotNull(uri);
+ this.callSpecificAppData = Assert.isNotNull(callSpecificAppData);
+ Assert.checkArgument(
+ callSpecificAppData.callInitiationType != CallInitiationType.Type.UNKNOWN_INITIATION);
+ }
+
+ public CallIntentBuilder(@NonNull Uri uri, int callInitiationType) {
+ this(uri, createCallSpecificAppData(callInitiationType));
+ }
+
+ public CallIntentBuilder(
+ @NonNull String number, @NonNull CallSpecificAppData callSpecificAppData) {
+ this(CallUtil.getCallUri(Assert.isNotNull(number)), callSpecificAppData);
+ }
+
+ public CallIntentBuilder(@NonNull String number, int callInitiationType) {
+ this(CallUtil.getCallUri(Assert.isNotNull(number)), callInitiationType);
+ }
+
+ public CallSpecificAppData getCallSpecificAppData() {
+ return callSpecificAppData;
+ }
+
+ public CallIntentBuilder setPhoneAccountHandle(@Nullable PhoneAccountHandle accountHandle) {
+ this.phoneAccountHandle = accountHandle;
+ return this;
+ }
+
+ public CallIntentBuilder setIsVideoCall(boolean isVideoCall) {
+ this.isVideoCall = isVideoCall;
+ return this;
+ }
+
+ public CallIntentBuilder setCallSubject(String callSubject) {
+ this.callSubject = callSubject;
+ return this;
+ }
+
+ public Intent build() {
+ Intent intent = new Intent(Intent.ACTION_CALL, uri);
+ intent.putExtra(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ isVideoCall ? VideoProfile.STATE_BIDIRECTIONAL : VideoProfile.STATE_AUDIO_ONLY);
+
+ Bundle extras = new Bundle();
+ extras.putLong(Constants.EXTRA_CALL_CREATED_TIME_MILLIS, SystemClock.elapsedRealtime());
+ CallIntentParser.putCallSpecificAppData(extras, callSpecificAppData);
+ intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
+
+ if (phoneAccountHandle != null) {
+ intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ }
+
+ if (!TextUtils.isEmpty(callSubject)) {
+ intent.putExtra(TelecomManager.EXTRA_CALL_SUBJECT, callSubject);
+ }
+
+ return intent;
+ }
+
+ private static @NonNull CallSpecificAppData createCallSpecificAppData(int callInitiationType) {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = callInitiationType;
+ return callSpecificAppData;
+ }
+}
diff --git a/java/com/android/dialer/callintent/CallIntentParser.java b/java/com/android/dialer/callintent/CallIntentParser.java
new file mode 100644
index 0000000..40c8ee3
--- /dev/null
+++ b/java/com/android/dialer/callintent/CallIntentParser.java
@@ -0,0 +1,54 @@
+/*
+ * 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.callintent;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+import com.google.protobuf.nano.MessageNano;
+
+/** Parses data for a call extra to get any dialer specific app data. */
+public class CallIntentParser {
+ @Nullable
+ public static CallSpecificAppData getCallSpecificAppData(@Nullable Bundle extras) {
+ if (extras == null) {
+ return null;
+ }
+
+ byte[] flatArray = extras.getByteArray(Constants.EXTRA_CALL_SPECIFIC_APP_DATA);
+ if (flatArray == null) {
+ return null;
+ }
+ try {
+ return CallSpecificAppData.parseFrom(flatArray);
+ } catch (InvalidProtocolBufferNanoException e) {
+ Assert.fail("unexpected exception: " + e);
+ return null;
+ }
+ }
+
+ public static void putCallSpecificAppData(
+ @NonNull Bundle extras, @NonNull CallSpecificAppData callSpecificAppData) {
+ extras.putByteArray(
+ Constants.EXTRA_CALL_SPECIFIC_APP_DATA, MessageNano.toByteArray(callSpecificAppData));
+ }
+
+ private CallIntentParser() {}
+}
diff --git a/java/com/android/dialer/callintent/Constants.java b/java/com/android/dialer/callintent/Constants.java
new file mode 100644
index 0000000..dd5d481
--- /dev/null
+++ b/java/com/android/dialer/callintent/Constants.java
@@ -0,0 +1,31 @@
+/*
+ * 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.callintent;
+
+/** Constants used to construct and parse call intents. These should never be made public. */
+/* package */ class Constants {
+ // This is a Dialer extra that is set for outgoing calls and used by the InCallUI.
+ /* package */ static final String EXTRA_CALL_SPECIFIC_APP_DATA =
+ "com.android.dialer.callintent.CALL_SPECIFIC_APP_DATA";
+
+ // This is a hidden system extra. For outgoing calls Dialer sets it and parses it but for incoming
+ // calls Telecom sets it and Dialer parses it.
+ /* package */ static final String EXTRA_CALL_CREATED_TIME_MILLIS =
+ "android.telecom.extra.CALL_CREATED_TIME_MILLIS";
+
+ private Constants() {}
+}
diff --git a/java/com/android/dialer/callintent/nano/CallInitiationType.java b/java/com/android/dialer/callintent/nano/CallInitiationType.java
new file mode 100644
index 0000000..4badd6e
--- /dev/null
+++ b/java/com/android/dialer/callintent/nano/CallInitiationType.java
@@ -0,0 +1,101 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.callintent.nano;
+
+@SuppressWarnings("hiding")
+public final class CallInitiationType extends
+ com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_INITIATION = 0;
+ public static final int INCOMING_INITIATION = 1;
+ public static final int DIALPAD = 2;
+ public static final int SPEED_DIAL = 3;
+ public static final int REMOTE_DIRECTORY = 4;
+ public static final int SMART_DIAL = 5;
+ public static final int REGULAR_SEARCH = 6;
+ public static final int CALL_LOG = 7;
+ public static final int CALL_LOG_FILTER = 8;
+ public static final int VOICEMAIL_LOG = 9;
+ public static final int CALL_DETAILS = 10;
+ public static final int QUICK_CONTACTS = 11;
+ public static final int EXTERNAL_INITIATION = 12;
+ public static final int LAUNCHER_SHORTCUT = 13;
+ public static final int CALL_COMPOSER = 14;
+ public static final int MISSED_CALL_NOTIFICATION = 15;
+ public static final int CALL_SUBJECT_DIALOG = 16;
+ }
+
+ private static volatile CallInitiationType[] _emptyArray;
+ public static CallInitiationType[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallInitiationType[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.callintent.CallInitiationType)
+
+ public CallInitiationType() {
+ clear();
+ }
+
+ public CallInitiationType clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public CallInitiationType mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallInitiationType parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallInitiationType(), data);
+ }
+
+ public static CallInitiationType parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new CallInitiationType().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/callintent/nano/CallSpecificAppData.java b/java/com/android/dialer/callintent/nano/CallSpecificAppData.java
new file mode 100644
index 0000000..fd00b0a
--- /dev/null
+++ b/java/com/android/dialer/callintent/nano/CallSpecificAppData.java
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.callintent.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class CallSpecificAppData
+ extends com.google.protobuf.nano.ExtendableMessageNano<CallSpecificAppData> {
+
+ private static volatile CallSpecificAppData[] _emptyArray;
+
+ public static CallSpecificAppData[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallSpecificAppData[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional int32 call_initiation_type = 1;
+ public int callInitiationType;
+
+ // optional int32 position_of_selected_search_result = 2;
+ public int positionOfSelectedSearchResult;
+
+ // optional int32 characters_in_search_string = 3;
+ public int charactersInSearchString;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.callintent.CallSpecificAppData)
+
+ public CallSpecificAppData() {
+ clear();
+ }
+
+ public CallSpecificAppData clear() {
+ callInitiationType = 0;
+ positionOfSelectedSearchResult = 0;
+ charactersInSearchString = 0;
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.callInitiationType != 0) {
+ output.writeInt32(1, this.callInitiationType);
+ }
+ if (this.positionOfSelectedSearchResult != 0) {
+ output.writeInt32(2, this.positionOfSelectedSearchResult);
+ }
+ if (this.charactersInSearchString != 0) {
+ output.writeInt32(3, this.charactersInSearchString);
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.callInitiationType != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(
+ 1, this.callInitiationType);
+ }
+ if (this.positionOfSelectedSearchResult != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(
+ 2, this.positionOfSelectedSearchResult);
+ }
+ if (this.charactersInSearchString != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(
+ 3, this.charactersInSearchString);
+ }
+ return size;
+ }
+
+ @Override
+ public CallSpecificAppData mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 8:
+ {
+ this.callInitiationType = input.readInt32();
+ break;
+ }
+ case 16:
+ {
+ this.positionOfSelectedSearchResult = input.readInt32();
+ break;
+ }
+ case 24:
+ {
+ this.charactersInSearchString = input.readInt32();
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallSpecificAppData parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallSpecificAppData(), data);
+ }
+
+ public static CallSpecificAppData parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+ return new CallSpecificAppData().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/common/AndroidManifest.xml b/java/com/android/dialer/common/AndroidManifest.xml
new file mode 100644
index 0000000..ae43d66
--- /dev/null
+++ b/java/com/android/dialer/common/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.common">
+</manifest>
diff --git a/java/com/android/dialer/common/Assert.java b/java/com/android/dialer/common/Assert.java
new file mode 100644
index 0000000..00b4f25
--- /dev/null
+++ b/java/com/android/dialer/common/Assert.java
@@ -0,0 +1,185 @@
+/*
+ * 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.common;
+
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/** Assertions which will result in program termination unless disabled by flags. */
+public class Assert {
+
+ private static boolean areThreadAssertsEnabled = true;
+
+ public static void setAreThreadAssertsEnabled(boolean areThreadAssertsEnabled) {
+ Assert.areThreadAssertsEnabled = areThreadAssertsEnabled;
+ }
+
+ /**
+ * Called when a truly exceptional case occurs.
+ *
+ * @throws AssertionError
+ */
+ public static void fail() {
+ throw new AssertionError("Fail");
+ }
+
+ /**
+ * Called when a truly exceptional case occurs.
+ *
+ * @param reason the optional reason to supply as the exception message
+ * @throws AssertionError
+ */
+ public static void fail(String reason) {
+ throw new AssertionError(reason);
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalArgumentException if {@code expression} is false
+ */
+ public static void checkArgument(boolean expression) {
+ checkArgument(expression, null);
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalArgumentException if {@code expression} is false
+ */
+ public static void checkArgument(
+ boolean expression, @Nullable String messageTemplate, Object... args) {
+ if (!expression) {
+ throw new IllegalArgumentException(format(messageTemplate, args));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance, but not
+ * involving any parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalStateException if {@code expression} is false
+ */
+ public static void checkState(boolean expression) {
+ checkState(expression, null);
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance, but not
+ * involving any parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalStateException if {@code expression} is false
+ */
+ public static void checkState(
+ boolean expression, @Nullable String messageTemplate, Object... args) {
+ if (!expression) {
+ throw new IllegalStateException(format(messageTemplate, args));
+ }
+ }
+
+ /**
+ * Ensures that an object reference passed as a parameter to the calling method is not null.
+ *
+ * @param reference an object reference
+ * @return the non-null reference that was validated
+ * @throws NullPointerException if {@code reference} is null
+ */
+ @NonNull
+ public static <T> T isNotNull(@Nullable T reference) {
+ return isNotNull(reference, null);
+ }
+
+ /**
+ * Ensures that an object reference passed as a parameter to the calling method is not null.
+ *
+ * @param reference an object reference
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @return the non-null reference that was validated
+ * @throws NullPointerException if {@code reference} is null
+ */
+ @NonNull
+ public static <T> T isNotNull(
+ @Nullable T reference, @Nullable String messageTemplate, Object... args) {
+ if (reference == null) {
+ throw new NullPointerException(format(messageTemplate, args));
+ }
+ return reference;
+ }
+
+ /**
+ * Ensures that the current thread is the main thread.
+ *
+ * @throws IllegalStateException if called on a background thread
+ */
+ public static void isMainThread() {
+ isMainThread(null);
+ }
+
+ /**
+ * Ensures that the current thread is the main thread.
+ *
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalStateException if called on a background thread
+ */
+ public static void isMainThread(@Nullable String messageTemplate, Object... args) {
+ if (!areThreadAssertsEnabled) {
+ return;
+ }
+ checkState(Looper.getMainLooper().equals(Looper.myLooper()), messageTemplate, args);
+ }
+
+ /**
+ * Ensures that the current thread is a worker thread.
+ *
+ * @throws IllegalStateException if called on the main thread
+ */
+ public static void isWorkerThread() {
+ isWorkerThread(null);
+ }
+
+ /**
+ * Ensures that the current thread is a worker thread.
+ *
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalStateException if called on the main thread
+ */
+ public static void isWorkerThread(@Nullable String messageTemplate, Object... args) {
+ if (!areThreadAssertsEnabled) {
+ return;
+ }
+ checkState(!Looper.getMainLooper().equals(Looper.myLooper()), messageTemplate, args);
+ }
+
+ private static String format(@Nullable String messageTemplate, Object... args) {
+ if (messageTemplate == null) {
+ return null;
+ }
+ return String.format(messageTemplate, args);
+ }
+}
diff --git a/java/com/android/dialer/common/AsyncTaskExecutor.java b/java/com/android/dialer/common/AsyncTaskExecutor.java
new file mode 100644
index 0000000..caadfe7
--- /dev/null
+++ b/java/com/android/dialer/common/AsyncTaskExecutor.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 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.common;
+
+import android.os.AsyncTask;
+import android.support.annotation.MainThread;
+import java.util.concurrent.Executor;
+
+/**
+ * Interface used to submit {@link AsyncTask} objects to run in the background.
+ *
+ * <p>This interface has a direct parallel with the {@link Executor} interface. It exists to
+ * decouple the mechanics of AsyncTask submission from the description of how that AsyncTask will
+ * execute.
+ *
+ * <p>One immediate benefit of this approach is that testing becomes much easier, since it is easy
+ * to introduce a mock or fake AsyncTaskExecutor in unit/integration tests, and thus inspect which
+ * tasks have been submitted and control their execution in an orderly manner.
+ *
+ * <p>Another benefit in due course will be the management of the submitted tasks. An extension to
+ * this interface is planned to allow Activities to easily cancel all the submitted tasks that are
+ * still pending in the onDestroy() method of the Activity.
+ */
+public interface AsyncTaskExecutor {
+
+ /**
+ * Executes the given AsyncTask with the default Executor.
+ *
+ * <p>This method <b>must only be called from the ui thread</b>.
+ *
+ * <p>The identifier supplied is any Object that can be used to identify the task later. Most
+ * commonly this will be an enum which the tests can also refer to. {@code null} is also accepted,
+ * though of course this won't help in identifying the task later.
+ */
+ @MainThread
+ <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params);
+}
diff --git a/java/com/android/dialer/common/AsyncTaskExecutors.java b/java/com/android/dialer/common/AsyncTaskExecutors.java
new file mode 100644
index 0000000..77bebdb
--- /dev/null
+++ b/java/com/android/dialer/common/AsyncTaskExecutors.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 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.common;
+
+import android.os.AsyncTask;
+import android.support.annotation.MainThread;
+import java.util.concurrent.Executor;
+
+/**
+ * Factory methods for creating AsyncTaskExecutors.
+ *
+ * <p>All of the factory methods on this class check first to see if you have set a static {@link
+ * AsyncTaskExecutorFactory} set through the {@link #setFactoryForTest(AsyncTaskExecutorFactory)}
+ * method, and if so delegate to that instead, which is one way of injecting dependencies for
+ * testing classes whose construction cannot be controlled such as {@link android.app.Activity}.
+ */
+public final class AsyncTaskExecutors {
+
+ /**
+ * A single instance of the {@link AsyncTaskExecutorFactory}, to which we delegate if it is
+ * non-null, for injecting when testing.
+ */
+ private static AsyncTaskExecutorFactory mInjectedAsyncTaskExecutorFactory = null;
+
+ /**
+ * Creates an AsyncTaskExecutor that submits tasks to run with {@link AsyncTask#SERIAL_EXECUTOR}.
+ */
+ public static AsyncTaskExecutor createAsyncTaskExecutor() {
+ synchronized (AsyncTaskExecutors.class) {
+ if (mInjectedAsyncTaskExecutorFactory != null) {
+ return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+ }
+ return new SimpleAsyncTaskExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+ }
+
+ /**
+ * Creates an AsyncTaskExecutor that submits tasks to run with {@link
+ * AsyncTask#THREAD_POOL_EXECUTOR}.
+ */
+ public static AsyncTaskExecutor createThreadPoolExecutor() {
+ synchronized (AsyncTaskExecutors.class) {
+ if (mInjectedAsyncTaskExecutorFactory != null) {
+ return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+ }
+ return new SimpleAsyncTaskExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ public static void setFactoryForTest(AsyncTaskExecutorFactory factory) {
+ synchronized (AsyncTaskExecutors.class) {
+ mInjectedAsyncTaskExecutorFactory = factory;
+ }
+ }
+
+ /** Interface for creating AsyncTaskExecutor objects. */
+ public interface AsyncTaskExecutorFactory {
+
+ AsyncTaskExecutor createAsyncTaskExeuctor();
+ }
+
+ private static class SimpleAsyncTaskExecutor implements AsyncTaskExecutor {
+
+ private final Executor mExecutor;
+
+ public SimpleAsyncTaskExecutor(Executor executor) {
+ mExecutor = executor;
+ }
+
+ @Override
+ @MainThread
+ public <T> AsyncTask<T, ?, ?> submit(Object identifer, AsyncTask<T, ?, ?> task, T... params) {
+ Assert.isMainThread();
+ return task.executeOnExecutor(mExecutor, params);
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java b/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
new file mode 100644
index 0000000..f9d7cea
--- /dev/null
+++ b/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
@@ -0,0 +1,79 @@
+/*
+ * 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.common;
+
+import android.support.annotation.Nullable;
+import javax.annotation.Generated;
+
+
+ final class AutoValue_FallibleAsyncTask_FallibleTaskResult<ResultT> extends FallibleAsyncTask.FallibleTaskResult<ResultT> {
+
+ private final Throwable throwable;
+ private final ResultT result;
+
+ AutoValue_FallibleAsyncTask_FallibleTaskResult(
+ @Nullable Throwable throwable,
+ @Nullable ResultT result) {
+ this.throwable = throwable;
+ this.result = result;
+ }
+
+ @Nullable
+ @Override
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ @Nullable
+ @Override
+ public ResultT getResult() {
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "FallibleTaskResult{"
+ + "throwable=" + throwable + ", "
+ + "result=" + result
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof FallibleAsyncTask.FallibleTaskResult) {
+ FallibleAsyncTask.FallibleTaskResult<?> that = (FallibleAsyncTask.FallibleTaskResult<?>) o;
+ return ((this.throwable == null) ? (that.getThrowable() == null) : this.throwable.equals(that.getThrowable()))
+ && ((this.result == null) ? (that.getResult() == null) : this.result.equals(that.getResult()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (throwable == null) ? 0 : this.throwable.hashCode();
+ h *= 1000003;
+ h ^= (result == null) ? 0 : this.result.hashCode();
+ return h;
+ }
+
+}
+
diff --git a/java/com/android/dialer/common/ConfigProvider.java b/java/com/android/dialer/common/ConfigProvider.java
new file mode 100644
index 0000000..c0791e9
--- /dev/null
+++ b/java/com/android/dialer/common/ConfigProvider.java
@@ -0,0 +1,27 @@
+/*
+ * 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.common;
+
+/** Gets config values from the container application. */
+public interface ConfigProvider {
+
+ String getString(String key, String defaultValue);
+
+ long getLong(String key, long defaultValue);
+
+ boolean getBoolean(String key, boolean defaultValue);
+}
diff --git a/java/com/android/dialer/common/ConfigProviderBindings.java b/java/com/android/dialer/common/ConfigProviderBindings.java
new file mode 100644
index 0000000..92e6cc3
--- /dev/null
+++ b/java/com/android/dialer/common/ConfigProviderBindings.java
@@ -0,0 +1,68 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+/** Accessor for getting a {@link ConfigProvider}. */
+public class ConfigProviderBindings {
+
+ private static ConfigProvider configProvider;
+
+ public static ConfigProvider get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ if (configProvider != null) {
+ return configProvider;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof ConfigProviderFactory) {
+ configProvider = ((ConfigProviderFactory) application).getConfigProvider();
+ }
+
+ if (configProvider == null) {
+ configProvider = new ConfigProviderStub();
+ }
+
+ return configProvider;
+ }
+
+ @VisibleForTesting
+ public static void setForTesting(@Nullable ConfigProvider configProviderForTesting) {
+ configProvider = configProviderForTesting;
+ }
+
+ private static class ConfigProviderStub implements ConfigProvider {
+ @Override
+ public String getString(String key, String defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public long getLong(String key, long defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/ConfigProviderFactory.java b/java/com/android/dialer/common/ConfigProviderFactory.java
new file mode 100644
index 0000000..aeb4f30
--- /dev/null
+++ b/java/com/android/dialer/common/ConfigProviderFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.common;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows dialer code to get
+ * references to a config provider.
+ */
+public interface ConfigProviderFactory {
+
+ ConfigProvider getConfigProvider();
+}
diff --git a/java/com/android/dialer/common/DpUtil.java b/java/com/android/dialer/common/DpUtil.java
new file mode 100644
index 0000000..0388824
--- /dev/null
+++ b/java/com/android/dialer/common/DpUtil.java
@@ -0,0 +1,31 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+
+/** Utility for dp to px conversion */
+public class DpUtil {
+
+ public static float pxToDp(Context context, float px) {
+ return px / context.getResources().getDisplayMetrics().density;
+ }
+
+ public static float dpToPx(Context context, float dp) {
+ return dp * context.getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/java/com/android/dialer/common/FallibleAsyncTask.java b/java/com/android/dialer/common/FallibleAsyncTask.java
new file mode 100644
index 0000000..fbdbda7
--- /dev/null
+++ b/java/com/android/dialer/common/FallibleAsyncTask.java
@@ -0,0 +1,94 @@
+/*
+ * 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.common;
+
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.FallibleAsyncTask.FallibleTaskResult;
+
+
+/**
+ * A task that runs work in the background, passing Throwables from {@link
+ * #doInBackground(Object[])} to {@link #onPostExecute(Object)} through a {@link
+ * FallibleTaskResult}.
+ *
+ * @param <ParamsT> the type of the parameters sent to the task upon execution
+ * @param <ProgressT> the type of the progress units published during the background computation
+ * @param <ResultT> the type of the result of the background computation
+ */
+public abstract class FallibleAsyncTask<ParamsT, ProgressT, ResultT>
+ extends AsyncTask<ParamsT, ProgressT, FallibleTaskResult<ResultT>> {
+
+ @Override
+ protected final FallibleTaskResult<ResultT> doInBackground(ParamsT... params) {
+ try {
+ return FallibleTaskResult.createSuccessResult(doInBackgroundFallible(params));
+ } catch (Throwable t) {
+ return FallibleTaskResult.createFailureResult(t);
+ }
+ }
+
+ /** Performs background work that may result in a Throwable. */
+ @Nullable
+ protected abstract ResultT doInBackgroundFallible(ParamsT... params) throws Throwable;
+
+ /**
+ * Holds the result of processing from {@link #doInBackground(Object[])}.
+ *
+ * @param <ResultT> the type of the result of the background computation
+ */
+
+ protected abstract static class FallibleTaskResult<ResultT> {
+
+ /** Creates an instance of FallibleTaskResult for the given throwable. */
+ private static <ResultT> FallibleTaskResult<ResultT> createFailureResult(@NonNull Throwable t) {
+ return new AutoValue_FallibleAsyncTask_FallibleTaskResult<>(t, null);
+ }
+
+ /** Creates an instance of FallibleTaskResult for the given result. */
+ private static <ResultT> FallibleTaskResult<ResultT> createSuccessResult(
+ @Nullable ResultT result) {
+ return new AutoValue_FallibleAsyncTask_FallibleTaskResult<>(null, result);
+ }
+
+ /**
+ * Returns the Throwable thrown in {@link #doInBackground(Object[])}, or {@code null} if
+ * background work completed without throwing.
+ */
+ @Nullable
+ public abstract Throwable getThrowable();
+
+ /**
+ * Returns the result of {@link #doInBackground(Object[])}, which may be {@code null}, or {@code
+ * null} if the background work threw a Throwable.
+ *
+ * <p>Use {@link #isFailure()} to determine if a {@code null} return is the result of a
+ * Throwable from the background work.
+ */
+ @Nullable
+ public abstract ResultT getResult();
+
+ /**
+ * Returns {@code true} if this object is the result of background work that threw a Throwable.
+ */
+ public boolean isFailure() {
+ //noinspection ThrowableResultOfMethodCallIgnored
+ return getThrowable() != null;
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/FragmentUtils.java b/java/com/android/dialer/common/FragmentUtils.java
new file mode 100644
index 0000000..cb03695
--- /dev/null
+++ b/java/com/android/dialer/common/FragmentUtils.java
@@ -0,0 +1,98 @@
+/*
+ * 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.common;
+
+import android.support.annotation.CheckResult;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+
+/** Utility methods for working with Fragments */
+public class FragmentUtils {
+
+ private static Object parentForTesting;
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void setParentForTesting(Object parentForTesting) {
+ FragmentUtils.parentForTesting = parentForTesting;
+ }
+
+ /**
+ * @return The parent of frag that implements the callbackInterface or null if no such parent can
+ * be found
+ */
+ @CheckResult(suggest = "#checkParent(Fragment, Class)}")
+ @Nullable
+ public static <T> T getParent(@NonNull Fragment fragment, @NonNull Class<T> callbackInterface) {
+ if (callbackInterface.isInstance(parentForTesting)) {
+ @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+ T parent = (T) parentForTesting;
+ return parent;
+ }
+
+ Fragment parentFragment = fragment.getParentFragment();
+ if (callbackInterface.isInstance(parentFragment)) {
+ @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+ T parent = (T) parentFragment;
+ return parent;
+ } else {
+ FragmentActivity activity = fragment.getActivity();
+ if (callbackInterface.isInstance(activity)) {
+ @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+ T parent = (T) activity;
+ return parent;
+ }
+ }
+ return null;
+ }
+
+ /** Returns the parent or throws. Should perform check elsewhere(e.g. onAttach, newInstance). */
+ @NonNull
+ public static <T> T getParentUnsafe(
+ @NonNull Fragment fragment, @NonNull Class<T> callbackInterface) {
+ return Assert.isNotNull(getParent(fragment, callbackInterface));
+ }
+
+ /**
+ * Ensures fragment has a parent that implements the corresponding interface
+ *
+ * @param frag The Fragment whose parents are to be checked
+ * @param callbackInterface The interface class that a parent should implement
+ * @throws IllegalStateException if no parents are found that implement callbackInterface
+ */
+ public static void checkParent(@NonNull Fragment frag, @NonNull Class<?> callbackInterface)
+ throws IllegalStateException {
+ if (parentForTesting != null) {
+ return;
+ }
+ if (FragmentUtils.getParent(frag, callbackInterface) == null) {
+ String parent =
+ frag.getParentFragment() == null
+ ? frag.getActivity().getClass().getName()
+ : frag.getParentFragment().getClass().getName();
+ throw new IllegalStateException(
+ frag.getClass().getName()
+ + " must be added to a parent"
+ + " that implements "
+ + callbackInterface.getName()
+ + ". Instead found "
+ + parent);
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/LogUtil.java b/java/com/android/dialer/common/LogUtil.java
new file mode 100644
index 0000000..32d7b96
--- /dev/null
+++ b/java/com/android/dialer/common/LogUtil.java
@@ -0,0 +1,214 @@
+/*
+ * 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.common;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+/** Provides logging functions. */
+public class LogUtil {
+
+ public static final String TAG = "Dialer";
+ private static final String SEPARATOR = " - ";
+
+ private LogUtil() {}
+
+ /**
+ * Log at a verbose level. Verbose logs should generally be filtered out, but may be useful when
+ * additional information is needed (e.g. to see how a particular flow evolved). These logs will
+ * not generally be available on production builds.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#v(String, String)}
+ */
+ public static void v(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.VERBOSE, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log at a debug level. Debug logs should provide known-useful information to aid in
+ * troubleshooting or evaluating flow. These logs will not generally be available on production
+ * builds.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'
+ * @param msg The message you would like logged, possibly with format arguments
+ * @param args Optional arguments to be used in the formatted string
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#d(String, String)}
+ */
+ public static void d(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.DEBUG, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log at an info level. Info logs provide information that would be useful to have on production
+ * builds for troubleshooting.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#i(String, String)}
+ */
+ public static void i(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.INFO, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log entry into a method at the info level.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ */
+ public static void enterBlock(String tag) {
+ println(android.util.Log.INFO, TAG, tag, "enter");
+ }
+
+ /**
+ * Log at a warn level. Warn logs indicate a possible error (e.g. a default switch branch was hit,
+ * or a null object was expected to be non-null), but recovery is possible. This may be used when
+ * it is not guaranteed that an indeterminate or bad state was entered, just that something may
+ * have gone wrong.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#w(String, String)}
+ */
+ public static void w(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.WARN, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log at an error level. Error logs are used when it is known that an error occurred and is
+ * possibly fatal. This is used to log information that will be useful for troubleshooting a crash
+ * or other severe condition (e.g. error codes, state values, etc.).
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#e(String, String)}
+ */
+ public static void e(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.ERROR, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log an exception at an error level. Error logs are used when it is known that an error occurred
+ * and is possibly fatal. This is used to log information that will be useful for troubleshooting
+ * a crash or other severe condition (e.g. error codes, state values, etc.).
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged.
+ * @param throwable The exception to log.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#e(String, String)}
+ */
+ public static void e(@NonNull String tag, @Nullable String msg, @NonNull Throwable throwable) {
+ if (!TextUtils.isEmpty(msg)) {
+ println(android.util.Log.ERROR, TAG, tag, msg);
+ }
+ println(android.util.Log.ERROR, TAG, tag, android.util.Log.getStackTraceString(throwable));
+ }
+
+ /**
+ * Used for log statements where we don't want to log various strings (e.g., usernames) with
+ * default logging to avoid leaking PII in logcat.
+ *
+ * @return text as is if {@value #TAG}'s log level is set to DEBUG or VERBOSE or on non-release
+ * builds; returns a redacted version otherwise.
+ */
+ public static String sanitizePii(@Nullable Object object) {
+ if (object == null) {
+ return "null";
+ }
+ if (isDebugEnabled()) {
+ return object.toString();
+ }
+ return "Redacted-" + object.toString().length() + "-chars";
+ }
+
+ /** Anonymizes char to prevent logging personally identifiable information. */
+ public static char sanitizeDialPadChar(char ch) {
+ if (isDebugEnabled()) {
+ return ch;
+ }
+ if (is12Key(ch)) {
+ return '*';
+ }
+ return ch;
+ }
+
+ /** Anonymizes the phone number to prevent logging personally identifiable information. */
+ public static String sanitizePhoneNumber(@Nullable String phoneNumber) {
+ if (isDebugEnabled()) {
+ return phoneNumber;
+ }
+ if (phoneNumber == null) {
+ return null;
+ }
+ StringBuilder stringBuilder = new StringBuilder(phoneNumber.length());
+ for (char c : phoneNumber.toCharArray()) {
+ stringBuilder.append(sanitizeDialPadChar(c));
+ }
+ return stringBuilder.toString();
+ }
+
+ public static boolean isVerboseEnabled() {
+ return android.util.Log.isLoggable(TAG, android.util.Log.VERBOSE);
+ }
+
+ public static boolean isDebugEnabled() {
+ return android.util.Log.isLoggable(TAG, android.util.Log.DEBUG);
+ }
+
+ private static boolean is12Key(char ch) {
+ return PhoneNumberUtils.is12Key(ch);
+ }
+
+ private static void println(
+ int level,
+ @NonNull String tag,
+ @NonNull String localTag,
+ @Nullable String msg,
+ @Nullable Object... args) {
+ // Formatted message is computed lazily if required.
+ String formattedMsg;
+ // Either null is passed as a single argument or more than one argument is passed.
+ boolean hasArgs = args == null || args.length > 0;
+ if ((level >= android.util.Log.INFO) || android.util.Log.isLoggable(tag, level)) {
+ formattedMsg = localTag;
+ if (!TextUtils.isEmpty(msg)) {
+ formattedMsg += SEPARATOR + (hasArgs ? String.format(msg, args) : msg);
+ }
+ android.util.Log.println(level, tag, formattedMsg);
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/MathUtil.java b/java/com/android/dialer/common/MathUtil.java
new file mode 100644
index 0000000..e811a46
--- /dev/null
+++ b/java/com/android/dialer/common/MathUtil.java
@@ -0,0 +1,57 @@
+/*
+ * 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.common;
+
+/** Utility class for common math operations */
+public class MathUtil {
+
+ /**
+ * Interpolates between two integer values based on percentage.
+ *
+ * @param begin Begin value
+ * @param end End value
+ * @param percent Percentage value, between 0 and 1
+ * @return Interpolated result
+ */
+ public static int lerp(int begin, int end, float percent) {
+ return (int) (begin * (1 - percent) + end * percent);
+ }
+
+ /**
+ * Interpolates between two float values based on percentage.
+ *
+ * @param begin Begin value
+ * @param end End value
+ * @param percent Percentage value, between 0 and 1
+ * @return Interpolated result
+ */
+ public static float lerp(float begin, float end, float percent) {
+ return begin * (1 - percent) + end * percent;
+ }
+
+ /**
+ * Clamps a value between two bounds inclusively.
+ *
+ * @param value Value to be clamped
+ * @param min Lower bound
+ * @param max Upper bound
+ * @return Clamped value
+ */
+ public static float clamp(float value, float min, float max) {
+ return Math.max(min, Math.min(value, max));
+ }
+}
diff --git a/java/com/android/dialer/common/NetworkUtil.java b/java/com/android/dialer/common/NetworkUtil.java
new file mode 100644
index 0000000..47d8424
--- /dev/null
+++ b/java/com/android/dialer/common/NetworkUtil.java
@@ -0,0 +1,192 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.StringDef;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Utility class for dealing with network */
+public class NetworkUtil {
+
+ /* Returns the current network type. */
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ @NetworkType
+ public static String getCurrentNetworkType(@Nullable Context context) {
+ if (context == null) {
+ return NetworkType.NONE;
+ }
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ return getNetworkType(connectivityManager.getActiveNetworkInfo());
+ }
+
+ /* Returns the current network info. */
+ @Nullable
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ public static NetworkInfo getCurrentNetworkInfo(@Nullable Context context) {
+ if (context == null) {
+ return null;
+ }
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ return connectivityManager.getActiveNetworkInfo();
+ }
+
+ /**
+ * Returns the current network type as a string. For mobile network types the subtype name of the
+ * network is appended.
+ */
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ public static String getCurrentNetworkTypeName(@Nullable Context context) {
+ if (context == null) {
+ return NetworkType.NONE;
+ }
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
+ @NetworkType String networkType = getNetworkType(netInfo);
+ if (isNetworkTypeMobile(networkType)) {
+ return networkType + " (" + netInfo.getSubtypeName() + ")";
+ }
+ return networkType;
+ }
+
+ @NetworkType
+ public static String getNetworkType(@Nullable NetworkInfo netInfo) {
+ if (netInfo == null || !netInfo.isConnected()) {
+ return NetworkType.NONE;
+ }
+ switch (netInfo.getType()) {
+ case ConnectivityManager.TYPE_WIFI:
+ return NetworkType.WIFI;
+ case ConnectivityManager.TYPE_MOBILE:
+ return getMobileNetworkType(netInfo.getSubtype());
+ default:
+ return NetworkType.UNKNOWN;
+ }
+ }
+
+ public static boolean isNetworkTypeMobile(@NetworkType String networkType) {
+ return Objects.equals(networkType, NetworkType.MOBILE_2G)
+ || Objects.equals(networkType, NetworkType.MOBILE_3G)
+ || Objects.equals(networkType, NetworkType.MOBILE_4G);
+ }
+
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ public static String getCurrentNetworkName(Context context) {
+ @NetworkType String networkType = getCurrentNetworkType(context);
+ switch (networkType) {
+ case NetworkType.WIFI:
+ return getWifiNetworkName(context);
+ case NetworkType.MOBILE_2G:
+ case NetworkType.MOBILE_3G:
+ case NetworkType.MOBILE_4G:
+ case NetworkType.MOBILE_UNKNOWN:
+ return getMobileNetworkName(context);
+ default:
+ return "";
+ }
+ }
+
+ private static String getWifiNetworkName(Context context) {
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ String name = null;
+ if (context.checkSelfPermission("android.permission.ACCESS_WIFI_STATE")
+ == PackageManager.PERMISSION_GRANTED) {
+ //noinspection MissingPermission
+ WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
+ if (wifiInfo == null) {
+ return "";
+ }
+ name = wifiInfo.getSSID();
+ }
+ return TextUtils.isEmpty(name)
+ ? context.getString(R.string.network_name_wifi)
+ : name.replaceAll("\"", "");
+ }
+
+ private static String getMobileNetworkName(Context context) {
+ TelephonyManager telephonyMgr =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ String name = telephonyMgr.getNetworkOperatorName();
+ return TextUtils.isEmpty(name)
+ ? context.getString(R.string.network_name_mobile)
+ : name.replaceAll("\"", "");
+ }
+
+ @NetworkType
+ private static String getMobileNetworkType(int networkSubtype) {
+ switch (networkSubtype) {
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return NetworkType.MOBILE_2G;
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ return NetworkType.MOBILE_3G;
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return NetworkType.MOBILE_4G;
+ default:
+ return NetworkType.MOBILE_UNKNOWN;
+ }
+ }
+
+ /** Network types. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ value = {
+ NetworkType.NONE,
+ NetworkType.WIFI,
+ NetworkType.MOBILE_2G,
+ NetworkType.MOBILE_3G,
+ NetworkType.MOBILE_4G,
+ NetworkType.MOBILE_UNKNOWN,
+ NetworkType.UNKNOWN
+ }
+ )
+ public @interface NetworkType {
+
+ String NONE = "NONE";
+ String WIFI = "WIFI";
+ String MOBILE_2G = "MOBILE_2G";
+ String MOBILE_3G = "MOBILE_3G";
+ String MOBILE_4G = "MOBILE_4G";
+ String MOBILE_UNKNOWN = "MOBILE_UNKNOWN";
+ String UNKNOWN = "UNKNOWN";
+ }
+}
diff --git a/java/com/android/dialer/common/UiUtil.java b/java/com/android/dialer/common/UiUtil.java
new file mode 100644
index 0000000..4c4ebea
--- /dev/null
+++ b/java/com/android/dialer/common/UiUtil.java
@@ -0,0 +1,41 @@
+/*
+ * 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.common;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+/** Utility class for commons functions used with Android UI. */
+public class UiUtil {
+
+ /** Hides the android keyboard. */
+ public static void hideKeyboardFrom(Context context, View view) {
+ InputMethodManager imm =
+ (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ /** Opens the android keyboard. */
+ public static void openKeyboardFrom(Context context, View view) {
+ InputMethodManager inputMethodManager =
+ (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.toggleSoftInputFromWindow(
+ view.getApplicationWindowToken(), InputMethodManager.SHOW_FORCED, 0);
+ }
+}
diff --git a/java/com/android/dialer/common/res/values/strings.xml b/java/com/android/dialer/common/res/values/strings.xml
new file mode 100644
index 0000000..8e96161
--- /dev/null
+++ b/java/com/android/dialer/common/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="network_name_wifi">Wifi</string>
+ <string name="network_name_mobile">Mobile</string>
+</resources>
diff --git a/java/com/android/dialer/compat/ActivityCompat.java b/java/com/android/dialer/compat/ActivityCompat.java
new file mode 100644
index 0000000..e59b115
--- /dev/null
+++ b/java/com/android/dialer/compat/ActivityCompat.java
@@ -0,0 +1,29 @@
+/*
+ * 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.compat;
+
+import android.app.Activity;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+/** Utility for calling methods introduced after Marshmallow for Activities. */
+public class ActivityCompat {
+
+ public static boolean isInMultiWindowMode(Activity activity) {
+ return VERSION.SDK_INT >= VERSION_CODES.N && activity.isInMultiWindowMode();
+ }
+}
diff --git a/java/com/android/dialer/compat/AppCompatConstants.java b/java/com/android/dialer/compat/AppCompatConstants.java
new file mode 100644
index 0000000..4a51d3f
--- /dev/null
+++ b/java/com/android/dialer/compat/AppCompatConstants.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.provider.CallLog.Calls;
+
+public final class AppCompatConstants {
+
+ public static final int CALLS_INCOMING_TYPE = Calls.INCOMING_TYPE;
+ public static final int CALLS_OUTGOING_TYPE = Calls.OUTGOING_TYPE;
+ public static final int CALLS_MISSED_TYPE = Calls.MISSED_TYPE;
+ public static final int CALLS_VOICEMAIL_TYPE = Calls.VOICEMAIL_TYPE;
+ // Added to android.provider.CallLog.Calls in N+.
+ public static final int CALLS_REJECTED_TYPE = 5;
+ // Added to android.provider.CallLog.Calls in N+.
+ public static final int CALLS_BLOCKED_TYPE = 6;
+ // Added to android.provider.CallLog.Calls in N+.
+ public static final int CALLS_ANSWERED_EXTERNALLY_TYPE = Calls.ANSWERED_EXTERNALLY_TYPE;
+}
diff --git a/java/com/android/dialer/compat/CompatUtils.java b/java/com/android/dialer/compat/CompatUtils.java
new file mode 100644
index 0000000..673cb70
--- /dev/null
+++ b/java/com/android/dialer/compat/CompatUtils.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+import java.lang.reflect.InvocationTargetException;
+
+public final class CompatUtils {
+
+ private static final String TAG = CompatUtils.class.getSimpleName();
+
+ /** PrioritizedMimeType is added in API level 23. */
+ public static boolean hasPrioritizedMimeType() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is compatible with multi-SIM and the phone account APIs. Can also
+ * force the version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if multi-SIM capability is available, {@code false} otherwise.
+ */
+ public static boolean isMSIMCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+ >= Build.VERSION_CODES.LOLLIPOP_MR1;
+ }
+
+ /**
+ * Determines if this version is compatible with video calling. Can also force the version to be
+ * lower through SdkVersionOverride.
+ *
+ * @return {@code true} if video calling is allowed, {@code false} otherwise.
+ */
+ public static boolean isVideoCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is capable of using presence checking for video calling. Support for
+ * video call presence indication is added in SDK 24.
+ *
+ * @return {@code true} if video presence checking is allowed, {@code false} otherwise.
+ */
+ public static boolean isVideoPresenceCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) > Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is compatible with call subject. Can also force the version to be
+ * lower through SdkVersionOverride.
+ *
+ * @return {@code true} if call subject is a feature on this device, {@code false} otherwise.
+ */
+ public static boolean isCallSubjectCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is compatible with a default dialer. Can also force the version to
+ * be lower through {@link SdkVersionOverride}.
+ *
+ * @return {@code true} if default dialer is a feature on this device, {@code false} otherwise.
+ */
+ public static boolean isDefaultDialerCompatible() {
+ return isMarshmallowCompatible();
+ }
+
+ /**
+ * Determines if this version is compatible with Lollipop Mr1-specific APIs. Can also force the
+ * version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if runtime sdk is compatible with Lollipop MR1, {@code false} otherwise.
+ */
+ public static boolean isLollipopMr1Compatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP_MR1)
+ >= Build.VERSION_CODES.LOLLIPOP_MR1;
+ }
+
+ /**
+ * Determines if this version is compatible with Marshmallow-specific APIs. Can also force the
+ * version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if runtime sdk is compatible with Marshmallow, {@code false} otherwise.
+ */
+ public static boolean isMarshmallowCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if the given class is available. Can be used to check if system apis exist at
+ * runtime.
+ *
+ * @param className the name of the class to look for.
+ * @return {@code true} if the given class is available, {@code false} otherwise or if className
+ * is empty.
+ */
+ public static boolean isClassAvailable(@Nullable String className) {
+ if (TextUtils.isEmpty(className)) {
+ return false;
+ }
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when checking if class:" + className + " exists at " + "runtime",
+ t);
+ return false;
+ }
+ }
+
+ /**
+ * Determines if the given class's method is available to call. Can be used to check if system
+ * apis exist at runtime.
+ *
+ * @param className the name of the class to look for
+ * @param methodName the name of the method to look for
+ * @param parameterTypes the needed parameter types for the method to look for
+ * @return {@code true} if the given class is available, {@code false} otherwise or if className
+ * or methodName are empty.
+ */
+ public static boolean isMethodAvailable(
+ @Nullable String className, @Nullable String methodName, Class<?>... parameterTypes) {
+ if (TextUtils.isEmpty(className) || TextUtils.isEmpty(methodName)) {
+ return false;
+ }
+
+ try {
+ Class.forName(className).getMethod(methodName, parameterTypes);
+ return true;
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ Log.v(TAG, "Could not find method: " + className + "#" + methodName);
+ return false;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when checking if method: "
+ + className
+ + "#"
+ + methodName
+ + " exists at runtime",
+ t);
+ return false;
+ }
+ }
+
+ /**
+ * Invokes a given class's method using reflection. Can be used to call system apis that exist at
+ * runtime but not in the SDK.
+ *
+ * @param instance The instance of the class to invoke the method on.
+ * @param methodName The name of the method to invoke.
+ * @param parameterTypes The needed parameter types for the method.
+ * @param parameters The parameter values to pass into the method.
+ * @return The result of the invocation or {@code null} if instance or methodName are empty, or if
+ * the reflection fails.
+ */
+ @Nullable
+ public static Object invokeMethod(
+ @Nullable Object instance,
+ @Nullable String methodName,
+ Class<?>[] parameterTypes,
+ Object[] parameters) {
+ if (instance == null || TextUtils.isEmpty(methodName)) {
+ return null;
+ }
+
+ String className = instance.getClass().getName();
+ try {
+ return Class.forName(className)
+ .getMethod(methodName, parameterTypes)
+ .invoke(instance, parameters);
+ } catch (ClassNotFoundException
+ | NoSuchMethodException
+ | IllegalArgumentException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ Log.v(TAG, "Could not invoke method: " + className + "#" + methodName);
+ return null;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when invoking method: "
+ + className
+ + "#"
+ + methodName
+ + " at runtime",
+ t);
+ return null;
+ }
+ }
+
+ /**
+ * Determines if this version is compatible with Lollipop-specific APIs. Can also force the
+ * version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if call subject is a feature on this device, {@code false} otherwise.
+ */
+ public static boolean isLollipopCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+ >= Build.VERSION_CODES.LOLLIPOP;
+ }
+}
diff --git a/java/com/android/dialer/compat/PathInterpolatorCompat.java b/java/com/android/dialer/compat/PathInterpolatorCompat.java
new file mode 100644
index 0000000..7139bc4
--- /dev/null
+++ b/java/com/android/dialer/compat/PathInterpolatorCompat.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.os.Build;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+public class PathInterpolatorCompat {
+
+ public static Interpolator create(
+ float controlX1, float controlY1, float controlX2, float controlY2) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
+ }
+ return new PathInterpolatorBase(controlX1, controlY1, controlX2, controlY2);
+ }
+
+ private static class PathInterpolatorBase implements Interpolator {
+
+ /** Governs the accuracy of the approximation of the {@link Path}. */
+ private static final float PRECISION = 0.002f;
+
+ private final float[] mX;
+ private final float[] mY;
+
+ public PathInterpolatorBase(Path path) {
+ final PathMeasure pathMeasure = new PathMeasure(path, false /* forceClosed */);
+
+ final float pathLength = pathMeasure.getLength();
+ final int numPoints = (int) (pathLength / PRECISION) + 1;
+
+ mX = new float[numPoints];
+ mY = new float[numPoints];
+
+ final float[] position = new float[2];
+ for (int i = 0; i < numPoints; ++i) {
+ final float distance = (i * pathLength) / (numPoints - 1);
+ pathMeasure.getPosTan(distance, position, null /* tangent */);
+
+ mX[i] = position[0];
+ mY[i] = position[1];
+ }
+ }
+
+ public PathInterpolatorBase(float controlX, float controlY) {
+ this(createQuad(controlX, controlY));
+ }
+
+ public PathInterpolatorBase(
+ float controlX1, float controlY1, float controlX2, float controlY2) {
+ this(createCubic(controlX1, controlY1, controlX2, controlY2));
+ }
+
+ private static Path createQuad(float controlX, float controlY) {
+ final Path path = new Path();
+ path.moveTo(0.0f, 0.0f);
+ path.quadTo(controlX, controlY, 1.0f, 1.0f);
+ return path;
+ }
+
+ private static Path createCubic(
+ float controlX1, float controlY1, float controlX2, float controlY2) {
+ final Path path = new Path();
+ path.moveTo(0.0f, 0.0f);
+ path.cubicTo(controlX1, controlY1, controlX2, controlY2, 1.0f, 1.0f);
+ return path;
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ if (t <= 0.0f) {
+ return 0.0f;
+ } else if (t >= 1.0f) {
+ return 1.0f;
+ }
+
+ // Do a binary search for the correct x to interpolate between.
+ int startIndex = 0;
+ int endIndex = mX.length - 1;
+ while (endIndex - startIndex > 1) {
+ int midIndex = (startIndex + endIndex) / 2;
+ if (t < mX[midIndex]) {
+ endIndex = midIndex;
+ } else {
+ startIndex = midIndex;
+ }
+ }
+
+ final float xRange = mX[endIndex] - mX[startIndex];
+ if (xRange == 0) {
+ return mY[startIndex];
+ }
+
+ final float tInRange = t - mX[startIndex];
+ final float fraction = tInRange / xRange;
+
+ final float startY = mY[startIndex];
+ final float endY = mY[endIndex];
+
+ return startY + (fraction * (endY - startY));
+ }
+ }
+}
diff --git a/java/com/android/dialer/compat/SdkVersionOverride.java b/java/com/android/dialer/compat/SdkVersionOverride.java
new file mode 100644
index 0000000..1d253a3
--- /dev/null
+++ b/java/com/android/dialer/compat/SdkVersionOverride.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.os.Build.VERSION;
+
+/**
+ * Class used to override the current sdk version to test specific branches of compatibility logic.
+ * When such branching occurs, use {@link #getSdkVersion(int)} rather than explicitly calling {@link
+ * VERSION#SDK_INT}. This allows the sdk version to be forced to a specific value.
+ */
+public class SdkVersionOverride {
+
+ /** Flag used to determine if override sdk versions are returned. */
+ private static final boolean ALLOW_OVERRIDE_VERSION = false;
+
+ private SdkVersionOverride() {}
+
+ /**
+ * Gets the sdk version
+ *
+ * @param overrideVersion the version to attempt using
+ * @return overrideVersion if the {@link #ALLOW_OVERRIDE_VERSION} flag is set to {@code true},
+ * otherwise the current version
+ */
+ public static int getSdkVersion(int overrideVersion) {
+ return ALLOW_OVERRIDE_VERSION ? overrideVersion : VERSION.SDK_INT;
+ }
+}
diff --git a/java/com/android/dialer/constants/Constants.java b/java/com/android/dialer/constants/Constants.java
new file mode 100644
index 0000000..7777301
--- /dev/null
+++ b/java/com/android/dialer/constants/Constants.java
@@ -0,0 +1,47 @@
+/*
+ * 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.constants;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.dialer.proguard.UsedByReflection;
+import com.android.dialer.constants.ConstantsImpl;
+
+/**
+ * Utility to access constants that are different across build variants (Google Dialer, AOSP,
+ * etc...). This functionality depends on a an implementation being present in the app that has the
+ * same package and the class name ending in "Impl". For example,
+ * com.android.dialer.constants.ConstantsImpl. This class is found by the module using reflection.
+ */
+@UsedByReflection(value = "Constants.java")
+public abstract class Constants {
+ private static Constants instance = new ConstantsImpl();
+ private static boolean didInitializeInstance;
+
+ @NonNull
+ public static synchronized Constants get() {
+ return instance;
+ }
+
+ @NonNull
+ public abstract String getFilteredNumberProviderAuthority();
+
+ @NonNull
+ public abstract String getFileProviderAuthority();
+
+ protected Constants() {}
+}
diff --git a/java/com/android/dialer/constants/ScheduledJobIds.java b/java/com/android/dialer/constants/ScheduledJobIds.java
new file mode 100644
index 0000000..88e9a3d
--- /dev/null
+++ b/java/com/android/dialer/constants/ScheduledJobIds.java
@@ -0,0 +1,31 @@
+/*
+ * 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.constants;
+
+/**
+ * Registry of scheduled job ids used by the dialer UID.
+ *
+ * <p>Any dialer jobs which use the android JobScheduler should register their IDs here, to avoid
+ * the same ID accidentally being reused.
+ */
+public final class ScheduledJobIds {
+ public static final int SPAM_JOB_WIFI = 50;
+ public static final int SPAM_JOB_ANY_NETWORK = 51;
+
+ // This job refreshes dynamic launcher shortcuts.
+ public static final int SHORTCUT_PERIODIC_JOB = 100;
+}
diff --git a/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java
new file mode 100644
index 0000000..6b78b98
--- /dev/null
+++ b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.constants;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.proguard.UsedByReflection;
+
+/** Provider config values for AOSP Dialer. */
+@UsedByReflection(value = "Constants.java")
+public class ConstantsImpl extends Constants {
+
+ @Override
+ @NonNull
+ public String getFilteredNumberProviderAuthority() {
+ return "com.android.dialer.blocking.filterednumberprovider";
+ }
+
+ @Override
+ @NonNull
+ public String getFileProviderAuthority() {
+ return "com.android.dialer.files";
+ }
+}
diff --git a/java/com/android/dialer/database/CallLogQueryHandler.java b/java/com/android/dialer/database/CallLogQueryHandler.java
new file mode 100644
index 0000000..ffca69f
--- /dev/null
+++ b/java/com/android/dialer/database/CallLogQueryHandler.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2011 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.database;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract.Status;
+import android.provider.VoicemailContract.Voicemails;
+import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.AppCompatConstants;
+import com.android.dialer.compat.SdkVersionOverride;
+import com.android.dialer.phonenumbercache.CallLogQuery;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Handles asynchronous queries to the call log. */
+public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
+
+ /**
+ * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
+ * type. Exception: excludes Calls.VOICEMAIL_TYPE.
+ */
+ public static final int CALL_TYPE_ALL = -1;
+
+ private static final String TAG = "CallLogQueryHandler";
+ private static final int NUM_LOGS_TO_DISPLAY = 1000;
+ /** The token for the query to fetch the old entries from the call log. */
+ private static final int QUERY_CALLLOG_TOKEN = 54;
+ /** The token for the query to mark all missed calls as old after seeing the call log. */
+ private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
+ /** The token for the query to mark all missed calls as read after seeing the call log. */
+ private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 56;
+ /** The token for the query to fetch voicemail status messages. */
+ private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
+ /** The token for the query to fetch the number of unread voicemails. */
+ private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
+ /** The token for the query to fetch the number of missed calls. */
+ private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
+
+ private final int mLogLimit;
+ private final WeakReference<Listener> mListener;
+
+ private final Context mContext;
+
+ public CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener) {
+ this(context, contentResolver, listener, -1);
+ }
+
+ public CallLogQueryHandler(
+ Context context, ContentResolver contentResolver, Listener listener, int limit) {
+ super(contentResolver);
+ mContext = context.getApplicationContext();
+ mListener = new WeakReference<Listener>(listener);
+ mLogLimit = limit;
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ // Provide our special handler that catches exceptions
+ return new CatchingWorkerHandler(looper);
+ }
+
+ /**
+ * Fetches the list of calls from the call log for a given type. This call ignores the new or old
+ * state.
+ *
+ * <p>It will asynchronously update the content of the list view when the fetch completes.
+ */
+ public void fetchCalls(int callType, long newerThan) {
+ cancelFetch();
+ if (PermissionsUtil.hasPhonePermissions(mContext)) {
+ fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan);
+ } else {
+ updateAdapterData(null);
+ }
+ }
+
+ public void fetchCalls(int callType) {
+ fetchCalls(callType, 0);
+ }
+
+ public void fetchVoicemailStatus() {
+ if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
+ startQuery(
+ QUERY_VOICEMAIL_STATUS_TOKEN,
+ null,
+ Status.CONTENT_URI,
+ VoicemailStatusQuery.getProjection(),
+ null,
+ null,
+ null);
+ }
+ }
+
+ public void fetchVoicemailUnreadCount() {
+ if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
+ // Only count voicemails that have not been read and have not been deleted.
+ startQuery(
+ QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN,
+ null,
+ Voicemails.CONTENT_URI,
+ new String[] {Voicemails._ID},
+ Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0",
+ null,
+ null);
+ }
+ }
+
+ /** Fetches the list of calls in the call log. */
+ private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
+ StringBuilder where = new StringBuilder();
+ List<String> selectionArgs = new ArrayList<>();
+
+ // Always hide blocked calls.
+ where.append("(").append(Calls.TYPE).append(" != ?)");
+ selectionArgs.add(Integer.toString(AppCompatConstants.CALLS_BLOCKED_TYPE));
+
+ // Ignore voicemails marked as deleted
+ if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+ where.append(" AND (").append(Voicemails.DELETED).append(" = 0)");
+ }
+
+ if (newOnly) {
+ where.append(" AND (").append(Calls.NEW).append(" = 1)");
+ }
+
+ if (callType > CALL_TYPE_ALL) {
+ where.append(" AND (").append(Calls.TYPE).append(" = ?)");
+ selectionArgs.add(Integer.toString(callType));
+ } else {
+ where.append(" AND NOT ");
+ where.append("(" + Calls.TYPE + " = " + AppCompatConstants.CALLS_VOICEMAIL_TYPE + ")");
+ }
+
+ if (newerThan > 0) {
+ where.append(" AND (").append(Calls.DATE).append(" > ?)");
+ selectionArgs.add(Long.toString(newerThan));
+ }
+
+ final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
+ final String selection = where.length() > 0 ? where.toString() : null;
+ Uri uri =
+ TelecomUtil.getCallLogUri(mContext)
+ .buildUpon()
+ .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
+ .build();
+ startQuery(
+ token,
+ null,
+ uri,
+ CallLogQuery.getProjection(),
+ selection,
+ selectionArgs.toArray(new String[selectionArgs.size()]),
+ Calls.DEFAULT_SORT_ORDER);
+ }
+
+ /** Cancel any pending fetch request. */
+ private void cancelFetch() {
+ cancelOperation(QUERY_CALLLOG_TOKEN);
+ }
+
+ /** Updates all new calls to mark them as old. */
+ public void markNewCallsAsOld() {
+ if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+ return;
+ }
+ // Mark all "new" calls as not new anymore.
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1");
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.NEW, "0");
+
+ startUpdate(
+ UPDATE_MARK_AS_OLD_TOKEN,
+ null,
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ where.toString(),
+ null);
+ }
+
+ /** Updates all missed calls to mark them as read. */
+ public void markMissedCallsAsRead() {
+ if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+ return;
+ }
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.IS_READ, "1");
+
+ startUpdate(
+ UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN,
+ null,
+ Calls.CONTENT_URI,
+ values,
+ getUnreadMissedCallsQuery(),
+ null);
+ }
+
+ /** Fetch all missed calls received since last time the tab was opened. */
+ public void fetchMissedCallsUnreadCount() {
+ if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+ return;
+ }
+
+ startQuery(
+ QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN,
+ null,
+ Calls.CONTENT_URI,
+ new String[] {Calls._ID},
+ getUnreadMissedCallsQuery(),
+ null,
+ null);
+ }
+
+ @Override
+ protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+ try {
+ if (token == QUERY_CALLLOG_TOKEN) {
+ if (updateAdapterData(cursor)) {
+ cursor = null;
+ }
+ } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
+ updateVoicemailStatus(cursor);
+ } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
+ updateVoicemailUnreadCount(cursor);
+ } else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) {
+ updateMissedCallsUnreadCount(cursor);
+ } else {
+ LogUtil.w(
+ "CallLogQueryHandler.onNotNullableQueryComplete",
+ "unknown query completed: ignoring: " + token);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Updates the adapter in the call log fragment to show the new cursor data. Returns true if the
+ * listener took ownership of the cursor.
+ */
+ private boolean updateAdapterData(Cursor cursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ return listener.onCallsFetched(cursor);
+ }
+ return false;
+ }
+
+ /** @return Query string to get all unread missed calls. */
+ private String getUnreadMissedCallsQuery() {
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.IS_READ).append(" = 0 OR ").append(Calls.IS_READ).append(" IS NULL");
+ where.append(" AND ");
+ where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
+ return where.toString();
+ }
+
+ private void updateVoicemailStatus(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onVoicemailStatusFetched(statusCursor);
+ }
+ }
+
+ private void updateVoicemailUnreadCount(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onVoicemailUnreadCountFetched(statusCursor);
+ }
+ }
+
+ private void updateMissedCallsUnreadCount(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onMissedCallsUnreadCountFetched(statusCursor);
+ }
+ }
+
+ /** Listener to completion of various queries. */
+ public interface Listener {
+
+ /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
+ void onVoicemailStatusFetched(Cursor statusCursor);
+
+ /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */
+ void onVoicemailUnreadCountFetched(Cursor cursor);
+
+ /** Called when {@link CallLogQueryHandler#fetchMissedCallsUnreadCount()} completes. */
+ void onMissedCallsUnreadCountFetched(Cursor cursor);
+
+ /**
+ * Called when {@link CallLogQueryHandler#fetchCalls(int)} complete. Returns true if takes
+ * ownership of cursor.
+ */
+ boolean onCallsFetched(Cursor combinedCursor);
+ }
+
+ /**
+ * Simple handler that wraps background calls to catch {@link SQLiteException}, such as when the
+ * disk is full.
+ */
+ protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
+
+ public CatchingWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ try {
+ // Perform same query while catching any exceptions
+ super.handleMessage(msg);
+ } catch (SQLiteDiskIOException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "exception on background worker thread", e);
+ } catch (SQLiteFullException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "exception on background worker thread", e);
+ } catch (SQLiteDatabaseCorruptException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "exception on background worker thread", e);
+ } catch (IllegalArgumentException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "contactsProvider not present on device", e);
+ } catch (SecurityException e) {
+ // Shouldn't happen if we are protecting the entry points correctly,
+ // but just in case.
+ LogUtil.e(
+ "CallLogQueryHandler.handleMessage", "no permission to access ContactsProvider.", e);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/database/Database.java b/java/com/android/dialer/database/Database.java
new file mode 100644
index 0000000..d13f15e
--- /dev/null
+++ b/java/com/android/dialer/database/Database.java
@@ -0,0 +1,49 @@
+/*
+ * 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.database;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Accessor for the database bindings. */
+public class Database {
+
+ private static DatabaseBindings databaseBindings;
+
+ private Database() {}
+
+ public static DatabaseBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (databaseBindings != null) {
+ return databaseBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof DatabaseBindingsFactory) {
+ databaseBindings = ((DatabaseBindingsFactory) application).newDatabaseBindings();
+ }
+
+ if (databaseBindings == null) {
+ databaseBindings = new DatabaseBindingsStub();
+ }
+ return databaseBindings;
+ }
+
+ public static void setForTesting(DatabaseBindings databaseBindings) {
+ Database.databaseBindings = databaseBindings;
+ }
+}
diff --git a/java/com/android/dialer/database/DatabaseBindings.java b/java/com/android/dialer/database/DatabaseBindings.java
new file mode 100644
index 0000000..f07b265
--- /dev/null
+++ b/java/com/android/dialer/database/DatabaseBindings.java
@@ -0,0 +1,25 @@
+/*
+ * 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.database;
+
+import android.content.Context;
+
+/** This interface allows the container application to customize the database module. */
+public interface DatabaseBindings {
+
+ DialerDatabaseHelper getDatabaseHelper(Context context);
+}
diff --git a/java/com/android/dialer/database/DatabaseBindingsFactory.java b/java/com/android/dialer/database/DatabaseBindingsFactory.java
new file mode 100644
index 0000000..7fa175e
--- /dev/null
+++ b/java/com/android/dialer/database/DatabaseBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.database;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the dialer module
+ * to get references to the DatabaseBindings.
+ */
+public interface DatabaseBindingsFactory {
+
+ DatabaseBindings newDatabaseBindings();
+}
diff --git a/java/com/android/dialer/database/DatabaseBindingsStub.java b/java/com/android/dialer/database/DatabaseBindingsStub.java
new file mode 100644
index 0000000..df8186a
--- /dev/null
+++ b/java/com/android/dialer/database/DatabaseBindingsStub.java
@@ -0,0 +1,35 @@
+/*
+ * 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.database;
+
+import android.content.Context;
+
+/** Default implementation for database bindings. */
+public class DatabaseBindingsStub implements DatabaseBindings {
+
+ private DialerDatabaseHelper dialerDatabaseHelper;
+
+ @Override
+ public DialerDatabaseHelper getDatabaseHelper(Context context) {
+ if (dialerDatabaseHelper == null) {
+ dialerDatabaseHelper =
+ new DialerDatabaseHelper(
+ context, DialerDatabaseHelper.DATABASE_NAME, DialerDatabaseHelper.DATABASE_VERSION);
+ }
+ return dialerDatabaseHelper;
+ }
+}
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
new file mode 100644
index 0000000..234958b
--- /dev/null
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -0,0 +1,1242 @@
+/*
+ * Copyright (C) 2013 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.database;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.StopWatch;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.smartdial.SmartDialNameMatcher;
+import com.android.dialer.smartdial.SmartDialPrefix;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Database helper for smart dial. Designed as a singleton to make sure there is only one access
+ * point to the database. Provides methods to maintain, update, and query the database.
+ */
+public class DialerDatabaseHelper extends SQLiteOpenHelper {
+
+ /**
+ * SmartDial DB version ranges:
+ *
+ * <pre>
+ * 0-98 KitKat
+ * </pre>
+ */
+ public static final int DATABASE_VERSION = 10;
+
+ public static final String DATABASE_NAME = "dialer.db";
+ public static final Uri SMART_DIAL_UPDATED_URI =
+ Uri.parse("content://com.android.dialer/smart_dial_updated");
+ private static final String TAG = "DialerDatabaseHelper";
+ private static final boolean DEBUG = false;
+ /** Saves the last update time of smart dial databases to shared preferences. */
+ private static final String DATABASE_LAST_CREATED_SHARED_PREF = "com.android.dialer";
+
+ private static final String LAST_UPDATED_MILLIS = "last_updated_millis";
+ private static final String DATABASE_VERSION_PROPERTY = "database_version";
+ private static final int MAX_ENTRIES = 20;
+
+ private final Context mContext;
+ private final Object mLock = new Object();
+ private final AtomicBoolean mInUpdate = new AtomicBoolean(false);
+ private boolean mIsTestInstance = false;
+
+ protected DialerDatabaseHelper(Context context, String databaseName, int dbVersion) {
+ super(context, databaseName, null, dbVersion);
+ mContext = Objects.requireNonNull(context, "Context must not be null");
+ }
+
+ public void setIsTestInstance(boolean isTestInstance) {
+ mIsTestInstance = isTestInstance;
+ }
+
+ /**
+ * Creates tables in the database when database is created for the first time.
+ *
+ * @param db The database.
+ */
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ setupTables(db);
+ }
+
+ private void setupTables(SQLiteDatabase db) {
+ dropTables(db);
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + SmartDialDbColumns.DATA_ID
+ + " INTEGER, "
+ + SmartDialDbColumns.NUMBER
+ + " TEXT,"
+ + SmartDialDbColumns.CONTACT_ID
+ + " INTEGER,"
+ + SmartDialDbColumns.LOOKUP_KEY
+ + " TEXT,"
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + " TEXT, "
+ + SmartDialDbColumns.PHOTO_ID
+ + " INTEGER, "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " LONG, "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + " LONG, "
+ + SmartDialDbColumns.TIMES_USED
+ + " INTEGER, "
+ + SmartDialDbColumns.STARRED
+ + " INTEGER, "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + " INTEGER, "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + " INTEGER, "
+ + SmartDialDbColumns.IS_PRIMARY
+ + " INTEGER, "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + " INTEGER NOT NULL DEFAULT 0"
+ + ");");
+
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + PrefixColumns.PREFIX
+ + " TEXT COLLATE NOCASE, "
+ + PrefixColumns.CONTACT_ID
+ + " INTEGER"
+ + ");");
+
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.PROPERTIES
+ + " ("
+ + PropertiesColumns.PROPERTY_KEY
+ + " TEXT PRIMARY KEY, "
+ + PropertiesColumns.PROPERTY_VALUE
+ + " TEXT "
+ + ");");
+
+ // This will need to also be updated in setupTablesForFilteredNumberTest and onUpgrade.
+ // Hardcoded so we know on glance what columns are updated in setupTables,
+ // and to be able to guarantee the state of the DB at each upgrade step.
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.FILTERED_NUMBER_TABLE
+ + " ("
+ + FilteredNumberColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + FilteredNumberColumns.NORMALIZED_NUMBER
+ + " TEXT UNIQUE,"
+ + FilteredNumberColumns.NUMBER
+ + " TEXT,"
+ + FilteredNumberColumns.COUNTRY_ISO
+ + " TEXT,"
+ + FilteredNumberColumns.TIMES_FILTERED
+ + " INTEGER,"
+ + FilteredNumberColumns.LAST_TIME_FILTERED
+ + " LONG,"
+ + FilteredNumberColumns.CREATION_TIME
+ + " LONG,"
+ + FilteredNumberColumns.TYPE
+ + " INTEGER,"
+ + FilteredNumberColumns.SOURCE
+ + " INTEGER"
+ + ");");
+
+ setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+ if (!mIsTestInstance) {
+ resetSmartDialLastUpdatedTime();
+ }
+ }
+
+ public void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PREFIX_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.SMARTDIAL_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PROPERTIES);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldNumber, int newNumber) {
+ // Disregard the old version and new versions provided by SQLiteOpenHelper, we will read
+ // our own from the database.
+
+ int oldVersion;
+
+ oldVersion = getPropertyAsInt(db, DATABASE_VERSION_PROPERTY, 0);
+
+ if (oldVersion == 0) {
+ LogUtil.e(
+ "DialerDatabaseHelper.onUpgrade", "malformed database version..recreating database");
+ }
+
+ if (oldVersion < 4) {
+ setupTables(db);
+ return;
+ }
+
+ if (oldVersion < 7) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE);
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.FILTERED_NUMBER_TABLE
+ + " ("
+ + FilteredNumberColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + FilteredNumberColumns.NORMALIZED_NUMBER
+ + " TEXT UNIQUE,"
+ + FilteredNumberColumns.NUMBER
+ + " TEXT,"
+ + FilteredNumberColumns.COUNTRY_ISO
+ + " TEXT,"
+ + FilteredNumberColumns.TIMES_FILTERED
+ + " INTEGER,"
+ + FilteredNumberColumns.LAST_TIME_FILTERED
+ + " LONG,"
+ + FilteredNumberColumns.CREATION_TIME
+ + " LONG,"
+ + FilteredNumberColumns.TYPE
+ + " INTEGER,"
+ + FilteredNumberColumns.SOURCE
+ + " INTEGER"
+ + ");");
+ oldVersion = 7;
+ }
+
+ if (oldVersion < 8) {
+ upgradeToVersion8(db);
+ oldVersion = 8;
+ }
+
+ if (oldVersion < 10) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE);
+ oldVersion = 10;
+ }
+
+ if (oldVersion != DATABASE_VERSION) {
+ throw new IllegalStateException(
+ "error upgrading the database to version " + DATABASE_VERSION);
+ }
+
+ setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+ }
+
+ public void upgradeToVersion8(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE smartdial_table ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+ }
+
+ /** Stores a key-value pair in the {@link Tables#PROPERTIES} table. */
+ public void setProperty(String key, String value) {
+ setProperty(getWritableDatabase(), key, value);
+ }
+
+ public void setProperty(SQLiteDatabase db, String key, String value) {
+ final ContentValues values = new ContentValues();
+ values.put(PropertiesColumns.PROPERTY_KEY, key);
+ values.put(PropertiesColumns.PROPERTY_VALUE, value);
+ db.replace(Tables.PROPERTIES, null, values);
+ }
+
+ /** Returns the value from the {@link Tables#PROPERTIES} table. */
+ public String getProperty(String key, String defaultValue) {
+ return getProperty(getReadableDatabase(), key, defaultValue);
+ }
+
+ public String getProperty(SQLiteDatabase db, String key, String defaultValue) {
+ try {
+ String value = null;
+ final Cursor cursor =
+ db.query(
+ Tables.PROPERTIES,
+ new String[] {PropertiesColumns.PROPERTY_VALUE},
+ PropertiesColumns.PROPERTY_KEY + "=?",
+ new String[] {key},
+ null,
+ null,
+ null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ value = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return value != null ? value : defaultValue;
+ } catch (SQLiteException e) {
+ return defaultValue;
+ }
+ }
+
+ public int getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue) {
+ final String stored = getProperty(db, key, "");
+ try {
+ return Integer.parseInt(stored);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ private void resetSmartDialLastUpdatedTime() {
+ final SharedPreferences databaseLastUpdateSharedPref =
+ mContext.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
+ final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
+ editor.putLong(LAST_UPDATED_MILLIS, 0);
+ editor.apply();
+ }
+
+ /** Starts the database upgrade process in the background. */
+ public void startSmartDialUpdateThread() {
+ if (PermissionsUtil.hasContactsPermissions(mContext)) {
+ new SmartDialUpdateAsyncTask().execute();
+ }
+ }
+
+ /**
+ * Removes rows in the smartdial database that matches the contacts that have been deleted by
+ * other apps since last update.
+ *
+ * @param db Database to operate on.
+ * @param deletedContactCursor Cursor containing rows of deleted contacts
+ */
+ @VisibleForTesting
+ void removeDeletedContacts(SQLiteDatabase db, Cursor deletedContactCursor) {
+ if (deletedContactCursor == null) {
+ return;
+ }
+
+ db.beginTransaction();
+ try {
+ while (deletedContactCursor.moveToNext()) {
+ final Long deleteContactId =
+ deletedContactCursor.getLong(DeleteContactQuery.DELETED_CONTACT_ID);
+ db.delete(
+ Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" + deleteContactId, null);
+ db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" + deleteContactId, null);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ deletedContactCursor.close();
+ db.endTransaction();
+ }
+ }
+
+ private Cursor getDeletedContactCursor(String lastUpdateMillis) {
+ return mContext
+ .getContentResolver()
+ .query(
+ DeleteContactQuery.URI,
+ DeleteContactQuery.PROJECTION,
+ DeleteContactQuery.SELECT_UPDATED_CLAUSE,
+ new String[] {lastUpdateMillis},
+ null);
+ }
+
+ /**
+ * Removes potentially corrupted entries in the database. These contacts may be added before the
+ * previous instance of the dialer was destroyed for some reason. For data integrity, we delete
+ * all of them.
+ *
+ * @param db Database pointer to the dialer database.
+ * @param last_update_time Time stamp of last successful update of the dialer database.
+ */
+ private void removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time) {
+ db.delete(
+ Tables.PREFIX_TABLE,
+ PrefixColumns.CONTACT_ID
+ + " IN "
+ + "(SELECT "
+ + SmartDialDbColumns.CONTACT_ID
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " > "
+ + last_update_time
+ + ")",
+ null);
+ db.delete(
+ Tables.SMARTDIAL_TABLE,
+ SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " + last_update_time,
+ null);
+ }
+
+ /**
+ * Removes rows in the smartdial database that matches updated contacts.
+ *
+ * @param db Database pointer to the smartdial database
+ * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
+ */
+ @VisibleForTesting
+ void removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor) {
+ db.beginTransaction();
+ try {
+ updatedContactCursor.moveToPosition(-1);
+ while (updatedContactCursor.moveToNext()) {
+ final Long contactId = updatedContactCursor.getLong(UpdatedContactQuery.UPDATED_CONTACT_ID);
+
+ db.delete(Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" + contactId, null);
+ db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" + contactId, null);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Inserts updated contacts as rows to the smartdial table.
+ *
+ * @param db Database pointer to the smartdial database.
+ * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
+ * @param currentMillis Current time to be recorded in the smartdial table as update timestamp.
+ */
+ @VisibleForTesting
+ protected void insertUpdatedContactsAndNumberPrefix(
+ SQLiteDatabase db, Cursor updatedContactCursor, Long currentMillis) {
+ db.beginTransaction();
+ try {
+ final String sqlInsert =
+ "INSERT INTO "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.DATA_ID
+ + ", "
+ + SmartDialDbColumns.NUMBER
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.LOOKUP_KEY
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.PHOTO_ID
+ + ", "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + ", "
+ + SmartDialDbColumns.TIMES_USED
+ + ", "
+ + SmartDialDbColumns.STARRED
+ + ", "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + ", "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + ", "
+ + SmartDialDbColumns.IS_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + ", "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + ") "
+ + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ final SQLiteStatement insert = db.compileStatement(sqlInsert);
+
+ final String numberSqlInsert =
+ "INSERT INTO "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ", "
+ + PrefixColumns.PREFIX
+ + ") "
+ + " VALUES (?, ?)";
+ final SQLiteStatement numberInsert = db.compileStatement(numberSqlInsert);
+
+ updatedContactCursor.moveToPosition(-1);
+ while (updatedContactCursor.moveToNext()) {
+ insert.clearBindings();
+
+ // Handle string columns which can possibly be null first. In the case of certain
+ // null columns (due to malformed rows possibly inserted by third-party apps
+ // or sync adapters), skip the phone number row.
+ final String number = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
+ if (TextUtils.isEmpty(number)) {
+ continue;
+ } else {
+ insert.bindString(2, number);
+ }
+
+ final String lookupKey = updatedContactCursor.getString(PhoneQuery.PHONE_LOOKUP_KEY);
+ if (TextUtils.isEmpty(lookupKey)) {
+ continue;
+ } else {
+ insert.bindString(4, lookupKey);
+ }
+
+ final String displayName = updatedContactCursor.getString(PhoneQuery.PHONE_DISPLAY_NAME);
+ if (displayName == null) {
+ insert.bindString(5, mContext.getResources().getString(R.string.missing_name));
+ } else {
+ insert.bindString(5, displayName);
+ }
+ insert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID));
+ insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
+ insert.bindLong(6, updatedContactCursor.getLong(PhoneQuery.PHONE_PHOTO_ID));
+ insert.bindLong(7, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED));
+ insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED));
+ insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED));
+ insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY));
+ insert.bindLong(11, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP));
+ insert.bindLong(12, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY));
+ insert.bindLong(13, updatedContactCursor.getInt(PhoneQuery.PHONE_CARRIER_PRESENCE));
+ insert.bindLong(14, currentMillis);
+ insert.executeInsert();
+ final String contactPhoneNumber = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
+ final ArrayList<String> numberPrefixes =
+ SmartDialPrefix.parseToNumberTokens(contactPhoneNumber);
+
+ for (String numberPrefix : numberPrefixes) {
+ numberInsert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
+ numberInsert.bindString(2, numberPrefix);
+ numberInsert.executeInsert();
+ numberInsert.clearBindings();
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Inserts prefixes of contact names to the prefix table.
+ *
+ * @param db Database pointer to the smartdial database.
+ * @param nameCursor Cursor pointing to the list of distinct updated contacts.
+ */
+ @VisibleForTesting
+ void insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor) {
+ final int columnIndexName = nameCursor.getColumnIndex(SmartDialDbColumns.DISPLAY_NAME_PRIMARY);
+ final int columnIndexContactId = nameCursor.getColumnIndex(SmartDialDbColumns.CONTACT_ID);
+
+ db.beginTransaction();
+ try {
+ final String sqlInsert =
+ "INSERT INTO "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ", "
+ + PrefixColumns.PREFIX
+ + ") "
+ + " VALUES (?, ?)";
+ final SQLiteStatement insert = db.compileStatement(sqlInsert);
+
+ while (nameCursor.moveToNext()) {
+ /** Computes a list of prefixes of a given contact name. */
+ final ArrayList<String> namePrefixes =
+ SmartDialPrefix.generateNamePrefixes(nameCursor.getString(columnIndexName));
+
+ for (String namePrefix : namePrefixes) {
+ insert.bindLong(1, nameCursor.getLong(columnIndexContactId));
+ insert.bindString(2, namePrefix);
+ insert.executeInsert();
+ insert.clearBindings();
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Updates the smart dial and prefix database. This method queries the Delta API to get changed
+ * contacts since last update, and updates the records in smartdial database and prefix database
+ * accordingly. It also queries the deleted contact database to remove newly deleted contacts
+ * since last update.
+ */
+ public void updateSmartDialDatabase() {
+ final SQLiteDatabase db = getWritableDatabase();
+
+ synchronized (mLock) {
+ LogUtil.v("DialerDatabaseHelper.updateSmartDialDatabase", "starting to update database");
+ final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null;
+
+ /** Gets the last update time on the database. */
+ final SharedPreferences databaseLastUpdateSharedPref =
+ mContext.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
+ final String lastUpdateMillis =
+ String.valueOf(databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0));
+
+ LogUtil.v(
+ "DialerDatabaseHelper.updateSmartDialDatabase", "last updated at " + lastUpdateMillis);
+
+ /** Sets the time after querying the database as the current update time. */
+ final Long currentMillis = System.currentTimeMillis();
+
+ if (DEBUG) {
+ stopWatch.lap("Queried the Contacts database");
+ }
+
+ /** Prevents the app from reading the dialer database when updating. */
+ mInUpdate.getAndSet(true);
+
+ /** Removes contacts that have been deleted. */
+ removeDeletedContacts(db, getDeletedContactCursor(lastUpdateMillis));
+ removePotentiallyCorruptedContacts(db, lastUpdateMillis);
+
+ if (DEBUG) {
+ stopWatch.lap("Finished deleting deleted entries");
+ }
+
+ /**
+ * If the database did not exist before, jump through deletion as there is nothing to delete.
+ */
+ if (!lastUpdateMillis.equals("0")) {
+ /**
+ * Removes contacts that have been updated. Updated contact information will be inserted
+ * later. Note that this has to use a separate result set from updatePhoneCursor, since it
+ * is possible for a contact to be updated (e.g. phone number deleted), but have no results
+ * show up in updatedPhoneCursor (since all of its phone numbers have been deleted).
+ */
+ final Cursor updatedContactCursor =
+ mContext
+ .getContentResolver()
+ .query(
+ UpdatedContactQuery.URI,
+ UpdatedContactQuery.PROJECTION,
+ UpdatedContactQuery.SELECT_UPDATED_CLAUSE,
+ new String[] {lastUpdateMillis},
+ null);
+ if (updatedContactCursor == null) {
+ LogUtil.e(
+ "DialerDatabaseHelper.updateSmartDialDatabase",
+ "smartDial query received null for cursor");
+ return;
+ }
+ try {
+ removeUpdatedContacts(db, updatedContactCursor);
+ } finally {
+ updatedContactCursor.close();
+ }
+ if (DEBUG) {
+ stopWatch.lap("Finished deleting entries belonging to updated contacts");
+ }
+ }
+
+ /**
+ * Queries the contact database to get all phone numbers that have been updated since the last
+ * update time.
+ */
+ final Cursor updatedPhoneCursor =
+ mContext
+ .getContentResolver()
+ .query(
+ PhoneQuery.URI,
+ PhoneQuery.PROJECTION,
+ PhoneQuery.SELECTION,
+ new String[] {lastUpdateMillis},
+ null);
+ if (updatedPhoneCursor == null) {
+ LogUtil.e(
+ "DialerDatabaseHelper.updateSmartDialDatabase",
+ "smartDial query received null for cursor");
+ return;
+ }
+
+ try {
+ /** Inserts recently updated phone numbers to the smartdial database. */
+ insertUpdatedContactsAndNumberPrefix(db, updatedPhoneCursor, currentMillis);
+ if (DEBUG) {
+ stopWatch.lap("Finished building the smart dial table");
+ }
+ } finally {
+ updatedPhoneCursor.close();
+ }
+
+ /**
+ * Gets a list of distinct contacts which have been updated, and adds the name prefixes of
+ * these contacts to the prefix table.
+ */
+ final Cursor nameCursor =
+ db.rawQuery(
+ "SELECT DISTINCT "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " = "
+ + Long.toString(currentMillis),
+ new String[] {});
+ if (nameCursor != null) {
+ try {
+ if (DEBUG) {
+ stopWatch.lap("Queried the smart dial table for contact names");
+ }
+
+ /** Inserts prefixes of names into the prefix table. */
+ insertNamePrefixes(db, nameCursor);
+ if (DEBUG) {
+ stopWatch.lap("Finished building the name prefix table");
+ }
+ } finally {
+ nameCursor.close();
+ }
+ }
+
+ /** Creates index on contact_id for fast JOIN operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.CONTACT_ID
+ + ");");
+ /** Creates index on last_smartdial_update_time for fast SELECT operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + ");");
+ /** Creates index on sorting fields for fast sort operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_sort_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.STARRED
+ + ", "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + ", "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + ", "
+ + SmartDialDbColumns.TIMES_USED
+ + ", "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.IS_PRIMARY
+ + ");");
+ /** Creates index on prefix for fast SELECT operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS nameprefix_index ON "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.PREFIX
+ + ");");
+ /** Creates index on contact_id for fast JOIN operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ");");
+
+ if (DEBUG) {
+ stopWatch.lap(TAG + "Finished recreating index");
+ }
+
+ /** Updates the database index statistics. */
+ db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE);
+ db.execSQL("ANALYZE " + Tables.PREFIX_TABLE);
+ db.execSQL("ANALYZE smartdial_contact_id_index");
+ db.execSQL("ANALYZE smartdial_last_update_index");
+ db.execSQL("ANALYZE nameprefix_index");
+ db.execSQL("ANALYZE nameprefix_contact_id_index");
+ if (DEBUG) {
+ stopWatch.stopAndLog(TAG + "Finished updating index stats", 0);
+ }
+
+ mInUpdate.getAndSet(false);
+
+ final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
+ editor.putLong(LAST_UPDATED_MILLIS, currentMillis);
+ editor.apply();
+
+ // Notify content observers that smart dial database has been updated.
+ mContext.getContentResolver().notifyChange(SMART_DIAL_UPDATED_URI, null, false);
+ }
+ }
+
+ /**
+ * Returns a list of candidate contacts where the query is a prefix of the dialpad index of the
+ * contact's name or phone number.
+ *
+ * @param query The prefix of a contact's dialpad index.
+ * @return A list of top candidate contacts that will be suggested to user to match their input.
+ */
+ public ArrayList<ContactNumber> getLooseMatches(String query, SmartDialNameMatcher nameMatcher) {
+ final boolean inUpdate = mInUpdate.get();
+ if (inUpdate) {
+ return new ArrayList<>();
+ }
+
+ final SQLiteDatabase db = getReadableDatabase();
+
+ /** Uses SQL query wildcard '%' to represent prefix matching. */
+ final String looseQuery = query + "%";
+
+ final ArrayList<ContactNumber> result = new ArrayList<>();
+
+ final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null;
+
+ final String currentTimeStamp = Long.toString(System.currentTimeMillis());
+
+ /** Queries the database to find contacts that have an index matching the query prefix. */
+ final Cursor cursor =
+ db.rawQuery(
+ "SELECT "
+ + SmartDialDbColumns.DATA_ID
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.PHOTO_ID
+ + ", "
+ + SmartDialDbColumns.NUMBER
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.LOOKUP_KEY
+ + ", "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.CONTACT_ID
+ + " IN "
+ + " (SELECT "
+ + PrefixColumns.CONTACT_ID
+ + " FROM "
+ + Tables.PREFIX_TABLE
+ + " WHERE "
+ + Tables.PREFIX_TABLE
+ + "."
+ + PrefixColumns.PREFIX
+ + " LIKE '"
+ + looseQuery
+ + "')"
+ + " ORDER BY "
+ + SmartDialSortingOrder.SORT_ORDER,
+ new String[] {currentTimeStamp});
+ if (cursor == null) {
+ return result;
+ }
+ try {
+ if (DEBUG) {
+ stopWatch.lap("Prefix query completed");
+ }
+
+ /** Gets the column ID from the cursor. */
+ final int columnDataId = 0;
+ final int columnDisplayNamePrimary = 1;
+ final int columnPhotoId = 2;
+ final int columnNumber = 3;
+ final int columnId = 4;
+ final int columnLookupKey = 5;
+ final int columnCarrierPresence = 6;
+ if (DEBUG) {
+ stopWatch.lap("Found column IDs");
+ }
+
+ final Set<ContactMatch> duplicates = new HashSet<>();
+ int counter = 0;
+ if (DEBUG) {
+ stopWatch.lap("Moved cursor to start");
+ }
+ /** Iterates the cursor to find top contact suggestions without duplication. */
+ while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) {
+ final long dataID = cursor.getLong(columnDataId);
+ final String displayName = cursor.getString(columnDisplayNamePrimary);
+ final String phoneNumber = cursor.getString(columnNumber);
+ final long id = cursor.getLong(columnId);
+ final long photoId = cursor.getLong(columnPhotoId);
+ final String lookupKey = cursor.getString(columnLookupKey);
+ final int carrierPresence = cursor.getInt(columnCarrierPresence);
+
+ /**
+ * If a contact already exists and another phone number of the contact is being processed,
+ * skip the second instance.
+ */
+ final ContactMatch contactMatch = new ContactMatch(lookupKey, id);
+ if (duplicates.contains(contactMatch)) {
+ continue;
+ }
+
+ /**
+ * If the contact has either the name or number that matches the query, add to the result.
+ */
+ final boolean nameMatches = nameMatcher.matches(displayName);
+ final boolean numberMatches = (nameMatcher.matchesNumber(phoneNumber, query) != null);
+ if (nameMatches || numberMatches) {
+ /** If a contact has not been added, add it to the result and the hash set. */
+ duplicates.add(contactMatch);
+ result.add(
+ new ContactNumber(
+ id, dataID, displayName, phoneNumber, lookupKey, photoId, carrierPresence));
+ counter++;
+ if (DEBUG) {
+ stopWatch.lap("Added one result: Name: " + displayName);
+ }
+ }
+ }
+
+ if (DEBUG) {
+ stopWatch.stopAndLog(TAG + "Finished loading cursor", 0);
+ }
+ } finally {
+ cursor.close();
+ }
+ return result;
+ }
+
+ public interface Tables {
+
+ /** Saves a list of numbers to be blocked. */
+ String FILTERED_NUMBER_TABLE = "filtered_numbers_table";
+ /** Saves the necessary smart dial information of all contacts. */
+ String SMARTDIAL_TABLE = "smartdial_table";
+ /** Saves all possible prefixes to refer to a contacts. */
+ String PREFIX_TABLE = "prefix_table";
+ /** Saves all archived voicemail information. */
+ String VOICEMAIL_ARCHIVE_TABLE = "voicemail_archive_table";
+ /** Database properties for internal use */
+ String PROPERTIES = "properties";
+ }
+
+ public interface SmartDialDbColumns {
+
+ String _ID = "id";
+ String DATA_ID = "data_id";
+ String NUMBER = "phone_number";
+ String CONTACT_ID = "contact_id";
+ String LOOKUP_KEY = "lookup_key";
+ String DISPLAY_NAME_PRIMARY = "display_name";
+ String PHOTO_ID = "photo_id";
+ String LAST_TIME_USED = "last_time_used";
+ String TIMES_USED = "times_used";
+ String STARRED = "starred";
+ String IS_SUPER_PRIMARY = "is_super_primary";
+ String IN_VISIBLE_GROUP = "in_visible_group";
+ String IS_PRIMARY = "is_primary";
+ String CARRIER_PRESENCE = "carrier_presence";
+ String LAST_SMARTDIAL_UPDATE_TIME = "last_smartdial_update_time";
+ }
+
+ public interface PrefixColumns extends BaseColumns {
+
+ String PREFIX = "prefix";
+ String CONTACT_ID = "contact_id";
+ }
+
+ public interface PropertiesColumns {
+
+ String PROPERTY_KEY = "property_key";
+ String PROPERTY_VALUE = "property_value";
+ }
+
+ /** Query options for querying the contact database. */
+ public interface PhoneQuery {
+
+ Uri URI =
+ Phone.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
+ .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true")
+ .build();
+
+ String[] PROJECTION =
+ new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.CONTACT_ID, // 4
+ Phone.LOOKUP_KEY, // 5
+ Phone.DISPLAY_NAME_PRIMARY, // 6
+ Phone.PHOTO_ID, // 7
+ Data.LAST_TIME_USED, // 8
+ Data.TIMES_USED, // 9
+ Contacts.STARRED, // 10
+ Data.IS_SUPER_PRIMARY, // 11
+ Contacts.IN_VISIBLE_GROUP, // 12
+ Data.IS_PRIMARY, // 13
+ Data.CARRIER_PRESENCE, // 14
+ };
+
+ int PHONE_ID = 0;
+ int PHONE_TYPE = 1;
+ int PHONE_LABEL = 2;
+ int PHONE_NUMBER = 3;
+ int PHONE_CONTACT_ID = 4;
+ int PHONE_LOOKUP_KEY = 5;
+ int PHONE_DISPLAY_NAME = 6;
+ int PHONE_PHOTO_ID = 7;
+ int PHONE_LAST_TIME_USED = 8;
+ int PHONE_TIMES_USED = 9;
+ int PHONE_STARRED = 10;
+ int PHONE_IS_SUPER_PRIMARY = 11;
+ int PHONE_IN_VISIBLE_GROUP = 12;
+ int PHONE_IS_PRIMARY = 13;
+ int PHONE_CARRIER_PRESENCE = 14;
+
+ /** Selects only rows that have been updated after a certain time stamp. */
+ String SELECT_UPDATED_CLAUSE = Phone.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
+
+ /**
+ * Ignores contacts that have an unreasonably long lookup key. These are likely to be the result
+ * of multiple (> 50) merged raw contacts, and are likely to cause OutOfMemoryExceptions within
+ * SQLite, or cause memory allocation problems later on when iterating through the cursor set
+ * (see b/13133579)
+ */
+ String SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE = "length(" + Phone.LOOKUP_KEY + ") < 1000";
+
+ String SELECTION = SELECT_UPDATED_CLAUSE + " AND " + SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE;
+ }
+
+ /**
+ * Query for all contacts that have been updated since the last time the smart dial database was
+ * updated.
+ */
+ public interface UpdatedContactQuery {
+
+ Uri URI = ContactsContract.Contacts.CONTENT_URI;
+
+ String[] PROJECTION =
+ new String[] {
+ ContactsContract.Contacts._ID // 0
+ };
+
+ int UPDATED_CONTACT_ID = 0;
+
+ String SELECT_UPDATED_CLAUSE =
+ ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
+ }
+
+ /** Query options for querying the deleted contact database. */
+ public interface DeleteContactQuery {
+
+ Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
+
+ String[] PROJECTION =
+ new String[] {
+ ContactsContract.DeletedContacts.CONTACT_ID, // 0
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, // 1
+ };
+
+ int DELETED_CONTACT_ID = 0;
+ int DELETED_TIMESTAMP = 1;
+
+ /** Selects only rows that have been deleted after a certain time stamp. */
+ String SELECT_UPDATED_CLAUSE =
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?";
+ }
+
+ /**
+ * Gets the sorting order for the smartdial table. This computes a SQL "ORDER BY" argument by
+ * composing contact status and recent contact details together.
+ */
+ private interface SmartDialSortingOrder {
+
+ /** Current contacts - those contacted within the last 3 days (in milliseconds) */
+ long LAST_TIME_USED_CURRENT_MS = 3L * 24 * 60 * 60 * 1000;
+ /** Recent contacts - those contacted within the last 30 days (in milliseconds) */
+ long LAST_TIME_USED_RECENT_MS = 30L * 24 * 60 * 60 * 1000;
+
+ /** Time since last contact. */
+ String TIME_SINCE_LAST_USED_MS =
+ "( ?1 - " + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.LAST_TIME_USED + ")";
+
+ /**
+ * Contacts that have been used in the past 3 days rank higher than contacts that have been used
+ * in the past 30 days, which rank higher than contacts that have not been used in recent 30
+ * days.
+ */
+ String SORT_BY_DATA_USAGE =
+ "(CASE WHEN "
+ + TIME_SINCE_LAST_USED_MS
+ + " < "
+ + LAST_TIME_USED_CURRENT_MS
+ + " THEN 0 "
+ + " WHEN "
+ + TIME_SINCE_LAST_USED_MS
+ + " < "
+ + LAST_TIME_USED_RECENT_MS
+ + " THEN 1 "
+ + " ELSE 2 END)";
+
+ /**
+ * This sort order is similar to that used by the ContactsProvider when returning a list of
+ * frequently called contacts.
+ */
+ String SORT_ORDER =
+ Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.STARRED
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + " DESC, "
+ + SORT_BY_DATA_USAGE
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.TIMES_USED
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IS_PRIMARY
+ + " DESC";
+ }
+
+ /**
+ * Simple data format for a contact, containing only information needed for showing up in smart
+ * dial interface.
+ */
+ public static class ContactNumber {
+
+ public final long id;
+ public final long dataId;
+ public final String displayName;
+ public final String phoneNumber;
+ public final String lookupKey;
+ public final long photoId;
+ public final int carrierPresence;
+
+ public ContactNumber(
+ long id,
+ long dataID,
+ String displayName,
+ String phoneNumber,
+ String lookupKey,
+ long photoId,
+ int carrierPresence) {
+ this.dataId = dataID;
+ this.id = id;
+ this.displayName = displayName;
+ this.phoneNumber = phoneNumber;
+ this.lookupKey = lookupKey;
+ this.photoId = photoId;
+ this.carrierPresence = carrierPresence;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ id, dataId, displayName, phoneNumber, lookupKey, photoId, carrierPresence);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof ContactNumber) {
+ final ContactNumber that = (ContactNumber) object;
+ return Objects.equals(this.id, that.id)
+ && Objects.equals(this.dataId, that.dataId)
+ && Objects.equals(this.displayName, that.displayName)
+ && Objects.equals(this.phoneNumber, that.phoneNumber)
+ && Objects.equals(this.lookupKey, that.lookupKey)
+ && Objects.equals(this.photoId, that.photoId)
+ && Objects.equals(this.carrierPresence, that.carrierPresence);
+ }
+ return false;
+ }
+ }
+
+ /** Data format for finding duplicated contacts. */
+ private static class ContactMatch {
+
+ private final String lookupKey;
+ private final long id;
+
+ public ContactMatch(String lookupKey, long id) {
+ this.lookupKey = lookupKey;
+ this.id = id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(lookupKey, id);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof ContactMatch) {
+ final ContactMatch that = (ContactMatch) object;
+ return Objects.equals(this.lookupKey, that.lookupKey) && Objects.equals(this.id, that.id);
+ }
+ return false;
+ }
+ }
+
+ private class SmartDialUpdateAsyncTask extends AsyncTask<Object, Object, Object> {
+
+ @Override
+ protected Object doInBackground(Object... objects) {
+ updateSmartDialDatabase();
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/dialer/database/FilteredNumberContract.java b/java/com/android/dialer/database/FilteredNumberContract.java
new file mode 100644
index 0000000..3efbaaf
--- /dev/null
+++ b/java/com/android/dialer/database/FilteredNumberContract.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2015 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.database;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+import com.android.dialer.constants.Constants;
+
+/**
+ * The contract between the filtered number provider and applications. Contains definitions for the
+ * supported URIs and columns. Currently only accessible within Dialer.
+ */
+public final class FilteredNumberContract {
+
+ public static final String AUTHORITY = Constants.get().getFilteredNumberProviderAuthority();
+
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+ /** The type of filtering to be applied, e.g. block the number or whitelist the number. */
+ public interface FilteredNumberTypes {
+
+ int UNDEFINED = 0;
+ /** Dialer will disconnect the call without sending the caller to voicemail. */
+ int BLOCKED_NUMBER = 1;
+ }
+
+ /** The original source of the filtered number, e.g. the user manually added it. */
+ public interface FilteredNumberSources {
+
+ int UNDEFINED = 0;
+ /** The user manually added this number through Dialer (e.g. from the call log or InCallUI). */
+ int USER = 1;
+ }
+
+ public interface FilteredNumberColumns {
+
+ // TYPE: INTEGER
+ String _ID = "_id";
+ /**
+ * Represents the number to be filtered, normalized to compare phone numbers for equality.
+ *
+ * <p>TYPE: TEXT
+ */
+ String NORMALIZED_NUMBER = "normalized_number";
+ /**
+ * Represents the number to be filtered, for formatting and used with country iso for contact
+ * lookups.
+ *
+ * <p>TYPE: TEXT
+ */
+ String NUMBER = "number";
+ /**
+ * The country code representing the country detected when the phone number was added to the
+ * database. Most numbers don't have the country code, so a best guess is provided by the
+ * country detector system. The country iso is also needed in order to format phone numbers
+ * correctly.
+ *
+ * <p>TYPE: TEXT
+ */
+ String COUNTRY_ISO = "country_iso";
+ /**
+ * The number of times the number has been filtered by Dialer. When this number is incremented,
+ * LAST_TIME_FILTERED should also be updated to the current time.
+ *
+ * <p>TYPE: INTEGER
+ */
+ String TIMES_FILTERED = "times_filtered";
+ /**
+ * Set to the current time when the phone number is filtered. When this is updated,
+ * TIMES_FILTERED should also be incremented.
+ *
+ * <p>TYPE: LONG
+ */
+ String LAST_TIME_FILTERED = "last_time_filtered";
+ // TYPE: LONG
+ String CREATION_TIME = "creation_time";
+ /**
+ * Indicates the type of filtering to be applied.
+ *
+ * <p>TYPE: INTEGER See {@link FilteredNumberTypes}
+ */
+ String TYPE = "type";
+ /**
+ * Integer representing the original source of the filtered number.
+ *
+ * <p>TYPE: INTEGER See {@link FilteredNumberSources}
+ */
+ String SOURCE = "source";
+ }
+
+ /**
+ * Constants for the table of filtered numbers.
+ *
+ * <h3>Operations</h3>
+ *
+ * <dl>
+ * <dt><b>Insert</b>
+ * <dd>Required fields: NUMBER, NORMALIZED_NUMBER, TYPE, SOURCE. A default value will be used for
+ * the other fields if left null.
+ * <dt><b>Update</b>
+ * <dt><b>Delete</b>
+ * <dt><b>Query</b>
+ * <dd>{@link #CONTENT_URI} can be used for any query, append an ID to retrieve a specific
+ * filtered number entry.
+ * </dl>
+ */
+ public static class FilteredNumber implements BaseColumns {
+
+ public static final String FILTERED_NUMBERS_TABLE = "filtered_numbers_table";
+
+ /**
+ * The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single filtered
+ * number.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/filtered_numbers_table";
+
+ public static final Uri CONTENT_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, FILTERED_NUMBERS_TABLE);
+
+ /** This utility class cannot be instantiated. */
+ private FilteredNumber() {}
+ }
+}
diff --git a/java/com/android/dialer/database/VoicemailStatusQuery.java b/java/com/android/dialer/database/VoicemailStatusQuery.java
new file mode 100644
index 0000000..d9e1b72
--- /dev/null
+++ b/java/com/android/dialer/database/VoicemailStatusQuery.java
@@ -0,0 +1,91 @@
+/*
+ * 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.database;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** The query for the call voicemail status table. */
+public class VoicemailStatusQuery {
+
+ // TODO: Column indices should be removed in favor of Cursor#getColumnIndex
+ public static final int SOURCE_PACKAGE_INDEX = 0;
+ public static final int SETTINGS_URI_INDEX = 1;
+ public static final int VOICEMAIL_ACCESS_URI_INDEX = 2;
+ public static final int CONFIGURATION_STATE_INDEX = 3;
+ public static final int DATA_CHANNEL_STATE_INDEX = 4;
+ public static final int NOTIFICATION_CHANNEL_STATE_INDEX = 5;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int QUOTA_OCCUPIED_INDEX = 6;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int QUOTA_TOTAL_INDEX = 7;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ // The PHONE_ACCOUNT columns were added in M, but aren't queryable until N MR1
+ public static final int PHONE_ACCOUNT_COMPONENT_NAME = 8;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ public static final int PHONE_ACCOUNT_ID = 9;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ public static final int SOURCE_TYPE_INDEX = 10;
+
+ private static final String[] PROJECTION_M =
+ new String[] {
+ Status.SOURCE_PACKAGE, // 0
+ Status.SETTINGS_URI, // 1
+ Status.VOICEMAIL_ACCESS_URI, // 2
+ Status.CONFIGURATION_STATE, // 3
+ Status.DATA_CHANNEL_STATE, // 4
+ Status.NOTIFICATION_CHANNEL_STATE // 5
+ };
+
+ @RequiresApi(VERSION_CODES.N)
+ private static final String[] PROJECTION_N;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ private static final String[] PROJECTION_NMR1;
+
+ static {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(PROJECTION_M));
+ projectionList.add(Status.QUOTA_OCCUPIED); // 6
+ projectionList.add(Status.QUOTA_TOTAL); // 7
+ PROJECTION_N = projectionList.toArray(new String[projectionList.size()]);
+
+ projectionList.add(Status.PHONE_ACCOUNT_COMPONENT_NAME); // 8
+ projectionList.add(Status.PHONE_ACCOUNT_ID); // 9
+ projectionList.add(Status.SOURCE_TYPE); // 10
+ PROJECTION_NMR1 = projectionList.toArray(new String[projectionList.size()]);
+ }
+
+ public static String[] getProjection() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ return PROJECTION_NMR1;
+ }
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PROJECTION_N;
+ }
+ return PROJECTION_M;
+ }
+}
diff --git a/java/com/android/dialer/debug/AndroidManifest.xml b/java/com/android/dialer/debug/AndroidManifest.xml
new file mode 100644
index 0000000..053d7e7
--- /dev/null
+++ b/java/com/android/dialer/debug/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.debug">
+</manifest>
diff --git a/java/com/android/dialer/debug/bindings/impl/DebugBindings.java b/java/com/android/dialer/debug/bindings/impl/DebugBindings.java
new file mode 100644
index 0000000..a8b4460
--- /dev/null
+++ b/java/com/android/dialer/debug/bindings/impl/DebugBindings.java
@@ -0,0 +1,32 @@
+/*
+ * 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.debug.bindings;
+
+import android.content.Context;
+import com.android.dialer.debug.impl.DebugConnectionService;
+
+/** Hooks into the debug module. */
+public class DebugBindings {
+
+ public static void registerConnectionService(Context context) {
+ DebugConnectionService.register(context);
+ }
+
+ public static void addNewIncomingCall(Context context, String phoneNumber) {
+ DebugConnectionService.addNewIncomingCall(context, phoneNumber);
+ }
+}
diff --git a/java/com/android/dialer/debug/impl/AndroidManifest.xml b/java/com/android/dialer/debug/impl/AndroidManifest.xml
new file mode 100644
index 0000000..b875661
--- /dev/null
+++ b/java/com/android/dialer/debug/impl/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.debug.impl">
+
+ <application>
+
+ <service
+ android:exported="true"
+ android:name=".DebugConnectionService"
+ android:permission="android.permission.BIND_CONNECTION_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecomm.ConnectionService"/>
+ </intent-filter>
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/debug/impl/DebugConnection.java b/java/com/android/dialer/debug/impl/DebugConnection.java
new file mode 100644
index 0000000..2ef83aa
--- /dev/null
+++ b/java/com/android/dialer/debug/impl/DebugConnection.java
@@ -0,0 +1,55 @@
+/*
+ * 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.debug.impl;
+
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import com.android.dialer.common.LogUtil;
+
+class DebugConnection extends Connection {
+
+ @Override
+ public void onAnswer() {
+ LogUtil.i("DebugConnection.onAnswer", null);
+ setActive();
+ }
+
+ @Override
+ public void onReject() {
+ LogUtil.i("DebugConnection.onReject", null);
+ setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+ }
+
+ @Override
+ public void onHold() {
+ LogUtil.i("DebugConnection.onHold", null);
+ setOnHold();
+ }
+
+ @Override
+ public void onUnhold() {
+ LogUtil.i("DebugConnection.onUnhold", null);
+ setActive();
+ }
+
+ @Override
+ public void onDisconnect() {
+ LogUtil.i("DebugConnection.onDisconnect", null);
+ setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+ destroy();
+ }
+}
diff --git a/java/com/android/dialer/debug/impl/DebugConnectionService.java b/java/com/android/dialer/debug/impl/DebugConnectionService.java
new file mode 100644
index 0000000..69aab1e
--- /dev/null
+++ b/java/com/android/dialer/debug/impl/DebugConnectionService.java
@@ -0,0 +1,103 @@
+/*
+ * 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.debug.impl;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Simple connection provider to create an incoming call. This is useful for emulators. */
+public class DebugConnectionService extends ConnectionService {
+
+ private static final String PHONE_ACCOUNT_ID = "DEBUG_DIALER";
+
+ public static void register(Context context) {
+ LogUtil.i(
+ "DebugConnectionService.register",
+ context.getSystemService(Context.TELECOM_SERVICE).toString());
+ context.getSystemService(TelecomManager.class).registerPhoneAccount(buildPhoneAccount(context));
+ }
+
+ public static void addNewIncomingCall(Context context, String phoneNumber) {
+ LogUtil.i("DebugConnectionService.addNewIncomingCall", null);
+ Bundle bundle = new Bundle();
+ bundle.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, phoneNumber);
+ try {
+ context
+ .getSystemService(TelecomManager.class)
+ .addNewIncomingCall(getConnectionServiceHandle(context), bundle);
+ } catch (SecurityException e) {
+ LogUtil.i(
+ "DebugConnectionService.addNewIncomingCall",
+ "unable to add call. Make sure to enable the service in Phone app -> Settings -> Calls ->"
+ + " Calling accounts.");
+ }
+ }
+
+ private static PhoneAccount buildPhoneAccount(Context context) {
+ PhoneAccount.Builder builder =
+ new PhoneAccount.Builder(
+ getConnectionServiceHandle(context), "DebugDialerConnectionService");
+ List<String> uriSchemes = new ArrayList<>();
+ uriSchemes.add(PhoneAccount.SCHEME_TEL);
+
+ return builder
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setShortDescription("Debug Dialer Connection Serivce")
+ .setSupportedUriSchemes(uriSchemes)
+ .build();
+ }
+
+ private static PhoneAccountHandle getConnectionServiceHandle(Context context) {
+ ComponentName componentName = new ComponentName(context, DebugConnectionService.class);
+ return new PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID);
+ }
+
+ private static Uri getPhoneNumber(ConnectionRequest request) {
+ String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
+ }
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ return null;
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ LogUtil.i("DebugConnectionService.onCreateIncomingConnection", null);
+ DebugConnection connection = new DebugConnection();
+ connection.setRinging();
+ connection.setAddress(getPhoneNumber(request), TelecomManager.PRESENTATION_ALLOWED);
+ connection.setConnectionCapabilities(
+ Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
+ return connection;
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/AndroidManifest.xml b/java/com/android/dialer/dialpadview/AndroidManifest.xml
new file mode 100644
index 0000000..011a004
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.dialpadview">
+</manifest>
diff --git a/java/com/android/dialer/dialpadview/DialpadKeyButton.java b/java/com/android/dialer/dialpadview/DialpadKeyButton.java
new file mode 100644
index 0000000..24ca9cc
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DialpadKeyButton.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2012 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.dialpadview;
+
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+
+/**
+ * Custom class for dialpad buttons.
+ *
+ * <p>When touch exploration mode is enabled for accessibility, this class implements the
+ * lift-to-type interaction model:
+ *
+ * <ul>
+ * <li>Hovering over the button will cause it to gain accessibility focus
+ * <li>Removing the hover pointer while inside the bounds of the button will perform a click action
+ * <li>If long-click is supported, hovering over the button for a longer period of time will switch
+ * to the long-click action
+ * <li>Moving the hover pointer outside of the bounds of the button will restore to the normal click
+ * action
+ * <ul>
+ */
+public class DialpadKeyButton extends FrameLayout {
+
+ /** Timeout before switching to long-click accessibility mode. */
+ private static final int LONG_HOVER_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2;
+
+ /** Accessibility manager instance used to check touch exploration state. */
+ private AccessibilityManager mAccessibilityManager;
+
+ /** Bounds used to filter HOVER_EXIT events. */
+ private RectF mHoverBounds = new RectF();
+
+ /** Whether this view is currently in the long-hover state. */
+ private boolean mLongHovered;
+
+ /** Alternate content description for long-hover state. */
+ private CharSequence mLongHoverContentDesc;
+
+ /** Backup of standard content description. Used for accessibility. */
+ private CharSequence mBackupContentDesc;
+
+ /** Backup of clickable property. Used for accessibility. */
+ private boolean mWasClickable;
+
+ /** Backup of long-clickable property. Used for accessibility. */
+ private boolean mWasLongClickable;
+
+ /** Runnable used to trigger long-click mode for accessibility. */
+ private Runnable mLongHoverRunnable;
+
+ private OnPressedListener mOnPressedListener;
+
+ public DialpadKeyButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initForAccessibility(context);
+ }
+
+ public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initForAccessibility(context);
+ }
+
+ public void setOnPressedListener(OnPressedListener onPressedListener) {
+ mOnPressedListener = onPressedListener;
+ }
+
+ private void initForAccessibility(Context context) {
+ mAccessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ }
+
+ public void setLongHoverContentDescription(CharSequence contentDescription) {
+ mLongHoverContentDesc = contentDescription;
+
+ if (mLongHovered) {
+ super.setContentDescription(mLongHoverContentDesc);
+ }
+ }
+
+ @Override
+ public void setContentDescription(CharSequence contentDescription) {
+ if (mLongHovered) {
+ mBackupContentDesc = contentDescription;
+ } else {
+ super.setContentDescription(contentDescription);
+ }
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+ super.setPressed(pressed);
+ if (mOnPressedListener != null) {
+ mOnPressedListener.onPressed(this, pressed);
+ }
+ }
+
+ @Override
+ public void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mHoverBounds.left = getPaddingLeft();
+ mHoverBounds.right = w - getPaddingRight();
+ mHoverBounds.top = getPaddingTop();
+ mHoverBounds.bottom = h - getPaddingBottom();
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+ simulateClickForAccessibility();
+ return true;
+ }
+
+ return super.performAccessibilityAction(action, arguments);
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ // When touch exploration is turned on, lifting a finger while inside
+ // the button's hover target bounds should perform a click action.
+ if (mAccessibilityManager.isEnabled() && mAccessibilityManager.isTouchExplorationEnabled()) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ // Lift-to-type temporarily disables double-tap activation.
+ mWasClickable = isClickable();
+ mWasLongClickable = isLongClickable();
+ if (mWasLongClickable && mLongHoverContentDesc != null) {
+ if (mLongHoverRunnable == null) {
+ mLongHoverRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ setLongHovered(true);
+ announceForAccessibility(mLongHoverContentDesc);
+ }
+ };
+ }
+ postDelayed(mLongHoverRunnable, LONG_HOVER_TIMEOUT);
+ }
+
+ setClickable(false);
+ setLongClickable(false);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (mHoverBounds.contains(event.getX(), event.getY())) {
+ if (mLongHovered) {
+ performLongClick();
+ } else {
+ simulateClickForAccessibility();
+ }
+ }
+
+ cancelLongHover();
+ setClickable(mWasClickable);
+ setLongClickable(mWasLongClickable);
+ break;
+ }
+ }
+
+ return super.onHoverEvent(event);
+ }
+
+ /**
+ * When accessibility is on, simulate press and release to preserve the semantic meaning of
+ * performClick(). Required for Braille support.
+ */
+ private void simulateClickForAccessibility() {
+ // Checking the press state prevents double activation.
+ if (isPressed()) {
+ return;
+ }
+
+ setPressed(true);
+
+ // Stay consistent with performClick() by sending the event after
+ // setting the pressed state but before performing the action.
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+ setPressed(false);
+ }
+
+ private void setLongHovered(boolean enabled) {
+ if (mLongHovered != enabled) {
+ mLongHovered = enabled;
+
+ // Switch between normal and alternate description, if available.
+ if (enabled) {
+ mBackupContentDesc = getContentDescription();
+ super.setContentDescription(mLongHoverContentDesc);
+ } else {
+ super.setContentDescription(mBackupContentDesc);
+ }
+ }
+ }
+
+ private void cancelLongHover() {
+ if (mLongHoverRunnable != null) {
+ removeCallbacks(mLongHoverRunnable);
+ }
+ setLongHovered(false);
+ }
+
+ public interface OnPressedListener {
+
+ void onPressed(View view, boolean pressed);
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/DialpadTextView.java b/java/com/android/dialer/dialpadview/DialpadTextView.java
new file mode 100644
index 0000000..5b1b7bb
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DialpadTextView.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 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.dialpadview;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+/**
+ * This is a custom text view intended only for rendering the numerals (and star and pound) on the
+ * dialpad. TextView has built in top/bottom padding to help account for ascenders/descenders.
+ *
+ * <p>Since vertical space is at a premium on the dialpad, particularly if the font size is scaled
+ * to a larger default, for the dialpad we use this class to more precisely render characters
+ * according to the precise amount of space they need.
+ */
+public class DialpadTextView extends TextView {
+
+ private Rect mTextBounds = new Rect();
+ private String mTextStr;
+
+ public DialpadTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /** Draw the text to fit within the height/width which have been specified during measurement. */
+ @Override
+ public void draw(Canvas canvas) {
+ Paint paint = getPaint();
+
+ // Without this, the draw does not respect the style's specified text color.
+ paint.setColor(getCurrentTextColor());
+
+ // The text bounds values are relative and can be negative,, so rather than specifying a
+ // standard origin such as 0, 0, we need to use negative of the left/top bounds.
+ // For example, the bounds may be: Left: 11, Right: 37, Top: -77, Bottom: 0
+ canvas.drawText(mTextStr, -mTextBounds.left, -mTextBounds.top, paint);
+ }
+
+ /**
+ * Calculate the pixel-accurate bounds of the text when rendered, and use that to specify the
+ * height and width.
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mTextStr = getText().toString();
+ getPaint().getTextBounds(mTextStr, 0, mTextStr.length(), mTextBounds);
+
+ int width = resolveSize(mTextBounds.width(), widthMeasureSpec);
+ int height = resolveSize(mTextBounds.height(), heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/DialpadView.java b/java/com/android/dialer/dialpadview/DialpadView.java
new file mode 100644
index 0000000..4a9b500
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DialpadView.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2014 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.dialpadview;
+
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
+import android.os.Build;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.animation.AnimUtils;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/** View that displays a twelve-key phone dialpad. */
+public class DialpadView extends LinearLayout {
+
+ private static final String TAG = DialpadView.class.getSimpleName();
+
+ private static final double DELAY_MULTIPLIER = 0.66;
+ private static final double DURATION_MULTIPLIER = 0.8;
+ // For animation.
+ private static final int KEY_FRAME_DURATION = 33;
+ /** {@code True} if the dialpad is in landscape orientation. */
+ private final boolean mIsLandscape;
+ /** {@code True} if the dialpad is showing in a right-to-left locale. */
+ private final boolean mIsRtl;
+
+ private final int[] mButtonIds =
+ new int[] {
+ R.id.zero,
+ R.id.one,
+ R.id.two,
+ R.id.three,
+ R.id.four,
+ R.id.five,
+ R.id.six,
+ R.id.seven,
+ R.id.eight,
+ R.id.nine,
+ R.id.star,
+ R.id.pound
+ };
+ private EditText mDigits;
+ private ImageButton mDelete;
+ private View mOverflowMenuButton;
+ private ColorStateList mRippleColor;
+ private ViewGroup mRateContainer;
+ private TextView mIldCountry;
+ private TextView mIldRate;
+ private boolean mCanDigitsBeEdited;
+ private int mTranslateDistance;
+
+ public DialpadView(Context context) {
+ this(context, null);
+ }
+
+ public DialpadView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DialpadView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Dialpad);
+ mRippleColor = a.getColorStateList(R.styleable.Dialpad_dialpad_key_button_touch_tint);
+ a.recycle();
+
+ mTranslateDistance =
+ getResources().getDimensionPixelSize(R.dimen.dialpad_key_button_translate_y);
+
+ mIsLandscape =
+ getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ mIsRtl =
+ TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setupKeypad();
+ mDigits = (EditText) findViewById(R.id.digits);
+ mDelete = (ImageButton) findViewById(R.id.deleteButton);
+ mOverflowMenuButton = findViewById(R.id.dialpad_overflow);
+ mRateContainer = (ViewGroup) findViewById(R.id.rate_container);
+ mIldCountry = (TextView) mRateContainer.findViewById(R.id.ild_country);
+ mIldRate = (TextView) mRateContainer.findViewById(R.id.ild_rate);
+
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (accessibilityManager.isEnabled()) {
+ // The text view must be selected to send accessibility events.
+ mDigits.setSelected(true);
+ }
+ }
+
+ private void setupKeypad() {
+ final int[] letterIds =
+ new int[] {
+ R.string.dialpad_0_letters,
+ R.string.dialpad_1_letters,
+ R.string.dialpad_2_letters,
+ R.string.dialpad_3_letters,
+ R.string.dialpad_4_letters,
+ R.string.dialpad_5_letters,
+ R.string.dialpad_6_letters,
+ R.string.dialpad_7_letters,
+ R.string.dialpad_8_letters,
+ R.string.dialpad_9_letters,
+ R.string.dialpad_star_letters,
+ R.string.dialpad_pound_letters
+ };
+
+ final Resources resources = getContext().getResources();
+
+ DialpadKeyButton dialpadKey;
+ TextView numberView;
+ TextView lettersView;
+
+ final Locale currentLocale = resources.getConfiguration().locale;
+ final NumberFormat nf;
+ // We translate dialpad numbers only for "fa" and not any other locale
+ // ("ar" anybody ?).
+ if ("fa".equals(currentLocale.getLanguage())) {
+ nf = DecimalFormat.getInstance(resources.getConfiguration().locale);
+ } else {
+ nf = DecimalFormat.getInstance(Locale.ENGLISH);
+ }
+
+ for (int i = 0; i < mButtonIds.length; i++) {
+ dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]);
+ numberView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_number);
+ lettersView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_letters);
+
+ final String numberString;
+ final CharSequence numberContentDescription;
+ if (mButtonIds[i] == R.id.pound) {
+ numberString = resources.getString(R.string.dialpad_pound_number);
+ numberContentDescription = numberString;
+ } else if (mButtonIds[i] == R.id.star) {
+ numberString = resources.getString(R.string.dialpad_star_number);
+ numberContentDescription = numberString;
+ } else {
+ numberString = nf.format(i);
+ // The content description is used for Talkback key presses. The number is
+ // separated by a "," to introduce a slight delay. Convert letters into a verbatim
+ // span so that they are read as letters instead of as one word.
+ String letters = resources.getString(letterIds[i]);
+ Spannable spannable =
+ Spannable.Factory.getInstance().newSpannable(numberString + "," + letters);
+ spannable.setSpan(
+ (new TtsSpan.VerbatimBuilder(letters)).build(),
+ numberString.length() + 1,
+ numberString.length() + 1 + letters.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ numberContentDescription = spannable;
+ }
+
+ final RippleDrawable rippleBackground =
+ (RippleDrawable) getDrawableCompat(getContext(), R.drawable.btn_dialpad_key);
+ if (mRippleColor != null) {
+ rippleBackground.setColor(mRippleColor);
+ }
+
+ numberView.setText(numberString);
+ numberView.setElegantTextHeight(false);
+ dialpadKey.setContentDescription(numberContentDescription);
+ dialpadKey.setBackground(rippleBackground);
+
+ if (lettersView != null) {
+ lettersView.setText(resources.getString(letterIds[i]));
+ }
+ }
+
+ final DialpadKeyButton one = (DialpadKeyButton) findViewById(R.id.one);
+ one.setLongHoverContentDescription(resources.getText(R.string.description_voicemail_button));
+
+ final DialpadKeyButton zero = (DialpadKeyButton) findViewById(R.id.zero);
+ zero.setLongHoverContentDescription(resources.getText(R.string.description_image_button_plus));
+ }
+
+ private Drawable getDrawableCompat(Context context, int id) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return context.getDrawable(id);
+ } else {
+ return context.getResources().getDrawable(id);
+ }
+ }
+
+ public void setShowVoicemailButton(boolean show) {
+ View view = findViewById(R.id.dialpad_key_voicemail);
+ if (view != null) {
+ view.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ /**
+ * Whether or not the digits above the dialer can be edited.
+ *
+ * @param canBeEdited If true, the backspace button will be shown and the digits EditText will be
+ * configured to allow text manipulation.
+ */
+ public void setCanDigitsBeEdited(boolean canBeEdited) {
+ View deleteButton = findViewById(R.id.deleteButton);
+ deleteButton.setVisibility(canBeEdited ? View.VISIBLE : View.INVISIBLE);
+ View overflowMenuButton = findViewById(R.id.dialpad_overflow);
+ overflowMenuButton.setVisibility(canBeEdited ? View.VISIBLE : View.GONE);
+
+ EditText digits = (EditText) findViewById(R.id.digits);
+ digits.setClickable(canBeEdited);
+ digits.setLongClickable(canBeEdited);
+ digits.setFocusableInTouchMode(canBeEdited);
+ digits.setCursorVisible(false);
+
+ mCanDigitsBeEdited = canBeEdited;
+ }
+
+ public void setCallRateInformation(String countryName, String displayRate) {
+ if (TextUtils.isEmpty(countryName) && TextUtils.isEmpty(displayRate)) {
+ mRateContainer.setVisibility(View.GONE);
+ return;
+ }
+ mRateContainer.setVisibility(View.VISIBLE);
+ mIldCountry.setText(countryName);
+ mIldRate.setText(displayRate);
+ }
+
+ public boolean canDigitsBeEdited() {
+ return mCanDigitsBeEdited;
+ }
+
+ /**
+ * Always returns true for onHoverEvent callbacks, to fix problems with accessibility due to the
+ * dialpad overlaying other fragments.
+ */
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ return true;
+ }
+
+ public void animateShow() {
+ // This is a hack; without this, the setTranslationY is delayed in being applied, and the
+ // numbers appear at their original position (0) momentarily before animating.
+ final AnimatorListenerAdapter showListener = new AnimatorListenerAdapter() {};
+
+ for (int i = 0; i < mButtonIds.length; i++) {
+ int delay = (int) (getKeyButtonAnimationDelay(mButtonIds[i]) * DELAY_MULTIPLIER);
+ int duration = (int) (getKeyButtonAnimationDuration(mButtonIds[i]) * DURATION_MULTIPLIER);
+ final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]);
+
+ ViewPropertyAnimator animator = dialpadKey.animate();
+ if (mIsLandscape) {
+ // Landscape orientation requires translation along the X axis.
+ // For RTL locales, ensure we translate negative on the X axis.
+ dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance);
+ animator.translationX(0);
+ } else {
+ // Portrait orientation requires translation along the Y axis.
+ dialpadKey.setTranslationY(mTranslateDistance);
+ animator.translationY(0);
+ }
+ animator
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setStartDelay(delay)
+ .setDuration(duration)
+ .setListener(showListener)
+ .start();
+ }
+ }
+
+ public EditText getDigits() {
+ return mDigits;
+ }
+
+ public ImageButton getDeleteButton() {
+ return mDelete;
+ }
+
+ public View getOverflowMenuButton() {
+ return mOverflowMenuButton;
+ }
+
+ /**
+ * Get the animation delay for the buttons, taking into account whether the dialpad is in
+ * landscape left-to-right, landscape right-to-left, or portrait.
+ *
+ * @param buttonId The button ID.
+ * @return The animation delay.
+ */
+ private int getKeyButtonAnimationDelay(int buttonId) {
+ if (mIsLandscape) {
+ if (mIsRtl) {
+ if (buttonId == R.id.three) {
+ return KEY_FRAME_DURATION * 1;
+ } else if (buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 2;
+ } else if (buttonId == R.id.nine) {
+ return KEY_FRAME_DURATION * 3;
+ } else if (buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 4;
+ } else if (buttonId == R.id.two) {
+ return KEY_FRAME_DURATION * 5;
+ } else if (buttonId == R.id.five) {
+ return KEY_FRAME_DURATION * 6;
+ } else if (buttonId == R.id.eight) {
+ return KEY_FRAME_DURATION * 7;
+ } else if (buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.one) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.four) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.seven || buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 11;
+ }
+ } else {
+ if (buttonId == R.id.one) {
+ return KEY_FRAME_DURATION * 1;
+ } else if (buttonId == R.id.four) {
+ return KEY_FRAME_DURATION * 2;
+ } else if (buttonId == R.id.seven) {
+ return KEY_FRAME_DURATION * 3;
+ } else if (buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 4;
+ } else if (buttonId == R.id.two) {
+ return KEY_FRAME_DURATION * 5;
+ } else if (buttonId == R.id.five) {
+ return KEY_FRAME_DURATION * 6;
+ } else if (buttonId == R.id.eight) {
+ return KEY_FRAME_DURATION * 7;
+ } else if (buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.three) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.nine || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 11;
+ }
+ }
+ } else {
+ if (buttonId == R.id.one) {
+ return KEY_FRAME_DURATION * 1;
+ } else if (buttonId == R.id.two) {
+ return KEY_FRAME_DURATION * 2;
+ } else if (buttonId == R.id.three) {
+ return KEY_FRAME_DURATION * 3;
+ } else if (buttonId == R.id.four) {
+ return KEY_FRAME_DURATION * 4;
+ } else if (buttonId == R.id.five) {
+ return KEY_FRAME_DURATION * 5;
+ } else if (buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 6;
+ } else if (buttonId == R.id.seven) {
+ return KEY_FRAME_DURATION * 7;
+ } else if (buttonId == R.id.eight) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.nine) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.zero || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 11;
+ }
+ }
+
+ Log.wtf(TAG, "Attempted to get animation delay for invalid key button id.");
+ return 0;
+ }
+
+ /**
+ * Get the button animation duration, taking into account whether the dialpad is in landscape
+ * left-to-right, landscape right-to-left, or portrait.
+ *
+ * @param buttonId The button ID.
+ * @return The animation duration.
+ */
+ private int getKeyButtonAnimationDuration(int buttonId) {
+ if (mIsLandscape) {
+ if (mIsRtl) {
+ if (buttonId == R.id.one
+ || buttonId == R.id.four
+ || buttonId == R.id.seven
+ || buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.two
+ || buttonId == R.id.five
+ || buttonId == R.id.eight
+ || buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.three
+ || buttonId == R.id.six
+ || buttonId == R.id.nine
+ || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 10;
+ }
+ } else {
+ if (buttonId == R.id.one
+ || buttonId == R.id.four
+ || buttonId == R.id.seven
+ || buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.two
+ || buttonId == R.id.five
+ || buttonId == R.id.eight
+ || buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.three
+ || buttonId == R.id.six
+ || buttonId == R.id.nine
+ || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 8;
+ }
+ }
+ } else {
+ if (buttonId == R.id.one
+ || buttonId == R.id.two
+ || buttonId == R.id.three
+ || buttonId == R.id.four
+ || buttonId == R.id.five
+ || buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.seven || buttonId == R.id.eight || buttonId == R.id.nine) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.star || buttonId == R.id.zero || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 8;
+ }
+ }
+
+ Log.wtf(TAG, "Attempted to get animation duration for invalid key button id.");
+ return 0;
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/DigitsEditText.java b/java/com/android/dialer/dialpadview/DigitsEditText.java
new file mode 100644
index 0000000..4a4b9b4
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DigitsEditText.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011 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.dialpadview;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.inputmethod.InputMethodManager;
+import com.android.dialer.widget.ResizingTextEditText;
+
+/** EditText which suppresses IME show up. */
+public class DigitsEditText extends ResizingTextEditText {
+
+ public DigitsEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ setShowSoftInputOnFocus(false);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ final InputMethodManager imm =
+ ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE));
+ if (imm != null && imm.isActive(this)) {
+ imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final boolean ret = super.onTouchEvent(event);
+ // Must be done after super.onTouchEvent()
+ final InputMethodManager imm =
+ ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE));
+ if (imm != null && imm.isActive(this)) {
+ imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0);
+ }
+ return ret;
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml
new file mode 100644
index 0000000..4efa80d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_in_duration"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml
new file mode 100644
index 0000000..4e5a205
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_in_duration"
+ android:fromXDelta="-100%p"
+ android:toXDelta="0"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml
new file mode 100644
index 0000000..5a6dfaa
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2014, 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.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_in_duration"
+ android:fromXDelta="100%p"
+ android:toXDelta="0"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml
new file mode 100644
index 0000000..01ac482
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_out_duration"
+ android:fromYDelta="0"
+ android:toYDelta="100%p"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml
new file mode 100644
index 0000000..5ac1d29
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_out_duration"
+ android:fromXDelta="0"
+ android:toXDelta="-100%"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml
new file mode 100644
index 0000000..5f56902
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2014, 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.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_out_duration"
+ android:fromXDelta="0"
+ android:toXDelta="100%"/>
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png
new file mode 100644
index 0000000..3380a89
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png
new file mode 100644
index 0000000..ff9753c
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png
new file mode 100644
index 0000000..7bf83fa
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png
new file mode 100644
index 0000000..1a9cd75
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png
new file mode 100644
index 0000000..e588d90
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png
new file mode 100644
index 0000000..4706112
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png
new file mode 100644
index 0000000..262e9df
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png
new file mode 100644
index 0000000..46630d4
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png
new file mode 100644
index 0000000..947aac1
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png
new file mode 100644
index 0000000..790f935
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png
new file mode 100644
index 0000000..40a1a84
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png
new file mode 100644
index 0000000..64a52d0
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png
new file mode 100644
index 0000000..e84d8f4
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png
new file mode 100644
index 0000000..0e720dd
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png
new file mode 100644
index 0000000..5dafee0
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png
new file mode 100644
index 0000000..e8bab3f
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png
new file mode 100644
index 0000000..6bd53f5
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png
new file mode 100644
index 0000000..6bc4372
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png
new file mode 100644
index 0000000..87bc113
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png
new file mode 100644
index 0000000..0b4e183
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png
new file mode 100644
index 0000000..9156076
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png
new file mode 100644
index 0000000..2b0dba7
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png
new file mode 100644
index 0000000..7e4fd3e
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png
new file mode 100644
index 0000000..6866fa4
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png
new file mode 100644
index 0000000..51b4401
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png
new file mode 100644
index 0000000..186508a
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png
new file mode 100644
index 0000000..a0a7c9d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png
new file mode 100644
index 0000000..92526f5
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png
new file mode 100644
index 0000000..59d9b95
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png
new file mode 100644
index 0000000..aa8849e
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png
new file mode 100644
index 0000000..7af3396
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png
new file mode 100644
index 0000000..df42fee
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png
new file mode 100644
index 0000000..c974a80
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png
new file mode 100644
index 0000000..c6e8be0
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png
new file mode 100644
index 0000000..9028bd4
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml b/java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml
new file mode 100644
index 0000000..53cd7a8
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml b/java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml
new file mode 100644
index 0000000..ee0f40a
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="270"
+ android:endColor="@android:color/darker_gray"
+ android:startColor="@android:color/transparent"/>
+</shape>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml
new file mode 100644
index 0000000..941fdb2
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<!-- A layout representing a single key in the dialpad -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="right|center_vertical"
+ android:baselineAligned="false"
+ android:orientation="horizontal">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"
+ android:layout_marginBottom="0dp"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"
+ android:layout_gravity="right"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"
+ android:layout_gravity="right|center"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml
new file mode 100644
index 0000000..65a2308
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/one"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="right|center_vertical"
+ android:baselineAligned="false"
+ android:orientation="horizontal">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"
+ android:layout_marginBottom="0dp"
+ android:layout_marginRight="@dimen/dialpad_key_one_margin_right"
+ android:layout_gravity="right"/>
+ <FrameLayout
+ android:layout_width="@dimen/dialpad_key_text_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left|center">
+ <ImageView
+ android:id="@+id/dialpad_key_voicemail"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_dialpad_voicemail"
+ android:tint="@color/dialpad_voicemail_tint"/>
+ </FrameLayout>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml
new file mode 100644
index 0000000..98c3531
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pound"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="center_vertical|right"
+ android:orientation="horizontal">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@id/dialpad_key_number"
+ style="@style/DialpadKeyPoundStyle"
+ android:layout_width="@dimen/dialpad_key_number_width"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"/>
+ <View
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml
new file mode 100644
index 0000000..b91c716
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/star"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="center_vertical|right"
+ android:orientation="horizontal">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@id/dialpad_key_number"
+ style="@style/DialpadKeyStarStyle"
+ android:layout_width="@dimen/dialpad_key_number_width"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"/>
+ <View
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml
new file mode 100644
index 0000000..d885ddf
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- A layout representing the zero key in the dialpad, with the plus sign shifted up because it is
+ smaller than a regular letter -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/zero"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="right|center_vertical"
+ android:baselineAligned="false"
+ android:orientation="horizontal">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadBottomKeyNumberStyle"
+ android:layout_marginBottom="0dp"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad.xml
new file mode 100644
index 0000000..5a14d14
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<!-- Dialpad in the Phone app. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dialpad"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include layout="@layout/dialpad_key_one"/>
+ <include
+ android:id="@+id/two"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/three"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include
+ android:id="@+id/four"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/five"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/six"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include
+ android:id="@+id/seven"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/eight"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/nine"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include layout="@layout/dialpad_key_star"/>
+ <include layout="@layout/dialpad_key_zero"/>
+ <include layout="@layout/dialpad_key_pound"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="?attr/dialpad_end_key_spacing"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml
new file mode 100644
index 0000000..77e4fc5
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<!-- A layout representing a single key in the dialpad -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout style="@style/DialpadKeyInternalLayoutStyle">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml
new file mode 100644
index 0000000..36f62c8
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/one"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"/>
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <ImageView
+ android:id="@+id/dialpad_key_voicemail"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:paddingTop="@dimen/dialpad_voicemail_icon_padding_top"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_dialpad_voicemail"
+ android:tint="?attr/dialpad_voicemail_tint"/>
+ <!-- Place empty text view so vertical height is same as other dialpad keys. -->
+ <TextView style="@style/DialpadKeyLettersStyle"/>
+ </RelativeLayout>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml
new file mode 100644
index 0000000..d37a6aa
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pound"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@id/dialpad_key_number"
+ style="@style/DialpadKeyPoundStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml
new file mode 100644
index 0000000..d288475
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/star"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyStarStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml
new file mode 100644
index 0000000..943ae48
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- A layout representing the zero key in the dialpad, with the plus sign shifted up because it is
+ smaller than a regular letter -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/zero"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout style="@style/DialpadKeyInternalLayoutStyle">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadBottomKeyNumberStyle"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml
new file mode 100644
index 0000000..47112fb
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:theme="?attr/dialpad_style">
+ <include layout="@layout/dialpad_view_unthemed"/>
+</FrameLayout>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml
new file mode 100644
index 0000000..4b08c6d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dialpad_view"
+ class="com.android.dialer.dialpadview.DialpadView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="bottom"
+ android:background="?attr/dialpad_background"
+ android:clickable="true"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <!-- Text field where call rate is displayed for ILD calls. -->
+ <LinearLayout
+ android:id="@+id/rate_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <LinearLayout
+ android:id="@+id/ild_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/ild_margin_height"
+ android:layout_marginBottom="@dimen/ild_margin_height"
+ android:layout_gravity="center_horizontal"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/ild_country"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/ild_rate"
+ android:textStyle="bold"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"/>
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#e3e3e3"/>
+
+ </LinearLayout>
+
+ <!-- Text field and possibly soft menu button above the keypad where
+ the digits are displayed. -->
+ <LinearLayout
+ android:id="@+id/digits_container"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/dialpad_digits_adjustable_height"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/dialpad_back"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/dialpad_overflow_margin"
+ android:paddingLeft="@dimen/dialpad_digits_menu_left_padding"
+ android:paddingRight="@dimen/dialpad_digits_menu_right_padding"
+ android:background="@drawable/btn_dialpad_key"
+ android:contentDescription="@string/description_dialpad_back"
+ android:gravity="center"
+ android:src="@drawable/ic_close_black_24dp"
+ android:tint="?attr/dialpad_icon_tint"
+ android:tintMode="src_in"
+ android:visibility="gone"/>
+
+ <ImageButton
+ android:id="@+id/dialpad_overflow"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/dialpad_overflow_margin"
+ android:paddingLeft="@dimen/dialpad_digits_menu_left_padding"
+ android:paddingRight="@dimen/dialpad_digits_menu_right_padding"
+ android:background="@drawable/btn_dialpad_key"
+ android:contentDescription="@string/description_dialpad_overflow"
+ android:gravity="center"
+ android:src="@drawable/ic_overflow_menu"
+ android:tint="?attr/dialpad_icon_tint"
+ android:tintMode="src_in"
+ android:visibility="gone"/>
+
+ <view xmlns:ex="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/digits"
+ class="com.android.dialer.dialpadview.DigitsEditText"
+ android:textStyle="normal"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent"
+ android:cursorVisible="false"
+ android:focusableInTouchMode="true"
+ android:fontFamily="sans-serif"
+ android:freezesText="true"
+ android:gravity="center"
+ android:maxLines="1"
+ android:scrollHorizontally="true"
+ android:singleLine="true"
+ android:textColor="?attr/dialpad_text_color"
+ android:textCursorDrawable="@null"
+ android:textSize="?attr/dialpad_digits_adjustable_text_size"
+ ex:resizing_text_min_size="@dimen/dialpad_digits_text_min_size"/>
+
+ <ImageButton
+ android:id="@+id/deleteButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/dialpad_digits_padding"
+ android:paddingRight="@dimen/dialpad_digits_padding"
+ android:background="@drawable/btn_dialpad_key"
+ android:contentDescription="@string/description_delete_button"
+ android:src="@drawable/ic_dialpad_delete"
+ android:state_enabled="false"
+ android:tint="?attr/dialpad_icon_tint"
+ android:tintMode="src_in"/>
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#e3e3e3"/>
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialpad_space_above_keys"/>
+
+ <include layout="@layout/dialpad"/>
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialpad_space_below_keys"/>
+
+</view>
diff --git a/java/com/android/dialer/dialpadview/res/values-land/dimens.xml b/java/com/android/dialer/dialpadview/res/values-land/dimens.xml
new file mode 100644
index 0000000..617134a
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values-land/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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="dialpad_key_margin_right">5dp</dimen>
+ <!-- Right margins for specific keys to align them correctly -->
+ <dimen name="dialpad_key_one_margin_right">3dp</dimen>
+ <dimen name="dialpad_key_text_width">35dp</dimen>
+ <dimen name="dialpad_key_number_width">20sp</dimen>
+ <dimen name="dialpad_symbol_margin_bottom">0dp</dimen>
+
+ <!-- The bottom space of the dialpad to account for the dial button -->
+ <dimen name="dialpad_bottom_space_height">65dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values-land/styles.xml b/java/com/android/dialer/dialpadview/res/values-land/styles.xml
new file mode 100644
index 0000000..f983725
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values-land/styles.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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>
+
+ <style name="DialpadKeyNumberStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_primary</item>
+ <item name="android:textSize">?attr/dialpad_key_numbers_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:layout_width">@dimen/dialpad_key_number_width</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">?attr/dialpad_key_number_margin_bottom</item>
+ </style>
+
+ <style name="DialpadKeyLettersStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_letters_size</item>
+ <item name="android:fontFamily">sans-serif-regular</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">left</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/animation_constants.xml b/java/com/android/dialer/dialpadview/res/values/animation_constants.xml
new file mode 100644
index 0000000..edd19d7
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/animation_constants.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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>
+ <integer name="dialpad_slide_in_duration">400</integer>
+ <integer name="dialpad_slide_out_duration">400</integer>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/attrs.xml b/java/com/android/dialer/dialpadview/res/values/attrs.xml
new file mode 100644
index 0000000..273879f
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/attrs.xml
@@ -0,0 +1,39 @@
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <attr format="reference" name="dialpad_style"/>
+ <attr format="dimension" name="dialpad_end_key_spacing"/>
+
+ <declare-styleable name="Dialpad">
+ <attr format="color" name="dialpad_key_button_touch_tint"/>
+ <attr format="dimension" name="dialpad_digits_adjustable_text_size"/>
+ <attr format="dimension" name="dialpad_digits_adjustable_height"/>
+ <attr format="dimension" name="dialpad_key_numbers_size"/>
+ <attr format="dimension" name="dialpad_key_number_margin_bottom"/>
+ <attr format="dimension" name="dialpad_zero_key_number_margin_bottom"/>
+ </declare-styleable>
+
+ <declare-styleable name="Theme.Dialpad">
+ <attr format="color" name="dialpad_text_color"/>
+ <attr format="color" name="dialpad_text_color_primary"/>
+ <attr format="color" name="dialpad_text_color_secondary"/>
+ <attr format="color" name="dialpad_icon_tint"/>
+ <attr format="color" name="dialpad_voicemail_tint"/>
+ <attr format="color" name="dialpad_background"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/colors.xml b/java/com/android/dialer/dialpadview/res/values/colors.xml
new file mode 100644
index 0000000..d27468d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/colors.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2012 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>
+ <!-- Colors for the dialpad -->
+ <color name="background_dialpad">#fcfcfc</color>
+ <color name="background_dialpad_pressed">#ececec</color>
+ <color name="dialpad_primary_text_color">@color/dialer_theme_color</color>
+ <color name="dialpad_secondary_text_color">#737373</color>
+ <color name="dialpad_digits_text_color">#333</color>
+ <color name="dialpad_separator_line_color">#dadada</color>
+ <color name="dialpad_icon_tint">#89000000</color>
+ <color name="dialpad_voicemail_tint">#919191</color>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/dimens.xml b/java/com/android/dialer/dialpadview/res/values/dimens.xml
new file mode 100644
index 0000000..210c816
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/dimens.xml
@@ -0,0 +1,48 @@
+<!--
+ ~ Copyright (C) 2012 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 dimensions for dialpad keys -->
+ <dimen name="dialpad_key_numbers_default_size">36sp</dimen>
+ <dimen name="dialpad_key_letters_size">12sp</dimen>
+ <dimen name="dialpad_key_pound_size">23sp</dimen>
+ <dimen name="dialpad_key_star_size">36sp</dimen>
+ <dimen name="dialpad_key_height">64dp</dimen>
+ <dimen name="dialpad_key_number_default_margin_bottom">3dp</dimen>
+ <!-- Zero key should have less space between self and text because "+" is smaller -->
+ <dimen name="dialpad_zero_key_number_default_margin_bottom">1dp</dimen>
+ <dimen name="dialpad_symbol_margin_bottom">13dp</dimen>
+ <dimen name="dialpad_key_plus_size">18sp</dimen>
+ <dimen name="dialpad_horizontal_padding">5dp</dimen>
+ <dimen name="dialpad_digits_text_size">34sp</dimen>
+ <dimen name="dialpad_digits_text_min_size">24sp</dimen>
+ <dimen name="dialpad_digits_height">60dp</dimen>
+ <dimen name="dialpad_digits_padding">16dp</dimen>
+ <dimen name="dialpad_digits_menu_left_padding">8dp</dimen>
+ <dimen name="dialpad_digits_menu_right_padding">10dp</dimen>
+ <dimen name="dialpad_center_margin">3dp</dimen>
+ <dimen name="dialpad_button_margin">2dp</dimen>
+ <dimen name="dialpad_voicemail_icon_padding_top">2dp</dimen>
+ <dimen name="dialpad_key_button_translate_y">100dp</dimen>
+ <dimen name="dialpad_overflow_margin">8dp</dimen>
+ <dimen name="dialpad_space_above_keys">14dp</dimen>
+ <dimen name="dialpad_space_below_keys">8dp</dimen>
+ <!-- The bottom space of the dialpad to account for the dial button -->
+ <dimen name="dialpad_bottom_space_height">80dp</dimen>
+
+ <!-- Top/Bottom padding around the ILD rate display box. -->
+ <dimen name="ild_margin_height">10dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/strings.xml b/java/com/android/dialer/dialpadview/res/values/strings.xml
new file mode 100644
index 0000000..920e6e2
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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>
+ <string name="dialpad_star_number" translatable="false">*</string>
+ <string name="dialpad_pound_number" translatable="false">#</string>
+
+ <string name="dialpad_0_letters" translatable="false">+</string>
+ <string name="dialpad_1_letters" translatable="false"></string>
+ <string name="dialpad_2_letters" translatable="false">ABC</string>
+ <string name="dialpad_3_letters" translatable="false">DEF</string>
+ <string name="dialpad_4_letters" translatable="false">GHI</string>
+ <string name="dialpad_5_letters" translatable="false">JKL</string>
+ <string name="dialpad_6_letters" translatable="false">MNO</string>
+ <string name="dialpad_7_letters" translatable="false">PQRS</string>
+ <string name="dialpad_8_letters" translatable="false">TUV</string>
+ <string name="dialpad_9_letters" translatable="false">WXYZ</string>
+ <string name="dialpad_star_letters" translatable="false"></string>
+ <string name="dialpad_pound_letters" translatable="false"></string>
+
+ <!-- String describing the back button in the dialpad. -->
+ <string name="description_dialpad_back">Navigate back</string>
+
+ <!-- String describing the overflow menu button in the dialpad. -->
+ <string name="description_dialpad_overflow">More options</string>
+
+ <!-- String describing the Delete/Backspace ImageButton.
+ Used by AccessibilityService to announce the purpose of the button.
+ -->
+ <string name="description_delete_button">backspace</string>
+
+ <!-- String describing the button used to add a plus (+) symbol to the dialpad -->
+ <string name="description_image_button_plus">plus</string>
+
+ <!-- String describing the Voicemail ImageButton.
+ Used by AccessibilityService to announce the purpose of the button.
+ -->
+ <string name="description_voicemail_button">voicemail</string>
+
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/styles.xml b/java/com/android/dialer/dialpadview/res/values/styles.xml
new file mode 100644
index 0000000..2fa2c3f
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/styles.xml
@@ -0,0 +1,118 @@
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <style name="DialpadSpaceStyle">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_weight">3</item>
+ </style>
+
+ <style name="DialpadKeyNumberStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_primary</item>
+ <item name="android:textSize">?attr/dialpad_key_numbers_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">?attr/dialpad_key_number_margin_bottom</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="DialpadBottomKeyNumberStyle" parent="DialpadKeyNumberStyle">
+ <item name="android:layout_marginBottom">?attr/dialpad_zero_key_number_margin_bottom</item>
+ </style>
+
+ <style name="DialpadKeyStarStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_star_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:alpha">0.8</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="DialpadKeyPoundStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_pound_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:alpha">0.8</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="DialpadKeyLettersStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_letters_size</item>
+ <item name="android:fontFamily">sans-serif-regular</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">center_horizontal</item>
+ </style>
+
+ <style name="DialpadKeyButtonStyle">
+ <item name="android:soundEffectsEnabled">false</item>
+ <item name="android:clickable">true</item>
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_weight">13</item>
+ <item name="android:minHeight">@dimen/dialpad_key_height</item>
+ <item name="android:background">@drawable/btn_dialpad_key</item>
+ <item name="android:focusable">true</item>
+ </style>
+
+ <style name="DialpadKeyInternalLayoutStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:gravity">center</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <style name="Dialpad">
+ <item name="dialpad_digits_adjustable_height">@dimen/dialpad_digits_height</item>
+ <item name="dialpad_digits_adjustable_text_size">@dimen/dialpad_digits_text_size</item>
+ <item name="dialpad_key_numbers_size">@dimen/dialpad_key_numbers_default_size</item>
+ <item name="dialpad_key_number_margin_bottom">@dimen/dialpad_key_number_default_margin_bottom
+ </item>
+ <item name="dialpad_zero_key_number_margin_bottom">
+ @dimen/dialpad_zero_key_number_default_margin_bottom
+ </item>
+ <item name="dialpad_end_key_spacing">@dimen/dialpad_bottom_space_height</item>
+ </style>
+
+ <style name="Dialpad.Light">
+ <item name="dialpad_text_color">@color/dialpad_digits_text_color</item>
+ <item name="dialpad_text_color_primary">@color/dialpad_primary_text_color</item>
+ <item name="dialpad_text_color_secondary">@color/dialpad_secondary_text_color</item>
+ <item name="dialpad_icon_tint">@color/dialpad_icon_tint</item>
+ <item name="dialpad_voicemail_tint">@color/dialpad_voicemail_tint</item>
+ <item name="dialpad_background">@color/background_dialpad</item>
+ </style>
+
+ <style name="Dialpad.Dark">
+ <item name="dialpad_text_color">@android:color/white</item>
+ <item name="dialpad_text_color_primary">@android:color/white</item>
+ <item name="dialpad_text_color_secondary">#ffd4d6d7</item>
+ <item name="dialpad_icon_tint">@android:color/white</item>
+ <item name="dialpad_voicemail_tint">?attr/dialpad_text_color_secondary</item>
+ <item name="dialpad_background">#00000000</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/disabled_lint_checks.txt b/java/com/android/dialer/disabled_lint_checks.txt
new file mode 100644
index 0000000..13a3d05
--- /dev/null
+++ b/java/com/android/dialer/disabled_lint_checks.txt
@@ -0,0 +1 @@
+InlinedApi
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
new file mode 100644
index 0000000..14299f9
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
@@ -0,0 +1,76 @@
+/*
+ * 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.enrichedcall;
+
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_EnrichedCallCapabilities extends EnrichedCallCapabilities {
+
+ private final boolean supportsCallComposer;
+ private final boolean supportsPostCall;
+
+ AutoValue_EnrichedCallCapabilities(
+ boolean supportsCallComposer,
+ boolean supportsPostCall) {
+ this.supportsCallComposer = supportsCallComposer;
+ this.supportsPostCall = supportsPostCall;
+ }
+
+ @Override
+ public boolean supportsCallComposer() {
+ return supportsCallComposer;
+ }
+
+ @Override
+ public boolean supportsPostCall() {
+ return supportsPostCall;
+ }
+
+ @Override
+ public String toString() {
+ return "EnrichedCallCapabilities{"
+ + "supportsCallComposer=" + supportsCallComposer + ", "
+ + "supportsPostCall=" + supportsPostCall + ", "
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof EnrichedCallCapabilities) {
+ EnrichedCallCapabilities that = (EnrichedCallCapabilities) o;
+ return (this.supportsCallComposer == that.supportsCallComposer())
+ && (this.supportsPostCall == that.supportsPostCall());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.supportsCallComposer ? 1231 : 1237;
+ h *= 1000003;
+ h ^= this.supportsPostCall ? 1231 : 1237;
+ return h;
+ }
+
+}
+
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
new file mode 100644
index 0000000..edfefc4
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
@@ -0,0 +1,127 @@
+/*
+ * 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.enrichedcall;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_OutgoingCallComposerData extends OutgoingCallComposerData {
+
+ private final String subject;
+ private final Uri imageUri;
+ private final String imageContentType;
+
+ private AutoValue_OutgoingCallComposerData(
+ @Nullable String subject,
+ @Nullable Uri imageUri,
+ @Nullable String imageContentType) {
+ this.subject = subject;
+ this.imageUri = imageUri;
+ this.imageContentType = imageContentType;
+ }
+
+ @Nullable
+ @Override
+ public String getSubject() {
+ return subject;
+ }
+
+ @Nullable
+ @Override
+ public Uri getImageUri() {
+ return imageUri;
+ }
+
+ @Nullable
+ @Override
+ public String getImageContentType() {
+ return imageContentType;
+ }
+
+ @Override
+ public String toString() {
+ return "OutgoingCallComposerData{"
+ + "subject=" + subject + ", "
+ + "imageUri=" + imageUri + ", "
+ + "imageContentType=" + imageContentType
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof OutgoingCallComposerData) {
+ OutgoingCallComposerData that = (OutgoingCallComposerData) o;
+ return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
+ && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
+ && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (subject == null) ? 0 : this.subject.hashCode();
+ h *= 1000003;
+ h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
+ h *= 1000003;
+ h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
+ return h;
+ }
+
+ static final class Builder extends OutgoingCallComposerData.Builder {
+ private String subject;
+ private Uri imageUri;
+ private String imageContentType;
+ Builder() {
+ }
+ private Builder(OutgoingCallComposerData source) {
+ this.subject = source.getSubject();
+ this.imageUri = source.getImageUri();
+ this.imageContentType = source.getImageContentType();
+ }
+ @Override
+ public OutgoingCallComposerData.Builder setSubject(@Nullable String subject) {
+ this.subject = subject;
+ return this;
+ }
+ @Override
+ OutgoingCallComposerData.Builder setImageUri(@Nullable Uri imageUri) {
+ this.imageUri = imageUri;
+ return this;
+ }
+ @Override
+ OutgoingCallComposerData.Builder setImageContentType(@Nullable String imageContentType) {
+ this.imageContentType = imageContentType;
+ return this;
+ }
+ @Override
+ OutgoingCallComposerData autoBuild() {
+ return new AutoValue_OutgoingCallComposerData(
+ this.subject,
+ this.imageUri,
+ this.imageContentType);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
new file mode 100644
index 0000000..b7d7809
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
@@ -0,0 +1,36 @@
+/*
+ * 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.enrichedcall;
+
+
+
+/** Value type holding enriched call capabilities. */
+
+public abstract class EnrichedCallCapabilities {
+
+ public static final EnrichedCallCapabilities NO_CAPABILITIES =
+ EnrichedCallCapabilities.create(false, false);
+
+ public static EnrichedCallCapabilities create(
+ boolean supportsCallComposer, boolean supportsPostCall) {
+ return new AutoValue_EnrichedCallCapabilities(supportsCallComposer, supportsPostCall);
+ }
+
+ public abstract boolean supportsCallComposer();
+
+ public abstract boolean supportsPostCall();
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
new file mode 100644
index 0000000..6af8c40
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
@@ -0,0 +1,225 @@
+/*
+ * 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.enrichedcall;
+
+import android.app.Application;
+import android.support.annotation.IntDef;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.multimedia.MultimediaData;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Performs all enriched calling logic. */
+public interface EnrichedCallManager {
+
+ /** Factory for {@link EnrichedCallManager}. */
+ interface Factory {
+ EnrichedCallManager getEnrichedCallManager();
+ }
+
+ /** Accessor for {@link EnrichedCallManager}. */
+ class Accessor {
+
+ /**
+ * @throws IllegalArgumentException if application does not implement {@link
+ * EnrichedCallManager.Factory}
+ */
+ @NonNull
+ public static EnrichedCallManager getInstance(@NonNull Application application) {
+ Assert.isNotNull(application);
+
+ return ((EnrichedCallManager.Factory) application).getEnrichedCallManager();
+ }
+ }
+
+ /** Receives updates when enriched call capabilities are ready. */
+ interface CapabilitiesListener {
+
+ /** Callback fired when the capabilities are updated. */
+ @MainThread
+ void onCapabilitiesUpdated();
+ }
+
+ /**
+ * Registers the given {@link CapabilitiesListener}.
+ *
+ * <p>As a result of this method, the listener will receive a call to {@link
+ * CapabilitiesListener#onCapabilitiesUpdated()} after a call to {@link
+ * #requestCapabilities(String)}.
+ */
+ @MainThread
+ void registerCapabilitiesListener(@NonNull CapabilitiesListener listener);
+
+ /**
+ * Starts an asynchronous process to get enriched call capabilities of the given number.
+ *
+ * <p>Registered listeners will receive a call to {@link
+ * CapabilitiesListener#onCapabilitiesUpdated()} on completion.
+ *
+ * @param number the remote number in any format
+ */
+ @MainThread
+ void requestCapabilities(@NonNull String number);
+
+ /**
+ * Unregisters the given {@link CapabilitiesListener}.
+ *
+ * <p>As a result of this method, the listener will not receive capabilities of the given number.
+ */
+ @MainThread
+ void unregisterCapabilitiesListener(@NonNull CapabilitiesListener listener);
+
+ /** Gets the cached capabilities for the given number, else null */
+ @MainThread
+ @Nullable
+ EnrichedCallCapabilities getCapabilities(@NonNull String number);
+
+ /** Clears any cached data, such as capabilities. */
+ @MainThread
+ void clearCachedData();
+
+ /** Possible states for call composer sessions. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_NONE,
+ STATE_STARTING,
+ STATE_STARTED,
+ STATE_START_FAILED,
+ STATE_MESSAGE_SENT,
+ STATE_MESSAGE_FAILED,
+ STATE_CLOSED,
+ })
+ @interface State {}
+
+ int STATE_NONE = 0;
+ int STATE_STARTING = STATE_NONE + 1;
+ int STATE_STARTED = STATE_STARTING + 1;
+ int STATE_START_FAILED = STATE_STARTED + 1;
+ int STATE_MESSAGE_SENT = STATE_START_FAILED + 1;
+ int STATE_MESSAGE_FAILED = STATE_MESSAGE_SENT + 1;
+ int STATE_CLOSED = STATE_MESSAGE_FAILED + 1;
+
+ /**
+ * Starts a call composer session with the given remote number.
+ *
+ * @param number the remote number in any format
+ * @return the id for the started session, or {@link Session#NO_SESSION_ID} if the session fails
+ */
+ @MainThread
+ long startCallComposerSession(@NonNull String number);
+
+ /**
+ * Sends the given information through an open enriched call session. As per the enriched calling
+ * spec, up to two messages are sent: the first is an enriched call data message that optionally
+ * includes the subject and the second is the optional image data message.
+ *
+ * @param sessionId the id for the session. See {@link #startCallComposerSession(String)}
+ * @param data the {@link MultimediaData}
+ * @throws IllegalArgumentException if there's no open session with the given number
+ * @throws IllegalStateException if the session isn't in the {@link #STATE_STARTED} state
+ */
+ @MainThread
+ void sendCallComposerData(long sessionId, @NonNull MultimediaData data);
+
+ /**
+ * Ends the given call composer session. Ending a session means that the call composer session
+ * will be closed.
+ *
+ * @param sessionId the id of the session to end
+ */
+ @MainThread
+ void endCallComposerSession(long sessionId);
+
+ /**
+ * Called once the capabilities are available for a corresponding call to {@link
+ * #requestCapabilities(String)}.
+ *
+ * @param number the remote number in any format
+ * @param capabilities the supported capabilities
+ */
+ @MainThread
+ void onCapabilitiesReceived(
+ @NonNull String number, @NonNull EnrichedCallCapabilities capabilities);
+
+ /** Receives updates when the state of an enriched call changes. */
+ interface StateChangedListener {
+
+ /**
+ * Callback fired when state changes. Listeners should call {@link #getSession(String)} to
+ * retrieve the new state.
+ */
+ void onEnrichedCallStateChanged();
+ }
+
+ /**
+ * Registers the given {@link StateChangedListener}.
+ *
+ * <p>As a result of this method, the listener will receive updates when the state of any enriched
+ * call changes.
+ */
+ @MainThread
+ void registerStateChangedListener(@NonNull StateChangedListener listener);
+
+ /** Returns the {@link Session} for the given number, or {@code null} if no session exists. */
+ @MainThread
+ @Nullable
+ Session getSession(@NonNull String number);
+
+ /** Returns the {@link Session} for the given sessionId, or {@code null} if no session exists. */
+ @MainThread
+ @Nullable
+ Session getSession(long sessionId);
+
+ /**
+ * Unregisters the given {@link StateChangedListener}.
+ *
+ * <p>As a result of this method, the listener will not receive updates when the state of enriched
+ * calls changes.
+ */
+ @MainThread
+ void unregisterStateChangedListener(@NonNull StateChangedListener listener);
+
+ /**
+ * Called when the status of an enriched call session changes.
+ *
+ *
+ * @throws IllegalArgumentException if the state is invalid
+ */
+ @MainThread
+ void onSessionStatusUpdate(long sessionId, @NonNull String number, int state);
+
+ /**
+ * Called when the status of an enriched call message updates.
+ *
+ *
+ * @throws IllegalArgumentException if the state is invalid
+ * @throws IllegalStateException if there's no session for the given id
+ */
+ @MainThread
+ void onMessageUpdate(long sessionId, @NonNull String messageId, int state);
+
+ /**
+ * Called when call composer data arrives for the given session.
+ *
+ * @throws IllegalStateException if there's no session for the given id
+ */
+ @MainThread
+ void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData);
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java b/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java
new file mode 100644
index 0000000..db9a799
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java
@@ -0,0 +1,84 @@
+/*
+ * 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.enrichedcall;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.multimedia.MultimediaData;
+
+/** Stub implementation of {@link EnrichedCallManager}. */
+public final class EnrichedCallManagerStub implements EnrichedCallManager {
+
+ @Override
+ public void registerCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
+
+ @Override
+ public void requestCapabilities(@NonNull String number) {}
+
+ @Override
+ public void unregisterCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
+
+ @Override
+ public EnrichedCallCapabilities getCapabilities(@NonNull String number) {
+ return null;
+ }
+
+ @Override
+ public void clearCachedData() {}
+
+ @Override
+ public long startCallComposerSession(@NonNull String number) {
+ return Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public void sendCallComposerData(long sessionId, @NonNull MultimediaData data) {}
+
+ @Override
+ public void endCallComposerSession(long sessionId) {}
+
+ @Override
+ public void onCapabilitiesReceived(
+ @NonNull String number, @NonNull EnrichedCallCapabilities capabilities) {}
+
+ @Override
+ public void registerStateChangedListener(@NonNull StateChangedListener listener) {}
+
+ @Nullable
+ @Override
+ public Session getSession(@NonNull String number) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Session getSession(long sessionId) {
+ return null;
+ }
+
+ @Override
+ public void unregisterStateChangedListener(@NonNull StateChangedListener listener) {}
+
+ @Override
+ public void onSessionStatusUpdate(long sessionId, @NonNull String number, int state) {}
+
+ @Override
+ public void onMessageUpdate(long sessionId, @NonNull String messageId, int state) {}
+
+ @Override
+ public void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData) {}
+}
diff --git a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
new file mode 100644
index 0000000..a8ee49d
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
@@ -0,0 +1,94 @@
+/*
+ * 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.enrichedcall;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+
+
+/**
+ * Value type holding references to all data that could be provided for the call composer.
+ *
+ * <p>Note: Either the subject, the image data, or both must be specified, e.g.
+ *
+ * <pre>
+ * OutgoingCallComposerData.builder.build(); // throws exception, no data set
+ * OutgoingCallComposerData
+ * .setSubject(subject)
+ * .build(); // Success
+ * OutgoingCallComposerData
+ * .setImageData(uri, contentType)
+ * .build(); // Success
+ * OutgoingCallComposerData
+ * .setSubject(subject)
+ * .setImageData(uri, contentType)
+ * .build(); // Success
+ * </pre>
+ */
+
+public abstract class OutgoingCallComposerData {
+
+ public static Builder builder() {
+ return new AutoValue_OutgoingCallComposerData.Builder();
+ }
+
+ public boolean hasImageData() {
+ return getImageUri() != null && getImageContentType() != null;
+ }
+
+ @Nullable
+ public abstract String getSubject();
+
+ @Nullable
+ public abstract Uri getImageUri();
+
+ @Nullable
+ public abstract String getImageContentType();
+
+ /** Builds instances of {@link OutgoingCallComposerData}. */
+
+ public abstract static class Builder {
+ public abstract Builder setSubject(String subject);
+
+ public Builder setImageData(@NonNull Uri imageUri, @NonNull String imageContentType) {
+ setImageUri(Assert.isNotNull(imageUri));
+ setImageContentType(Assert.isNotNull(imageContentType));
+ return this;
+ }
+
+ abstract Builder setImageUri(Uri imageUri);
+
+ abstract Builder setImageContentType(String imageContentType);
+
+ abstract OutgoingCallComposerData autoBuild();
+
+ /**
+ * Returns the OutgoingCallComposerData from this builder.
+ *
+ * @return the OutgoingCallComposerData.
+ * @throws IllegalStateException if neither {@link #setSubject(String)} nor {@link
+ * #setImageData(Uri, String)} were called.
+ */
+ public OutgoingCallComposerData build() {
+ OutgoingCallComposerData data = autoBuild();
+ Assert.checkState(data.getSubject() != null || data.hasImageData());
+ return data;
+ }
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/Session.java b/java/com/android/dialer/enrichedcall/Session.java
new file mode 100644
index 0000000..b0439fa
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/Session.java
@@ -0,0 +1,63 @@
+/*
+ * 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.enrichedcall;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.enrichedcall.EnrichedCallManager.State;
+import com.android.dialer.multimedia.MultimediaData;
+
+/** Holds state information and data about enriched calling sessions. */
+public interface Session {
+
+ /** Id used for sessions that fail to start. */
+ long NO_SESSION_ID = -1;
+
+ /**
+ * An id for the specific case when sending a message fails so early that a message id isn't
+ * created.
+ */
+ String MESSAGE_ID_COULD_NOT_CREATE_ID = "messageIdCouldNotCreateId";
+
+ /**
+ * Returns the id associated with this session, or {@link #NO_SESSION_ID} if this represents a
+ * session that failed to start.
+ */
+ long getSessionId();
+
+ /** Returns the number associated with the remote end of this session. */
+ @NonNull
+ String getRemoteNumber();
+
+ /** Returns the {@link State} for this session. */
+ @State
+ int getState();
+
+ /** Returns the {@link MultimediaData} associated with this session. */
+ @NonNull
+ MultimediaData getMultimediaData();
+
+ /** Returns type of this session, based on some arbitrarily defined type. */
+ int getType();
+
+ /**
+ * Sets the {@link MultimediaData} for this session.
+ *
+ *
+ * @throws IllegalArgumentException if the type is invalid
+ */
+ void setSessionData(@NonNull MultimediaData multimediaData, int type);
+}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
new file mode 100644
index 0000000..39c55d0
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
@@ -0,0 +1,32 @@
+/*
+ * 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.enrichedcall;
+
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** Module which binds {@link EnrichedCallManagerStub}. */
+@Module
+public class StubEnrichedCallModule {
+
+ @Provides
+ @Singleton
+ static EnrichedCallManager provideEnrichedCallManager() {
+ return new EnrichedCallManagerStub();
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/extensions/StateExtension.java b/java/com/android/dialer/enrichedcall/extensions/StateExtension.java
new file mode 100644
index 0000000..8a4f640
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/extensions/StateExtension.java
@@ -0,0 +1,54 @@
+/*
+ * 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.enrichedcall.extensions;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.State;
+
+/** Extends the {@link State} to include a toString method. */
+public class StateExtension {
+
+ /** Returns the string representation for the given {@link State}. */
+ @NonNull
+ public static String toString(@State int callComposerState) {
+ if (callComposerState == EnrichedCallManager.STATE_NONE) {
+ return "STATE_NONE";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_STARTING) {
+ return "STATE_STARTING";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_STARTED) {
+ return "STATE_STARTED";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_START_FAILED) {
+ return "STATE_START_FAILED";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_MESSAGE_SENT) {
+ return "STATE_MESSAGE_SENT";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_MESSAGE_FAILED) {
+ return "STATE_MESSAGE_FAILED";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_CLOSED) {
+ return "STATE_CLOSED";
+ }
+ Assert.checkArgument(false, "Unexpected callComposerState: %d", callComposerState);
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/inject/ApplicationModule.java b/java/com/android/dialer/inject/ApplicationModule.java
new file mode 100644
index 0000000..99e5296
--- /dev/null
+++ b/java/com/android/dialer/inject/ApplicationModule.java
@@ -0,0 +1,39 @@
+/*
+ * 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.inject;
+
+import android.app.Application;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import dagger.Module;
+import dagger.Provides;
+
+/** Provides the singleton application object. */
+@Module
+public final class ApplicationModule {
+
+ @NonNull private final Application application;
+
+ public ApplicationModule(@NonNull Application application) {
+ this.application = Assert.isNotNull(application);
+ }
+
+ @Provides
+ Application provideApplication() {
+ return application;
+ }
+}
diff --git a/java/com/android/dialer/inject/DialerAppComponent.java b/java/com/android/dialer/inject/DialerAppComponent.java
new file mode 100644
index 0000000..9832ce8
--- /dev/null
+++ b/java/com/android/dialer/inject/DialerAppComponent.java
@@ -0,0 +1,29 @@
+/*
+ * 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.inject;
+
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.StubEnrichedCallModule;
+import dagger.Component;
+import javax.inject.Singleton;
+
+/** Core application-wide {@link Component} for the open source dialer app. */
+@Singleton
+@Component(modules = {ApplicationModule.class, StubEnrichedCallModule.class})
+public interface DialerAppComponent {
+ EnrichedCallManager enrichedCallManager();
+}
diff --git a/java/com/android/dialer/interactions/AndroidManifest.xml b/java/com/android/dialer/interactions/AndroidManifest.xml
new file mode 100644
index 0000000..4571a69
--- /dev/null
+++ b/java/com/android/dialer/interactions/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.interactions">
+
+ <application>
+
+ <!-- Service to update a contact -->
+ <service
+ android:exported="false"
+ android:name="com.android.dialer.interactions.ContactUpdateService"/>
+
+ <receiver android:name="com.android.dialer.interactions.UndemoteOutgoingCallReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
+ </intent-filter>
+ </receiver>
+
+ </application>
+
+</manifest>
+
diff --git a/java/com/android/dialer/interactions/ContactUpdateService.java b/java/com/android/dialer/interactions/ContactUpdateService.java
new file mode 100644
index 0000000..9b2d701
--- /dev/null
+++ b/java/com/android/dialer/interactions/ContactUpdateService.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.interactions;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import com.android.contacts.common.database.ContactUpdateUtils;
+
+/** Service for updating primary number on a contact. */
+public class ContactUpdateService extends IntentService {
+
+ public static final String EXTRA_PHONE_NUMBER_DATA_ID = "phone_number_data_id";
+
+ public ContactUpdateService() {
+ super(ContactUpdateService.class.getSimpleName());
+ setIntentRedelivery(true);
+ }
+
+ /** Creates an intent that sets the selected data item as super primary (default) */
+ public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
+ Intent serviceIntent = new Intent(context, ContactUpdateService.class);
+ serviceIntent.putExtra(EXTRA_PHONE_NUMBER_DATA_ID, dataId);
+ return serviceIntent;
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ // Currently this service only handles one type of update.
+ long dataId = intent.getLongExtra(EXTRA_PHONE_NUMBER_DATA_ID, -1);
+
+ ContactUpdateUtils.setSuperPrimary(this, dataId);
+ }
+}
diff --git a/java/com/android/dialer/interactions/PhoneNumberInteraction.java b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
new file mode 100644
index 0000000..f36e531
--- /dev/null
+++ b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2010 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.interactions;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.IntDef;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.Collapser.Collapsible;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.CallIntentParser;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.TransactionSafeActivity;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Initiates phone calls or a text message. If there are multiple candidates, this class shows a
+ * dialog to pick one. Creating one of these interactions should be done through the static factory
+ * methods.
+ *
+ * <p>Note that this class initiates not only usual *phone* calls but also *SIP* calls.
+ *
+ * <p>TODO: clean up code and documents since it is quite confusing to use "phone numbers" or "phone
+ * calls" here while they can be SIP addresses or SIP calls (See also issue 5039627).
+ */
+public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
+
+ private static final String TAG = PhoneNumberInteraction.class.getSimpleName();
+ /** The identifier for a permissions request if one is generated. */
+ public static final int REQUEST_READ_CONTACTS = 1;
+
+ private static final String[] PHONE_NUMBER_PROJECTION =
+ new String[] {
+ Phone._ID,
+ Phone.NUMBER,
+ Phone.IS_SUPER_PRIMARY,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.MIMETYPE,
+ Phone.CONTACT_ID,
+ };
+
+ private static final String PHONE_NUMBER_SELECTION =
+ Data.MIMETYPE
+ + " IN ('"
+ + Phone.CONTENT_ITEM_TYPE
+ + "', "
+ + "'"
+ + SipAddress.CONTENT_ITEM_TYPE
+ + "') AND "
+ + Data.DATA1
+ + " NOT NULL";
+ private static final int UNKNOWN_CONTACT_ID = -1;
+ private final Context mContext;
+ private final int mInteractionType;
+ private final CallSpecificAppData mCallSpecificAppData;
+ private long mContactId = UNKNOWN_CONTACT_ID;
+ private CursorLoader mLoader;
+ private boolean mIsVideoCall;
+
+ /** Error codes for interactions. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ InteractionErrorCode.CONTACT_NOT_FOUND,
+ InteractionErrorCode.CONTACT_HAS_NO_NUMBER,
+ InteractionErrorCode.USER_LEAVING_ACTIVITY,
+ InteractionErrorCode.OTHER_ERROR
+ }
+ )
+ public @interface InteractionErrorCode {
+
+ int CONTACT_NOT_FOUND = 1;
+ int CONTACT_HAS_NO_NUMBER = 2;
+ int OTHER_ERROR = 3;
+ int USER_LEAVING_ACTIVITY = 4;
+ }
+
+ /**
+ * Activities which use this class must implement this. They will be notified if there was an
+ * error performing the interaction. For example, this callback will be invoked on the activity if
+ * the contact URI provided points to a deleted contact, or to a contact without a phone number.
+ */
+ public interface InteractionErrorListener {
+
+ void interactionError(@InteractionErrorCode int interactionErrorCode);
+ }
+
+ /**
+ * Activities which use this class must implement this. They will be notified if the phone number
+ * disambiguation dialog is dismissed.
+ */
+ public interface DisambigDialogDismissedListener {
+ void onDisambigDialogDismissed();
+ }
+
+ private PhoneNumberInteraction(
+ Context context,
+ int interactionType,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ mContext = context;
+ mInteractionType = interactionType;
+ mCallSpecificAppData = callSpecificAppData;
+ mIsVideoCall = isVideoCall;
+
+ Assert.checkArgument(context instanceof InteractionErrorListener);
+ Assert.checkArgument(context instanceof DisambigDialogDismissedListener);
+ Assert.checkArgument(context instanceof ActivityCompat.OnRequestPermissionsResultCallback);
+ }
+
+ private static void performAction(
+ Context context,
+ String phoneNumber,
+ int interactionType,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ Intent intent;
+ switch (interactionType) {
+ case ContactDisplayUtils.INTERACTION_SMS:
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null));
+ break;
+ default:
+ intent =
+ new CallIntentBuilder(phoneNumber, callSpecificAppData)
+ .setIsVideoCall(isVideoCall)
+ .build();
+ break;
+ }
+ DialerUtils.startActivityWithErrorToast(context, intent);
+ }
+
+ /**
+ * @param activity that is calling this interaction. This must be of type {@link
+ * TransactionSafeActivity} because we need to check on the activity state after the phone
+ * numbers have been queried for. The activity must implement {@link InteractionErrorListener}
+ * and {@link DisambigDialogDismissedListener}.
+ * @param isVideoCall {@code true} if the call is a video call, {@code false} otherwise.
+ */
+ public static void startInteractionForPhoneCall(
+ TransactionSafeActivity activity,
+ Uri uri,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ new PhoneNumberInteraction(
+ activity, ContactDisplayUtils.INTERACTION_CALL, isVideoCall, callSpecificAppData)
+ .startInteraction(uri);
+ }
+
+ private void performAction(String phoneNumber) {
+ PhoneNumberInteraction.performAction(
+ mContext, phoneNumber, mInteractionType, mIsVideoCall, mCallSpecificAppData);
+ }
+
+ /**
+ * Initiates the interaction to result in either a phone call or sms message for a contact.
+ *
+ * @param uri Contact Uri
+ */
+ private void startInteraction(Uri uri) {
+ // It's possible for a shortcut to have been created, and then Contacts permissions revoked. To
+ // avoid a crash when the user tries to use such a shortcut, check for this condition and ask
+ // the user for the permission.
+ if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("PhoneNumberInteraction.startInteraction", "No contact permissions");
+ ActivityCompat.requestPermissions(
+ (Activity) mContext,
+ new String[] {Manifest.permission.READ_CONTACTS},
+ REQUEST_READ_CONTACTS);
+ return;
+ }
+
+ if (mLoader != null) {
+ mLoader.reset();
+ }
+ final Uri queryUri;
+ final String inputUriAsString = uri.toString();
+ if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) {
+ if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) {
+ queryUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+ } else {
+ queryUri = uri;
+ }
+ } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) {
+ queryUri = uri;
+ } else {
+ throw new UnsupportedOperationException(
+ "Input Uri must be contact Uri or data Uri (input: \"" + uri + "\")");
+ }
+
+ mLoader =
+ new CursorLoader(
+ mContext, queryUri, PHONE_NUMBER_PROJECTION, PHONE_NUMBER_SELECTION, null, null);
+ mLoader.registerListener(0, this);
+ mLoader.startLoading();
+ }
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
+ if (cursor == null) {
+ LogUtil.i("PhoneNumberInteraction.onLoadComplete", "null cursor");
+ interactionError(InteractionErrorCode.OTHER_ERROR);
+ return;
+ }
+ try {
+ ArrayList<PhoneItem> phoneList = new ArrayList<>();
+ String primaryPhone = null;
+ if (!isSafeToCommitTransactions()) {
+ LogUtil.i("PhoneNumberInteraction.onLoadComplete", "not safe to commit transaction");
+ interactionError(InteractionErrorCode.USER_LEAVING_ACTIVITY);
+ return;
+ }
+ if (cursor.moveToFirst()) {
+ int contactIdColumn = cursor.getColumnIndexOrThrow(Phone.CONTACT_ID);
+ int isSuperPrimaryColumn = cursor.getColumnIndexOrThrow(Phone.IS_SUPER_PRIMARY);
+ int phoneNumberColumn = cursor.getColumnIndexOrThrow(Phone.NUMBER);
+ int phoneIdColumn = cursor.getColumnIndexOrThrow(Phone._ID);
+ int accountTypeColumn = cursor.getColumnIndexOrThrow(RawContacts.ACCOUNT_TYPE);
+ int dataSetColumn = cursor.getColumnIndexOrThrow(RawContacts.DATA_SET);
+ int phoneTypeColumn = cursor.getColumnIndexOrThrow(Phone.TYPE);
+ int phoneLabelColumn = cursor.getColumnIndexOrThrow(Phone.LABEL);
+ int phoneMimeTpeColumn = cursor.getColumnIndexOrThrow(Phone.MIMETYPE);
+ do {
+ if (mContactId == UNKNOWN_CONTACT_ID) {
+ mContactId = cursor.getLong(contactIdColumn);
+ }
+
+ if (cursor.getInt(isSuperPrimaryColumn) != 0) {
+ // Found super primary, call it.
+ primaryPhone = cursor.getString(phoneNumberColumn);
+ }
+
+ PhoneItem item = new PhoneItem();
+ item.id = cursor.getLong(phoneIdColumn);
+ item.phoneNumber = cursor.getString(phoneNumberColumn);
+ item.accountType = cursor.getString(accountTypeColumn);
+ item.dataSet = cursor.getString(dataSetColumn);
+ item.type = cursor.getInt(phoneTypeColumn);
+ item.label = cursor.getString(phoneLabelColumn);
+ item.mimeType = cursor.getString(phoneMimeTpeColumn);
+
+ phoneList.add(item);
+ } while (cursor.moveToNext());
+ } else {
+ interactionError(InteractionErrorCode.CONTACT_NOT_FOUND);
+ return;
+ }
+
+ if (primaryPhone != null) {
+ performAction(primaryPhone);
+ return;
+ }
+
+ Collapser.collapseList(phoneList, mContext);
+ if (phoneList.size() == 0) {
+ interactionError(InteractionErrorCode.CONTACT_HAS_NO_NUMBER);
+ } else if (phoneList.size() == 1) {
+ PhoneItem item = phoneList.get(0);
+ performAction(item.phoneNumber);
+ } else {
+ // There are multiple candidates. Let the user choose one.
+ showDisambiguationDialog(phoneList);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void interactionError(@InteractionErrorCode int interactionErrorCode) {
+ // mContext is really the activity -- see ctor docs.
+ ((InteractionErrorListener) mContext).interactionError(interactionErrorCode);
+ }
+
+ private boolean isSafeToCommitTransactions() {
+ return !(mContext instanceof TransactionSafeActivity)
+ || ((TransactionSafeActivity) mContext).isSafeToCommitTransactions();
+ }
+
+ @VisibleForTesting
+ /* package */ CursorLoader getLoader() {
+ return mLoader;
+ }
+
+ private void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
+ final Activity activity = (Activity) mContext;
+ if (activity.isDestroyed()) {
+ // Check whether the activity is still running
+ return;
+ }
+ try {
+ PhoneDisambiguationDialogFragment.show(
+ activity.getFragmentManager(),
+ phoneList,
+ mInteractionType,
+ mIsVideoCall,
+ mCallSpecificAppData);
+ } catch (IllegalStateException e) {
+ // ignore to be safe. Shouldn't happen because we checked the
+ // activity wasn't destroyed, but to be safe.
+ }
+ }
+
+ /** A model object for capturing a phone number for a given contact. */
+ @VisibleForTesting
+ /* package */ static class PhoneItem implements Parcelable, Collapsible<PhoneItem> {
+
+ public static final Parcelable.Creator<PhoneItem> CREATOR =
+ new Parcelable.Creator<PhoneItem>() {
+ @Override
+ public PhoneItem createFromParcel(Parcel in) {
+ return new PhoneItem(in);
+ }
+
+ @Override
+ public PhoneItem[] newArray(int size) {
+ return new PhoneItem[size];
+ }
+ };
+ long id;
+ String phoneNumber;
+ String accountType;
+ String dataSet;
+ long type;
+ String label;
+ /** {@link Phone#CONTENT_ITEM_TYPE} or {@link SipAddress#CONTENT_ITEM_TYPE}. */
+ String mimeType;
+
+ private PhoneItem() {}
+
+ private PhoneItem(Parcel in) {
+ this.id = in.readLong();
+ this.phoneNumber = in.readString();
+ this.accountType = in.readString();
+ this.dataSet = in.readString();
+ this.type = in.readLong();
+ this.label = in.readString();
+ this.mimeType = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeString(phoneNumber);
+ dest.writeString(accountType);
+ dest.writeString(dataSet);
+ dest.writeLong(type);
+ dest.writeString(label);
+ dest.writeString(mimeType);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void collapseWith(PhoneItem phoneItem) {
+ // Just keep the number and id we already have.
+ }
+
+ @Override
+ public boolean shouldCollapseWith(PhoneItem phoneItem, Context context) {
+ return MoreContactUtils.shouldCollapse(
+ Phone.CONTENT_ITEM_TYPE, phoneNumber, Phone.CONTENT_ITEM_TYPE, phoneItem.phoneNumber);
+ }
+
+ @Override
+ public String toString() {
+ return phoneNumber;
+ }
+ }
+
+ /** A list adapter that populates the list of contact's phone numbers. */
+ private static class PhoneItemAdapter extends ArrayAdapter<PhoneItem> {
+
+ private final int mInteractionType;
+
+ PhoneItemAdapter(Context context, List<PhoneItem> list, int interactionType) {
+ super(context, R.layout.phone_disambig_item, android.R.id.text2, list);
+ mInteractionType = interactionType;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view = super.getView(position, convertView, parent);
+
+ final PhoneItem item = getItem(position);
+ Assert.isNotNull(item, "Null item at position: %d", position);
+ final TextView typeView = (TextView) view.findViewById(android.R.id.text1);
+ CharSequence value =
+ ContactDisplayUtils.getLabelForCallOrSms(
+ (int) item.type, item.label, mInteractionType, getContext());
+
+ typeView.setText(value);
+ return view;
+ }
+ }
+
+ /**
+ * {@link DialogFragment} used for displaying a dialog with a list of phone numbers of which one
+ * will be chosen to make a call or initiate an sms message.
+ *
+ * <p>It is recommended to use {@link #startInteractionForPhoneCall(TransactionSafeActivity, Uri,
+ * boolean, int)} instead of directly using this class, as those methods handle one or multiple
+ * data cases appropriately.
+ *
+ * <p>This fragment may only be attached to activities which implement {@link
+ * DisambigDialogDismissedListener}.
+ */
+ @SuppressWarnings("WeakerAccess") // Made public to let the system reach this class
+ public static class PhoneDisambiguationDialogFragment extends DialogFragment
+ implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+
+ private static final String ARG_PHONE_LIST = "phoneList";
+ private static final String ARG_INTERACTION_TYPE = "interactionType";
+ private static final String ARG_IS_VIDEO_CALL = "is_video_call";
+
+ private int mInteractionType;
+ private ListAdapter mPhonesAdapter;
+ private List<PhoneItem> mPhoneList;
+ private CallSpecificAppData mCallSpecificAppData;
+ private boolean mIsVideoCall;
+
+ public PhoneDisambiguationDialogFragment() {
+ super();
+ }
+
+ public static void show(
+ FragmentManager fragmentManager,
+ ArrayList<PhoneItem> phoneList,
+ int interactionType,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
+ bundle.putInt(ARG_INTERACTION_TYPE, interactionType);
+ bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
+ CallIntentParser.putCallSpecificAppData(bundle, callSpecificAppData);
+ fragment.setArguments(bundle);
+ fragment.show(fragmentManager, TAG);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+ Assert.checkState(activity instanceof DisambigDialogDismissedListener);
+
+ mPhoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
+ mInteractionType = getArguments().getInt(ARG_INTERACTION_TYPE);
+ mIsVideoCall = getArguments().getBoolean(ARG_IS_VIDEO_CALL);
+ mCallSpecificAppData = CallIntentParser.getCallSpecificAppData(getArguments());
+
+ mPhonesAdapter = new PhoneItemAdapter(activity, mPhoneList, mInteractionType);
+ final LayoutInflater inflater = activity.getLayoutInflater();
+ @SuppressLint("InflateParams") // Allowed since dialog view is not available yet
+ final View setPrimaryView = inflater.inflate(R.layout.set_primary_checkbox, null);
+ return new AlertDialog.Builder(activity)
+ .setAdapter(mPhonesAdapter, this)
+ .setTitle(
+ mInteractionType == ContactDisplayUtils.INTERACTION_SMS
+ ? R.string.sms_disambig_title
+ : R.string.call_disambig_title)
+ .setView(setPrimaryView)
+ .create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+ final AlertDialog alertDialog = (AlertDialog) dialog;
+ if (mPhoneList.size() > which && which >= 0) {
+ final PhoneItem phoneItem = mPhoneList.get(which);
+ final CheckBox checkBox = (CheckBox) alertDialog.findViewById(R.id.setPrimary);
+ if (checkBox.isChecked()) {
+ // Request to mark the data as primary in the background.
+ final Intent serviceIntent =
+ ContactUpdateService.createSetSuperPrimaryIntent(activity, phoneItem.id);
+ activity.startService(serviceIntent);
+ }
+
+ PhoneNumberInteraction.performAction(
+ activity, phoneItem.phoneNumber, mInteractionType, mIsVideoCall, mCallSpecificAppData);
+ } else {
+ dialog.dismiss();
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ super.onDismiss(dialogInterface);
+ Activity activity = getActivity();
+ if (activity != null) {
+ ((DisambigDialogDismissedListener) activity).onDisambigDialogDismissed();
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java b/java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java
new file mode 100644
index 0000000..68b011a
--- /dev/null
+++ b/java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 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.interactions;
+
+import static android.Manifest.permission.READ_CONTACTS;
+import static android.Manifest.permission.WRITE_CONTACTS;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.PinnedPositions;
+import android.text.TextUtils;
+import com.android.dialer.util.PermissionsUtil;
+
+/**
+ * This broadcast receiver is used to listen to outgoing calls and undemote formerly demoted
+ * contacts if a phone call is made to a phone number belonging to that contact.
+ *
+ * <p>NOTE This doesn't work for corp contacts.
+ */
+public class UndemoteOutgoingCallReceiver extends BroadcastReceiver {
+
+ private static final long NO_CONTACT_FOUND = -1;
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (!PermissionsUtil.hasPermission(context, READ_CONTACTS)
+ || !PermissionsUtil.hasPermission(context, WRITE_CONTACTS)) {
+ return;
+ }
+ if (intent != null && Intent.ACTION_NEW_OUTGOING_CALL.equals(intent.getAction())) {
+ final String number = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
+ if (TextUtils.isEmpty(number)) {
+ return;
+ }
+ new Thread() {
+ @Override
+ public void run() {
+ final long id = getContactIdFromPhoneNumber(context, number);
+ if (id != NO_CONTACT_FOUND) {
+ undemoteContactWithId(context, id);
+ }
+ }
+ }.start();
+ }
+ }
+
+ private void undemoteContactWithId(Context context, long id) {
+ // If the contact is not demoted, this will not do anything. Otherwise, it will
+ // restore it to an unpinned position. If it was a frequently called contact, it will
+ // show up once again show up on the favorites screen.
+ if (PermissionsUtil.hasPermission(context, WRITE_CONTACTS)) {
+ try {
+ PinnedPositions.undemote(context.getContentResolver(), id);
+ } catch (SecurityException e) {
+ // Just in case
+ }
+ }
+ }
+
+ private long getContactIdFromPhoneNumber(Context context, String number) {
+ if (!PermissionsUtil.hasPermission(context, READ_CONTACTS)) {
+ return NO_CONTACT_FOUND;
+ }
+ final Uri contactUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
+ final Cursor cursor;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(contactUri, new String[] {PhoneLookup._ID}, null, null, null);
+ } catch (SecurityException e) {
+ // Just in case
+ return NO_CONTACT_FOUND;
+ }
+ if (cursor == null) {
+ return NO_CONTACT_FOUND;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ final long id = cursor.getLong(0);
+ return id;
+ } else {
+ return NO_CONTACT_FOUND;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+}
diff --git a/java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml b/java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml
new file mode 100644
index 0000000..879ea0e
--- /dev/null
+++ b/java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.contacts.common.widget.ActivityTouchLinearLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="30dip"
+ android:paddingEnd="30dip"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/text1"
+ android:textStyle="bold"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <!-- Phone number should be displayed ltr -->
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-4dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textDirection="ltr"/>
+
+</view>
diff --git a/java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml b/java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml
new file mode 100644
index 0000000..62ef4b7
--- /dev/null
+++ b/java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="14dip"
+ android:paddingEnd="15dip"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/setPrimary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:text="@string/make_primary"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/interactions/res/values/strings.xml b/java/com/android/dialer/interactions/res/values/strings.xml
new file mode 100644
index 0000000..eea8795
--- /dev/null
+++ b/java/com/android/dialer/interactions/res/values/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <!-- Title for the sms disambiguation dialog -->
+ <string name="sms_disambig_title">Choose number</string>
+
+ <!-- Title for the call disambiguation dialog -->
+ <string name="call_disambig_title">Choose number</string>
+
+ <!-- Message next to disambiguation dialog check box -->
+ <string name="make_primary">Remember this choice</string>
+
+</resources>
diff --git a/java/com/android/dialer/logging/Logger.java b/java/com/android/dialer/logging/Logger.java
new file mode 100644
index 0000000..207c35d
--- /dev/null
+++ b/java/com/android/dialer/logging/Logger.java
@@ -0,0 +1,49 @@
+/*
+ * 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.logging;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Single entry point for all logging/analytics-related work for all user interactions. */
+public class Logger {
+
+ private static LoggingBindings loggingBindings;
+
+ private Logger() {}
+
+ public static LoggingBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (loggingBindings != null) {
+ return loggingBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof LoggingBindingsFactory) {
+ loggingBindings = ((LoggingBindingsFactory) application).newLoggingBindings();
+ }
+
+ if (loggingBindings == null) {
+ loggingBindings = new LoggingBindingsStub();
+ }
+ return loggingBindings;
+ }
+
+ public static void setForTesting(LoggingBindings loggingBindings) {
+ Logger.loggingBindings = loggingBindings;
+ }
+}
diff --git a/java/com/android/dialer/logging/LoggingBindings.java b/java/com/android/dialer/logging/LoggingBindings.java
new file mode 100644
index 0000000..cf921c3
--- /dev/null
+++ b/java/com/android/dialer/logging/LoggingBindings.java
@@ -0,0 +1,59 @@
+/*
+ * 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.logging;
+
+import android.app.Activity;
+
+/** Allows the container application to gather analytics. */
+public interface LoggingBindings {
+
+ /**
+ * Logs an impression for a general dialer event that's not associated with a specific call.
+ *
+ * @param dialerImpression an integer representing what event occurred.
+ * @see com.android.dialer.logging.nano.DialerImpression
+ */
+ void logImpression(int dialerImpression);
+
+ /**
+ * Logs an impression for a general dialer event that's associated with a specific call.
+ *
+ * @param dialerImpression an integer representing what event occurred.
+ * @param callId unique ID of the call.
+ * @param callStartTimeMillis the absolute time when the call started.
+ * @see com.android.dialer.logging.nano.DialerImpression
+ */
+ void logCallImpression(int dialerImpression, String callId, long callStartTimeMillis);
+
+ /**
+ * Logs an interaction that occurred.
+ *
+ * @param interaction an integer representing what interaction occurred.
+ * @see com.android.dialer.logging.nano.InteractionEvent
+ */
+ void logInteraction(int interaction);
+
+ /**
+ * Logs an event indicating that a screen was displayed.
+ *
+ * @param screenEvent an integer representing the displayed screen.
+ * @param activity Parent activity of the displayed screen.
+ * @see com.android.dialer.logging.nano.ScreenEvent
+ */
+ void logScreenView(int screenEvent, Activity activity);
+
+ /** Logs a hit event to the analytics server. */
+ void sendHitEventAnalytics(String category, String action, String label, long value);
+}
diff --git a/java/com/android/dialer/logging/LoggingBindingsFactory.java b/java/com/android/dialer/logging/LoggingBindingsFactory.java
new file mode 100644
index 0000000..0722cf4
--- /dev/null
+++ b/java/com/android/dialer/logging/LoggingBindingsFactory.java
@@ -0,0 +1,24 @@
+/*
+ * 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.logging;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows this module to get
+ * references to the LoggingBindings.
+ */
+public interface LoggingBindingsFactory {
+
+ LoggingBindings newLoggingBindings();
+}
diff --git a/java/com/android/dialer/logging/LoggingBindingsStub.java b/java/com/android/dialer/logging/LoggingBindingsStub.java
new file mode 100644
index 0000000..89c56eb
--- /dev/null
+++ b/java/com/android/dialer/logging/LoggingBindingsStub.java
@@ -0,0 +1,36 @@
+/*
+ * 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.logging;
+
+import android.app.Activity;
+
+/** Default implementation for logging bindings. */
+public class LoggingBindingsStub implements LoggingBindings {
+
+ @Override
+ public void logImpression(int dialerImpression) {}
+
+ @Override
+ public void logCallImpression(int dialerImpression, String callId, long callStartTimeMillis) {}
+
+ @Override
+ public void logInteraction(int interaction) {}
+
+ @Override
+ public void logScreenView(int screenEvent, Activity activity) {}
+
+ @Override
+ public void sendHitEventAnalytics(String category, String action, String label, long value) {}
+}
diff --git a/java/com/android/dialer/logging/nano/ContactLookupResult.java b/java/com/android/dialer/logging/nano/ContactLookupResult.java
new file mode 100644
index 0000000..8960560
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ContactLookupResult.java
@@ -0,0 +1,91 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class ContactLookupResult extends
+ com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_LOOKUP_RESULT_TYPE = 0;
+ public static final int NOT_FOUND = 1;
+ public static final int LOCAL_CONTACT = 2;
+ public static final int LOCAL_CACHE = 3;
+ public static final int REMOTE = 4;
+ public static final int EMERGENCY = 5;
+ public static final int VOICEMAIL = 6;
+ }
+
+ private static volatile ContactLookupResult[] _emptyArray;
+ public static ContactLookupResult[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ContactLookupResult[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ContactLookupResult)
+
+ public ContactLookupResult() {
+ clear();
+ }
+
+ public ContactLookupResult clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ContactLookupResult mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ContactLookupResult parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ContactLookupResult(), data);
+ }
+
+ public static ContactLookupResult parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ContactLookupResult().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/ContactSource.java b/java/com/android/dialer/logging/nano/ContactSource.java
new file mode 100644
index 0000000..35d8b8c
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ContactSource.java
@@ -0,0 +1,90 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class ContactSource extends
+ com.google.protobuf.nano.ExtendableMessageNano<ContactSource> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_SOURCE_TYPE = 0;
+ public static final int SOURCE_TYPE_DIRECTORY = 1;
+ public static final int SOURCE_TYPE_EXTENDED = 2;
+ public static final int SOURCE_TYPE_PLACES = 3;
+ public static final int SOURCE_TYPE_PROFILE = 4;
+ public static final int SOURCE_TYPE_CNAP = 5;
+ }
+
+ private static volatile ContactSource[] _emptyArray;
+ public static ContactSource[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ContactSource[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ContactSource)
+
+ public ContactSource() {
+ clear();
+ }
+
+ public ContactSource clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ContactSource mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ContactSource parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ContactSource(), data);
+ }
+
+ public static ContactSource parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ContactSource().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/DialerImpression.java b/java/com/android/dialer/logging/nano/DialerImpression.java
new file mode 100644
index 0000000..6bb5675
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/DialerImpression.java
@@ -0,0 +1,178 @@
+/*
+ * 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.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class DialerImpression extends
+ com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_AOSP_EVENT_TYPE = 1000;
+ public static final int APP_LAUNCHED = 1001;
+ public static final int IN_CALL_SCREEN_TURN_ON_SPEAKERPHONE = 1002;
+ public static final int IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE = 1003;
+ public static final int CALL_LOG_BLOCK_REPORT_SPAM = 1004;
+ public static final int CALL_LOG_BLOCK_NUMBER = 1005;
+ public static final int CALL_LOG_UNBLOCK_NUMBER = 1006;
+ public static final int CALL_LOG_REPORT_AS_NOT_SPAM = 1007;
+ public static final int DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM = 1008;
+ public static final int REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER = 1009;
+ public static final int DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER = 1010;
+ public static final int REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011;
+ public static final int USER_ACTION_BLOCKED_NUMBER = 1012;
+ public static final int USER_ACTION_UNBLOCKED_NUMBER = 1013;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER = 1014;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_SHOW_SPAM_DIALOG = 1015;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG = 1016;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS = 1019;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM = 1020;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = 1021;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM = 1022;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG = 1024;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG = 1025;
+ public static final int SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_SPAM = 1026;
+ public static final int SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_NOT_SPAM = 1027;
+ public static final int USER_PARTICIPATED_IN_A_CALL = 1028;
+ public static final int INCOMING_SPAM_CALL = 1029;
+ public static final int INCOMING_NON_SPAM_CALL = 1030;
+ public static final int SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE = 1041;
+ public static final int SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE = 1042;
+ public static final int NON_SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE = 1043;
+ public static final int NON_SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE = 1044;
+ public static final int VOICEMAIL_ALERT_SET_PIN_SHOWN = 1045;
+ public static final int VOICEMAIL_ALERT_SET_PIN_CLICKED = 1046;
+ public static final int USER_DID_NOT_PARTICIPATE_IN_CALL = 1047;
+ public static final int USER_DELETED_CALL_LOG_ITEM = 1048;
+ public static final int CALL_LOG_SEND_MESSAGE = 1049;
+ public static final int CALL_LOG_ADD_TO_CONTACT = 1050;
+ public static final int CALL_LOG_CREATE_NEW_CONTACT = 1051;
+ public static final int VOICEMAIL_DELETE_ENTRY = 1052;
+ public static final int VOICEMAIL_EXPAND_ENTRY = 1053;
+ public static final int VOICEMAIL_PLAY_AUDIO_DIRECTLY = 1054;
+ public static final int VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY = 1055;
+ public static final int REJECT_INCOMING_CALL_FROM_NOTIFICATION = 1056;
+ public static final int REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN = 1057;
+ public static final int CALL_LOG_CONTEXT_MENU_BLOCK_REPORT_SPAM = 1058;
+ public static final int CALL_LOG_CONTEXT_MENU_BLOCK_NUMBER = 1059;
+ public static final int CALL_LOG_CONTEXT_MENU_UNBLOCK_NUMBER = 1060;
+ public static final int CALL_LOG_CONTEXT_MENU_REPORT_AS_NOT_SPAM = 1061;
+ public static final int NEW_CONTACT_OVERFLOW = 1062;
+ public static final int NEW_CONTACT_FAB = 1063;
+ public static final int VOICEMAIL_VVM3_TOS_SHOWN = 1064;
+ public static final int VOICEMAIL_VVM3_TOS_ACCEPTED = 1065;
+ public static final int VOICEMAIL_VVM3_TOS_DECLINED = 1066;
+ public static final int VOICEMAIL_VVM3_TOS_DECLINE_CLICKED = 1067;
+ public static final int VOICEMAIL_VVM3_TOS_DECLINE_CHANGE_PIN_SHOWN = 1068;
+ public static final int STORAGE_PERMISSION_DISPLAYED = 1069;
+ public static final int CAMERA_PERMISSION_DISPLAYED = 1074;
+ public static final int STORAGE_PERMISSION_REQUESTED = 1070;
+ public static final int CAMERA_PERMISSION_REQUESTED = 1075;
+ public static final int STORAGE_PERMISSION_SETTINGS = 1071;
+ public static final int CAMERA_PERMISSION_SETTINGS = 1076;
+ public static final int STORAGE_PERMISSION_GRANTED = 1072;
+ public static final int CAMERA_PERMISSION_GRANTED = 1077;
+ public static final int STORAGE_PERMISSION_DENIED = 1073;
+ public static final int CAMERA_PERMISSION_DENIED = 1078;
+ public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY = 1079;
+ public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION = 1080;
+ public static final int BACKUP_ON_BACKUP = 1081;
+ public static final int BACKUP_ON_FULL_BACKUP = 1082;
+ public static final int BACKUP_ON_BACKUP_DISABLED = 1083;
+ public static final int BACKUP_VOICEMAIL_BACKED_UP = 1084;
+ public static final int BACKUP_FULL_BACKED_UP = 1085;
+ public static final int BACKUP_ON_BACKUP_JSON_EXCEPTION = 1086;
+ public static final int BACKUP_ON_QUOTA_EXCEEDED = 1087;
+ public static final int BACKUP_ON_RESTORE = 1088;
+ public static final int BACKUP_RESTORED_FILE = 1089;
+ public static final int BACKUP_RESTORED_VOICEMAIL = 1090;
+ public static final int BACKUP_ON_RESTORE_FINISHED = 1091;
+ public static final int BACKUP_ON_RESTORE_DISABLED = 1092;
+ public static final int BACKUP_ON_RESTORE_JSON_EXCEPTION = 1093;
+ public static final int BACKUP_ON_RESTORE_IO_EXCEPTION = 1094;
+ public static final int BACKUP_MAX_VM_BACKUP_REACHED = 1095;
+ public static final int EVENT_ANSWER_HINT_ACTIVATED = 1096;
+ public static final int EVENT_ANSWER_HINT_DEACTIVATED = 1097;
+ public static final int VVM_TAB_VISIBLE = 1098;
+ public static final int VVM_SHARE_VISIBLE = 1099;
+ public static final int VVM_SHARE_PRESSED = 1100;
+ public static final int OUTGOING_VIDEO_CALL = 1101;
+ public static final int INCOMING_VIDEO_CALL = 1102;
+ public static final int USER_PARTICIPATED_IN_A_VIDEO_CALL = 1103;
+ public static final int BACKUP_ON_RESTORE_VM_DUPLICATE_NOT_RESTORING = 1104;
+ public static final int CALL_LOG_SHARE_AND_CALL = 1105;
+ public static final int CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL = 1106;
+ public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY = 1107;
+ }
+
+ private static volatile DialerImpression[] _emptyArray;
+ public static DialerImpression[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new DialerImpression[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.DialerImpression)
+
+ public DialerImpression() {
+ clear();
+ }
+
+ public DialerImpression clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public DialerImpression mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static DialerImpression parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new DialerImpression(), data);
+ }
+
+ public static DialerImpression parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new DialerImpression().mergeFrom(input);
+ }
+}
+
diff --git a/java/com/android/dialer/logging/nano/InteractionEvent.java b/java/com/android/dialer/logging/nano/InteractionEvent.java
new file mode 100644
index 0000000..8d9430b
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/InteractionEvent.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class InteractionEvent
+ extends com.google.protobuf.nano.ExtendableMessageNano<InteractionEvent> {
+
+ // enum Type
+ /** This file is autogenerated, but javadoc required. */
+ public interface Type {
+ public static final int UNKNOWN = 0;
+ public static final int CALL_BLOCKED = 15;
+ public static final int BLOCK_NUMBER_CALL_LOG = 16;
+ public static final int BLOCK_NUMBER_CALL_DETAIL = 17;
+ public static final int BLOCK_NUMBER_MANAGEMENT_SCREEN = 18;
+ public static final int UNBLOCK_NUMBER_CALL_LOG = 19;
+ public static final int UNBLOCK_NUMBER_CALL_DETAIL = 20;
+ public static final int UNBLOCK_NUMBER_MANAGEMENT_SCREEN = 21;
+ public static final int IMPORT_SEND_TO_VOICEMAIL = 22;
+ public static final int UNDO_BLOCK_NUMBER = 23;
+ public static final int UNDO_UNBLOCK_NUMBER = 24;
+ }
+
+ private static volatile InteractionEvent[] _emptyArray;
+ public static InteractionEvent[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new InteractionEvent[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.InteractionEvent)
+
+ public InteractionEvent() {
+ clear();
+ }
+
+ public InteractionEvent clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public InteractionEvent mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static InteractionEvent parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new InteractionEvent(), data);
+ }
+
+ public static InteractionEvent parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new InteractionEvent().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/ReportingLocation.java b/java/com/android/dialer/logging/nano/ReportingLocation.java
new file mode 100644
index 0000000..1f05ce4
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ReportingLocation.java
@@ -0,0 +1,87 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class ReportingLocation extends
+ com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_REPORTING_LOCATION = 0;
+ public static final int CALL_LOG_HISTORY = 1;
+ public static final int FEEDBACK_PROMPT = 2;
+ }
+
+ private static volatile ReportingLocation[] _emptyArray;
+ public static ReportingLocation[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ReportingLocation[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ReportingLocation)
+
+ public ReportingLocation() {
+ clear();
+ }
+
+ public ReportingLocation clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ReportingLocation mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ReportingLocation parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ReportingLocation(), data);
+ }
+
+ public static ReportingLocation parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ReportingLocation().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/ScreenEvent.java b/java/com/android/dialer/logging/nano/ScreenEvent.java
new file mode 100644
index 0000000..be4e5eb
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ScreenEvent.java
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class ScreenEvent extends com.google.protobuf.nano.ExtendableMessageNano<ScreenEvent> {
+
+ // enum Type
+ /** This file is autogenerated, but javadoc required. */
+ public interface Type {
+ public static final int UNKNOWN = 0;
+ public static final int DIALPAD = 1;
+ public static final int SPEED_DIAL = 2;
+ public static final int CALL_LOG = 3;
+ public static final int VOICEMAIL_LOG = 4;
+ public static final int ALL_CONTACTS = 5;
+ public static final int REGULAR_SEARCH = 6;
+ public static final int SMART_DIAL_SEARCH = 7;
+ public static final int CALL_LOG_FILTER = 8;
+ public static final int SETTINGS = 9;
+ public static final int IMPORT_EXPORT_CONTACTS = 10;
+ public static final int CLEAR_FREQUENTS = 11;
+ public static final int SEND_FEEDBACK = 12;
+ public static final int INCALL = 13;
+ public static final int INCOMING_CALL = 14;
+ public static final int CONFERENCE_MANAGEMENT = 15;
+ public static final int INCALL_DIALPAD = 16;
+ public static final int CALL_LOG_CONTEXT_MENU = 17;
+ public static final int BLOCKED_NUMBER_MANAGEMENT = 18;
+ public static final int BLOCKED_NUMBER_ADD_NUMBER = 19;
+ public static final int CALL_DETAILS = 20;
+ }
+
+ private static volatile ScreenEvent[] _emptyArray;
+ public static ScreenEvent[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ScreenEvent[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ScreenEvent)
+
+ public ScreenEvent() {
+ clear();
+ }
+
+ public ScreenEvent clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ScreenEvent mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ScreenEvent parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ScreenEvent(), data);
+ }
+
+ public static ScreenEvent parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ScreenEvent().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java b/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
new file mode 100644
index 0000000..cc68150
--- /dev/null
+++ b/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
@@ -0,0 +1,165 @@
+/*
+ * 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.multimedia;
+
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_MultimediaData extends MultimediaData {
+
+ private final String subject;
+ private final Location location;
+ private final Uri imageUri;
+ private final String imageContentType;
+ private final boolean important;
+
+ private AutoValue_MultimediaData(
+ @Nullable String subject,
+ @Nullable Location location,
+ @Nullable Uri imageUri,
+ @Nullable String imageContentType,
+ boolean important) {
+ this.subject = subject;
+ this.location = location;
+ this.imageUri = imageUri;
+ this.imageContentType = imageContentType;
+ this.important = important;
+ }
+
+ @Nullable
+ @Override
+ public String getSubject() {
+ return subject;
+ }
+
+ @Nullable
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Nullable
+ @Override
+ public Uri getImageUri() {
+ return imageUri;
+ }
+
+ @Nullable
+ @Override
+ public String getImageContentType() {
+ return imageContentType;
+ }
+
+ @Override
+ public boolean isImportant() {
+ return important;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof MultimediaData) {
+ MultimediaData that = (MultimediaData) o;
+ return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
+ && ((this.location == null) ? (that.getLocation() == null) : this.location.equals(that.getLocation()))
+ && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
+ && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()))
+ && (this.important == that.isImportant());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (subject == null) ? 0 : this.subject.hashCode();
+ h *= 1000003;
+ h ^= (location == null) ? 0 : this.location.hashCode();
+ h *= 1000003;
+ h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
+ h *= 1000003;
+ h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
+ h *= 1000003;
+ h ^= this.important ? 1231 : 1237;
+ return h;
+ }
+
+ static final class Builder extends MultimediaData.Builder {
+ private String subject;
+ private Location location;
+ private Uri imageUri;
+ private String imageContentType;
+ private Boolean important;
+ Builder() {
+ }
+ private Builder(MultimediaData source) {
+ this.subject = source.getSubject();
+ this.location = source.getLocation();
+ this.imageUri = source.getImageUri();
+ this.imageContentType = source.getImageContentType();
+ this.important = source.isImportant();
+ }
+ @Override
+ public MultimediaData.Builder setSubject(@Nullable String subject) {
+ this.subject = subject;
+ return this;
+ }
+ @Override
+ public MultimediaData.Builder setLocation(@Nullable Location location) {
+ this.location = location;
+ return this;
+ }
+ @Override
+ MultimediaData.Builder setImageUri(@Nullable Uri imageUri) {
+ this.imageUri = imageUri;
+ return this;
+ }
+ @Override
+ MultimediaData.Builder setImageContentType(@Nullable String imageContentType) {
+ this.imageContentType = imageContentType;
+ return this;
+ }
+ @Override
+ public MultimediaData.Builder setImportant(boolean important) {
+ this.important = important;
+ return this;
+ }
+ @Override
+ public MultimediaData build() {
+ String missing = "";
+ if (this.important == null) {
+ missing += " important";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_MultimediaData(
+ this.subject,
+ this.location,
+ this.imageUri,
+ this.imageContentType,
+ this.important);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/multimedia/MultimediaData.java b/java/com/android/dialer/multimedia/MultimediaData.java
new file mode 100644
index 0000000..ebd41a9
--- /dev/null
+++ b/java/com/android/dialer/multimedia/MultimediaData.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.multimedia;
+
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+
+
+/** Holds the data associated with an enriched call session. */
+
+public abstract class MultimediaData {
+
+ public static final MultimediaData EMPTY = builder().build();
+
+ @NonNull
+ public static Builder builder() {
+ return new AutoValue_MultimediaData.Builder().setImportant(false);
+ }
+
+ /** Returns the call composer subject if set, or null if this isn't a call composer session. */
+ @Nullable
+ public abstract String getSubject();
+
+ /** Returns the call composer location if set, or null if this isn't a call composer session. */
+ @Nullable
+ public abstract Location getLocation();
+
+ /** Returns {@code true} if this session contains image data. */
+ public boolean hasImageData() {
+ // imageUri and content are always either both null or nonnull
+ return getImageUri() != null && getImageContentType() != null;
+ }
+
+ /** Returns the call composer photo if set, or null if this isn't a call composer session. */
+ @Nullable
+ public abstract Uri getImageUri();
+
+ /**
+ * Returns the content type of the image, either image/png or image/jpeg, if set, or null if this
+ * isn't a call composer session.
+ */
+ @Nullable
+ public abstract String getImageContentType();
+
+ /** Returns {@code true} if this is a call composer session that's marked as important. */
+ public abstract boolean isImportant();
+
+ /** Returns the string form of this MultimediaData with no PII. */
+ @Override
+ public String toString() {
+ return String.format(
+ "MultimediaData{subject: %s, location: %s, imageUrl: %s, imageContentType: %s, "
+ + "important: %b}",
+ LogUtil.sanitizePii(getSubject()),
+ LogUtil.sanitizePii(getLocation()),
+ LogUtil.sanitizePii(getImageUri()),
+ getImageContentType(),
+ isImportant());
+ }
+
+ /** Creates instances of {@link MultimediaData}. */
+
+ public abstract static class Builder {
+
+ public abstract Builder setSubject(@NonNull String subject);
+
+ public abstract Builder setLocation(@NonNull Location location);
+
+ public Builder setImage(@NonNull Uri image, @NonNull String imageContentType) {
+ setImageUri(image);
+ setImageContentType(imageContentType);
+ return this;
+ }
+
+ abstract Builder setImageUri(@NonNull Uri image);
+
+ abstract Builder setImageContentType(@NonNull String imageContentType);
+
+ public abstract Builder setImportant(boolean isImportant);
+
+ public abstract MultimediaData build();
+ }
+}
diff --git a/java/com/android/dialer/p13n/inference/P13nRanking.java b/java/com/android/dialer/p13n/inference/P13nRanking.java
new file mode 100644
index 0000000..6bfc035
--- /dev/null
+++ b/java/com/android/dialer/p13n/inference/P13nRanking.java
@@ -0,0 +1,75 @@
+/*
+ * 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.p13n.inference;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.p13n.inference.protocol.P13nRanker;
+import com.android.dialer.p13n.inference.protocol.P13nRankerFactory;
+import java.util.List;
+
+/** Single entry point for all personalized ranking. */
+public final class P13nRanking {
+
+ private static P13nRanker ranker;
+
+ private P13nRanking() {}
+
+ @MainThread
+ @NonNull
+ public static P13nRanker get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ Assert.isMainThread();
+ if (ranker != null) {
+ return ranker;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof P13nRankerFactory) {
+ ranker = ((P13nRankerFactory) application).newP13nRanker();
+ }
+
+ if (ranker == null) {
+ ranker =
+ new P13nRanker() {
+ @Override
+ public void refresh(@Nullable P13nRefreshCompleteListener listener) {}
+
+ @Override
+ public List<String> rankList(List<String> phoneNumbers) {
+ return phoneNumbers;
+ }
+
+ @NonNull
+ @Override
+ public Cursor rankCursor(
+ @NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex) {
+ return phoneQueryResults;
+ }
+ };
+ }
+ return ranker;
+ }
+
+ public static void setForTesting(@NonNull P13nRanker ranker) {
+ P13nRanking.ranker = ranker;
+ }
+}
diff --git a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
new file mode 100644
index 0000000..9a859a6
--- /dev/null
+++ b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
@@ -0,0 +1,75 @@
+/*
+ * 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.p13n.inference.protocol;
+
+import android.database.Cursor;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.util.List;
+
+/** Provides personalized ranking of outgoing call targets. */
+public interface P13nRanker {
+
+ /**
+ * Re-orders a list of phone numbers according to likelihood they will be the next outgoing call.
+ *
+ * @param phoneNumbers the list of candidate numbers to call (may be in contacts list or not)
+ */
+ @NonNull
+ @MainThread
+ List<String> rankList(@NonNull List<String> phoneNumbers);
+
+ /**
+ * Re-orders a retrieved contact list according to likelihood they will be the next outgoing call.
+ *
+ * <p>A new cursor with reordered data is returned; the input cursor is unmodified except for its
+ * position. If the order is unchanged, this method may return a reference to the unmodified input
+ * cursor directly. The order would be unchanged if the ranking cache is not yet ready, or if the
+ * input cursor is closed or invalid, or if any other error occurs in the ranking process.
+ *
+ * @param phoneQueryResults cursor of results of a Dialer search query
+ * @param phoneNumberColumnIndex column index of the phone number in the cursor data
+ * @return new cursor of data reordered by ranking (or reference to input cursor if order
+ * unchanged)
+ */
+ @NonNull
+ @MainThread
+ Cursor rankCursor(@NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex);
+
+ /**
+ * Refreshes ranking cache (pulls fresh contextual features, pre-caches inference results, etc.).
+ *
+ * <p>Asynchronously runs in background as the process might take a few seconds, notifying a
+ * listener upon completion; meanwhile, any calls to {@link #rankList} will simply return the
+ * input in same order.
+ *
+ * @param listener callback for when ranking refresh has completed; null value skips notification.
+ */
+ @MainThread
+ void refresh(@Nullable P13nRefreshCompleteListener listener);
+
+ /**
+ * Callback class for when ranking refresh has completed.
+ *
+ * <p>Primary use is to notify {@link com.android.dialer.app.DialtactsActivity} that the ranking
+ * functions {@link #rankList} and {@link #rankCursor(Cursor, int)} will now give useful results.
+ */
+ interface P13nRefreshCompleteListener {
+
+ /** Callback for when ranking refresh has completed. */
+ void onP13nRefreshComplete();
+ }
+}
diff --git a/java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java b/java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java
new file mode 100644
index 0000000..7038cf4
--- /dev/null
+++ b/java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.p13n.inference.protocol;
+
+import android.support.annotation.Nullable;
+
+/**
+ * This interface should be implemented by the Application subclass. It allows this module to get
+ * references to the {@link P13nRanker}.
+ */
+public interface P13nRankerFactory {
+ @Nullable
+ P13nRanker newP13nRanker();
+}
diff --git a/java/com/android/dialer/p13n/logging/P13nLogger.java b/java/com/android/dialer/p13n/logging/P13nLogger.java
new file mode 100644
index 0000000..069a293
--- /dev/null
+++ b/java/com/android/dialer/p13n/logging/P13nLogger.java
@@ -0,0 +1,35 @@
+/*
+ * 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.p13n.logging;
+
+import com.android.contacts.common.list.PhoneNumberListAdapter;
+
+/** Allows logging of data for personalization. */
+public interface P13nLogger {
+
+ /**
+ * Logs a search query (text or digits) entered by user.
+ *
+ * @param query search text (or digits) entered by user
+ * @param adapter list adapter providing access to contacts matching search query
+ */
+ void onSearchQuery(String query, PhoneNumberListAdapter adapter);
+
+ /**
+ * Resets logging session (clears searches, re-initializes app entry timestamp, etc.) Should be
+ * called when Dialer app is resumed.
+ */
+ void reset();
+}
diff --git a/java/com/android/dialer/p13n/logging/P13nLoggerFactory.java b/java/com/android/dialer/p13n/logging/P13nLoggerFactory.java
new file mode 100644
index 0000000..7350e99
--- /dev/null
+++ b/java/com/android/dialer/p13n/logging/P13nLoggerFactory.java
@@ -0,0 +1,29 @@
+/*
+ * 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.p13n.logging;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * This interface should be implemented by the Application subclass. It allows this module to get
+ * references to the P13nLogger.
+ */
+public interface P13nLoggerFactory {
+
+ @Nullable
+ P13nLogger newP13nLogger(@NonNull Context context);
+}
diff --git a/java/com/android/dialer/p13n/logging/P13nLogging.java b/java/com/android/dialer/p13n/logging/P13nLogging.java
new file mode 100644
index 0000000..21b9725
--- /dev/null
+++ b/java/com/android/dialer/p13n/logging/P13nLogging.java
@@ -0,0 +1,60 @@
+/*
+ * 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.p13n.logging;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.android.contacts.common.list.PhoneNumberListAdapter;
+import com.android.dialer.common.Assert;
+
+/** Single entry point for all logging for personalization. */
+public final class P13nLogging {
+
+ private static P13nLogger logger;
+
+ private P13nLogging() {}
+
+ @NonNull
+ public static P13nLogger get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ Assert.isMainThread();
+ if (logger != null) {
+ return logger;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof P13nLoggerFactory) {
+ logger = ((P13nLoggerFactory) application).newP13nLogger(context);
+ }
+
+ if (logger == null) {
+ logger =
+ new P13nLogger() {
+ @Override
+ public void onSearchQuery(String query, PhoneNumberListAdapter adapter) {}
+
+ @Override
+ public void reset() {}
+ };
+ }
+ return logger;
+ }
+
+ public static void setForTesting(@NonNull P13nLogger logger) {
+ P13nLogging.logger = logger;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
new file mode 100644
index 0000000..03b77b9
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
@@ -0,0 +1,77 @@
+/*
+ * 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.phonenumbercache;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import java.io.InputStream;
+
+public interface CachedNumberLookupService {
+
+ CachedContactInfo buildCachedContactInfo(ContactInfo info);
+
+ /**
+ * Perform a lookup using the cached number lookup service to return contact information stored in
+ * the cache that corresponds to the given number.
+ *
+ * @param context Valid context
+ * @param number Phone number to lookup the cache for
+ * @return A {@link CachedContactInfo} containing the contact information if the phone number is
+ * found in the cache, {@link ContactInfo#EMPTY} if the phone number was not found in the
+ * cache, and null if there was an error when querying the cache.
+ */
+ CachedContactInfo lookupCachedContactFromNumber(Context context, String number);
+
+ void addContact(Context context, CachedContactInfo info);
+
+ boolean isCacheUri(String uri);
+
+ boolean isBusiness(int sourceType);
+
+ boolean canReportAsInvalid(int sourceType, String objectId);
+
+ /** @return return {@link Uri} to the photo or return {@code null} when failing to add photo */
+ @Nullable
+ Uri addPhoto(Context context, String number, InputStream in);
+
+ /**
+ * Remove all cached phone number entries from the cache, regardless of how old they are.
+ *
+ * @param context Valid context
+ */
+ void clearAllCacheEntries(Context context);
+
+ interface CachedContactInfo {
+
+ int SOURCE_TYPE_DIRECTORY = 1;
+ int SOURCE_TYPE_EXTENDED = 2;
+ int SOURCE_TYPE_PLACES = 3;
+ int SOURCE_TYPE_PROFILE = 4;
+ int SOURCE_TYPE_CNAP = 5;
+
+ ContactInfo getContactInfo();
+
+ void setSource(int sourceType, String name, long directoryId);
+
+ void setDirectorySource(String name, long directoryId);
+
+ void setExtendedSource(String name, long directoryId);
+
+ void setLookupKey(String lookupKey);
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/CallLogQuery.java b/java/com/android/dialer/phonenumbercache/CallLogQuery.java
new file mode 100644
index 0000000..6d47569
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/CallLogQuery.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** The query for the call log table. */
+public final class CallLogQuery {
+
+ public static final int ID = 0;
+ public static final int NUMBER = 1;
+ public static final int DATE = 2;
+ public static final int DURATION = 3;
+ public static final int CALL_TYPE = 4;
+ public static final int COUNTRY_ISO = 5;
+ public static final int VOICEMAIL_URI = 6;
+ public static final int GEOCODED_LOCATION = 7;
+ public static final int CACHED_NAME = 8;
+ public static final int CACHED_NUMBER_TYPE = 9;
+ public static final int CACHED_NUMBER_LABEL = 10;
+ public static final int CACHED_LOOKUP_URI = 11;
+ public static final int CACHED_MATCHED_NUMBER = 12;
+ public static final int CACHED_NORMALIZED_NUMBER = 13;
+ public static final int CACHED_PHOTO_ID = 14;
+ public static final int CACHED_FORMATTED_NUMBER = 15;
+ public static final int IS_READ = 16;
+ public static final int NUMBER_PRESENTATION = 17;
+ public static final int ACCOUNT_COMPONENT_NAME = 18;
+ public static final int ACCOUNT_ID = 19;
+ public static final int FEATURES = 20;
+ public static final int DATA_USAGE = 21;
+ public static final int TRANSCRIPTION = 22;
+ public static final int CACHED_PHOTO_URI = 23;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int POST_DIAL_DIGITS = 24;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int VIA_NUMBER = 25;
+
+ private static final String[] PROJECTION_M =
+ new String[] {
+ Calls._ID, // 0
+ Calls.NUMBER, // 1
+ Calls.DATE, // 2
+ Calls.DURATION, // 3
+ Calls.TYPE, // 4
+ Calls.COUNTRY_ISO, // 5
+ Calls.VOICEMAIL_URI, // 6
+ Calls.GEOCODED_LOCATION, // 7
+ Calls.CACHED_NAME, // 8
+ Calls.CACHED_NUMBER_TYPE, // 9
+ Calls.CACHED_NUMBER_LABEL, // 10
+ Calls.CACHED_LOOKUP_URI, // 11
+ Calls.CACHED_MATCHED_NUMBER, // 12
+ Calls.CACHED_NORMALIZED_NUMBER, // 13
+ Calls.CACHED_PHOTO_ID, // 14
+ Calls.CACHED_FORMATTED_NUMBER, // 15
+ Calls.IS_READ, // 16
+ Calls.NUMBER_PRESENTATION, // 17
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME, // 18
+ Calls.PHONE_ACCOUNT_ID, // 19
+ Calls.FEATURES, // 20
+ Calls.DATA_USAGE, // 21
+ Calls.TRANSCRIPTION, // 22
+ Calls.CACHED_PHOTO_URI, // 23
+ };
+
+ private static final String[] PROJECTION_N;
+
+ static {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(PROJECTION_M));
+ projectionList.add(CallLog.Calls.POST_DIAL_DIGITS);
+ projectionList.add(CallLog.Calls.VIA_NUMBER);
+ PROJECTION_N = projectionList.toArray(new String[projectionList.size()]);
+ }
+
+ @NonNull
+ public static String[] getProjection() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PROJECTION_N;
+ }
+ return PROJECTION_M;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfo.java b/java/com/android/dialer/phonenumbercache/ContactInfo.java
new file mode 100644
index 0000000..d7a75c3
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/ContactInfo.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.util.UriUtils;
+
+/** Information for a contact as needed by the Call Log. */
+public class ContactInfo {
+
+ public static final ContactInfo EMPTY = new ContactInfo();
+ public Uri lookupUri;
+ /**
+ * Contact lookup key. Note this may be a lookup key for a corp contact, in which case "lookup by
+ * lookup key" doesn't work on the personal profile.
+ */
+ public String lookupKey;
+
+ public String name;
+ public String nameAlternative;
+ public int type;
+ public String label;
+ public String number;
+ public String formattedNumber;
+ /*
+ * ContactInfo.normalizedNumber is a column value returned by PhoneLookup query. By definition,
+ * it's E164 representation.
+ * http://developer.android.com/reference/android/provider/ContactsContract.PhoneLookupColumns.
+ * html#NORMALIZED_NUMBER.
+ *
+ * The fallback value, when PhoneLookup fails or else, should be either null or
+ * PhoneNumberUtils.formatNumberToE164.
+ */
+ public String normalizedNumber;
+ /** The photo for the contact, if available. */
+ public long photoId;
+ /** The high-res photo for the contact, if available. */
+ public Uri photoUri;
+
+ public boolean isBadData;
+ public String objectId;
+ public @UserType long userType;
+ public int sourceType = 0;
+
+ /** @see android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE */
+ public int carrierPresence;
+
+ @Override
+ public int hashCode() {
+ // Uses only name and contactUri to determine hashcode.
+ // This should be sufficient to have a reasonable distribution of hash codes.
+ // Moreover, there should be no two people with the same lookupUri.
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((lookupUri == null) ? 0 : lookupUri.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ContactInfo other = (ContactInfo) obj;
+ if (!UriUtils.areEqual(lookupUri, other.lookupUri)) {
+ return false;
+ }
+ if (!TextUtils.equals(name, other.name)) {
+ return false;
+ }
+ if (!TextUtils.equals(nameAlternative, other.nameAlternative)) {
+ return false;
+ }
+ if (type != other.type) {
+ return false;
+ }
+ if (!TextUtils.equals(label, other.label)) {
+ return false;
+ }
+ if (!TextUtils.equals(number, other.number)) {
+ return false;
+ }
+ if (!TextUtils.equals(formattedNumber, other.formattedNumber)) {
+ return false;
+ }
+ if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) {
+ return false;
+ }
+ if (photoId != other.photoId) {
+ return false;
+ }
+ if (!UriUtils.areEqual(photoUri, other.photoUri)) {
+ return false;
+ }
+ if (!TextUtils.equals(objectId, other.objectId)) {
+ return false;
+ }
+ if (userType != other.userType) {
+ return false;
+ }
+ return carrierPresence == other.carrierPresence;
+ }
+
+ @Override
+ public String toString() {
+ return "ContactInfo{"
+ + "lookupUri="
+ + lookupUri
+ + ", name='"
+ + name
+ + '\''
+ + ", nameAlternative='"
+ + nameAlternative
+ + '\''
+ + ", type="
+ + type
+ + ", label='"
+ + label
+ + '\''
+ + ", number='"
+ + number
+ + '\''
+ + ", formattedNumber='"
+ + formattedNumber
+ + '\''
+ + ", normalizedNumber='"
+ + normalizedNumber
+ + '\''
+ + ", photoId="
+ + photoId
+ + ", photoUri="
+ + photoUri
+ + ", objectId='"
+ + objectId
+ + '\''
+ + ", userType="
+ + userType
+ + ", carrierPresence="
+ + carrierPresence
+ + '}';
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
new file mode 100644
index 0000000..6a5e2e6
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.List;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Utility class to look up the contact information for a given number. */
+// This class uses Java 7 language features, so it must target M+
+@TargetApi(VERSION_CODES.M)
+public class ContactInfoHelper {
+
+ private static final String TAG = ContactInfoHelper.class.getSimpleName();
+
+ private final Context mContext;
+ private final String mCurrentCountryIso;
+ private final CachedNumberLookupService mCachedNumberLookupService;
+
+ public ContactInfoHelper(Context context, String currentCountryIso) {
+ mContext = context;
+ mCurrentCountryIso = currentCountryIso;
+ mCachedNumberLookupService = PhoneNumberCache.get(mContext).getCachedNumberLookupService();
+ }
+
+ /**
+ * Creates a JSON-encoded lookup uri for a unknown number without an associated contact
+ *
+ * @param number - Unknown phone number
+ * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick
+ * contact card.
+ */
+ private static Uri createTemporaryContactUri(String number) {
+ try {
+ final JSONObject contactRows =
+ new JSONObject()
+ .put(
+ Phone.CONTENT_ITEM_TYPE,
+ new JSONObject().put(Phone.NUMBER, number).put(Phone.TYPE, Phone.TYPE_CUSTOM));
+
+ final String jsonString =
+ new JSONObject()
+ .put(Contacts.DISPLAY_NAME, number)
+ .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE)
+ .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
+ .toString();
+
+ return Contacts.CONTENT_LOOKUP_URI
+ .buildUpon()
+ .appendPath(Constants.LOOKUP_URI_ENCODED)
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Long.MAX_VALUE))
+ .encodedFragment(jsonString)
+ .build();
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public static String lookUpDisplayNameAlternative(
+ Context context, String lookupKey, @UserType long userType, @Nullable Long directoryId) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ if (lookupKey == null || userType == ContactsUtils.USER_TYPE_WORK) {
+ return null;
+ }
+
+ if (directoryId != null) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} with work lookup key is not allowed.
+ if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
+ return null;
+ }
+
+ // Skip this to avoid an extra remote network call for alternative name
+ if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ return null;
+ }
+ }
+
+ final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+ Cursor cursor = null;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(uri, PhoneQuery.DISPLAY_NAME_ALTERNATIVE_PROJECTION, null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getString(PhoneQuery.NAME_ALTERNATIVE);
+ }
+ } catch (IllegalArgumentException e) {
+ // Avoid dialer crash when lookup key is not valid
+ Log.e(TAG, "IllegalArgumentException in lookUpDisplayNameAlternative", e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return null;
+ }
+
+ public static Uri getContactInfoLookupUri(String number) {
+ return getContactInfoLookupUri(number, -1);
+ }
+
+ public static Uri getContactInfoLookupUri(String number, long directoryId) {
+ // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
+ // the number is a SIP number.
+ Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ if (directoryId != -1) {
+ // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
+ uri = PhoneLookup.CONTENT_FILTER_URI;
+ } else {
+ // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
+ number = Uri.encode(number);
+ }
+ }
+ Uri.Builder builder =
+ uri.buildUpon()
+ .appendPath(number)
+ .appendQueryParameter(
+ PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+ String.valueOf(PhoneNumberHelper.isUriNumber(number)));
+ if (directoryId != -1) {
+ builder.appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the contact information stored in an entry of the call log.
+ *
+ * @param c A cursor pointing to an entry in the call log.
+ */
+ public static ContactInfo getContactInfo(Cursor c) {
+ ContactInfo info = new ContactInfo();
+ info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
+ info.name = c.getString(CallLogQuery.CACHED_NAME);
+ info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+ info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+ String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
+ String postDialDigits =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ info.number =
+ (matchedNumber == null) ? c.getString(CallLogQuery.NUMBER) + postDialDigits : matchedNumber;
+
+ info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
+ info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
+ info.photoUri =
+ UriUtils.nullForNonContactsUri(
+ UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)));
+ info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
+
+ return info;
+ }
+
+ public ContactInfo lookupNumber(String number, String countryIso) {
+ return lookupNumber(number, countryIso, -1);
+ }
+
+ /**
+ * Returns the contact information for the given number.
+ *
+ * <p>If the number does not match any contact, returns a contact info containing only the number
+ * and the formatted number.
+ *
+ * <p>If an error occurs during the lookup, it returns null.
+ *
+ * @param number the number to look up
+ * @param countryIso the country associated with this number
+ * @param directoryId the id of the directory to lookup
+ */
+ @Nullable
+ @SuppressWarnings("ReferenceEquality")
+ public ContactInfo lookupNumber(String number, String countryIso, long directoryId) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ ContactInfo info;
+
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ // The number is a SIP address..
+ info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
+ if (info == null || info == ContactInfo.EMPTY) {
+ // If lookup failed, check if the "username" of the SIP address is a phone number.
+ String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
+ if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+ info = queryContactInfoForPhoneNumber(username, countryIso, directoryId);
+ }
+ }
+ } else {
+ // Look for a contact that has the given phone number.
+ info = queryContactInfoForPhoneNumber(number, countryIso, directoryId);
+ }
+
+ final ContactInfo updatedInfo;
+ if (info == null) {
+ // The lookup failed.
+ updatedInfo = null;
+ } else {
+ // If we did not find a matching contact, generate an empty contact info for the number.
+ if (info == ContactInfo.EMPTY) {
+ // Did not find a matching contact.
+ updatedInfo = createEmptyContactInfoForNumber(number, countryIso);
+ } else {
+ updatedInfo = info;
+ }
+ }
+ return updatedInfo;
+ }
+
+ private ContactInfo createEmptyContactInfoForNumber(String number, String countryIso) {
+ ContactInfo contactInfo = new ContactInfo();
+ contactInfo.number = number;
+ contactInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ contactInfo.lookupUri = createTemporaryContactUri(contactInfo.formattedNumber);
+ return contactInfo;
+ }
+
+ /**
+ * Return the contact info object if the remote directory lookup succeeds, otherwise return an
+ * empty contact info for the number.
+ */
+ public ContactInfo lookupNumberInRemoteDirectory(String number, String countryIso) {
+ if (mCachedNumberLookupService != null) {
+ List<Long> remoteDirectories = getRemoteDirectories(mContext);
+ for (long directoryId : remoteDirectories) {
+ ContactInfo contactInfo = lookupNumber(number, countryIso, directoryId);
+ if (hasName(contactInfo)) {
+ return contactInfo;
+ }
+ }
+ }
+ return createEmptyContactInfoForNumber(number, countryIso);
+ }
+
+ public boolean hasName(ContactInfo contactInfo) {
+ return contactInfo != null && !TextUtils.isEmpty(contactInfo.name);
+ }
+
+ private List<Long> getRemoteDirectories(Context context) {
+ List<Long> remoteDirectories = new ArrayList<>();
+ Uri uri =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ ? Directory.ENTERPRISE_CONTENT_URI
+ : Directory.CONTENT_URI;
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = cr.query(uri, new String[] {Directory._ID}, null, null, null);
+ int idIndex = cursor.getColumnIndex(Directory._ID);
+ if (cursor == null) {
+ return remoteDirectories;
+ }
+ try {
+ while (cursor.moveToNext()) {
+ long directoryId = cursor.getLong(idIndex);
+ if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ remoteDirectories.add(directoryId);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return remoteDirectories;
+ }
+
+ /**
+ * Looks up a contact using the given URI.
+ *
+ * <p>It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
+ * found, or the {@link ContactInfo} for the given contact.
+ *
+ * <p>The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
+ * value.
+ */
+ ContactInfo lookupContactFromUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ if (!PermissionsUtil.hasContactsPermissions(mContext)) {
+ return ContactInfo.EMPTY;
+ }
+
+ Cursor phoneLookupCursor = null;
+ try {
+ String[] projection = PhoneQuery.getPhoneLookupProjection(uri);
+ phoneLookupCursor = mContext.getContentResolver().query(uri, projection, null, null, null);
+ } catch (NullPointerException e) {
+ // Trap NPE from pre-N CP2
+ return null;
+ }
+ if (phoneLookupCursor == null) {
+ return null;
+ }
+
+ try {
+ if (!phoneLookupCursor.moveToFirst()) {
+ return ContactInfo.EMPTY;
+ }
+ String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
+ ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
+ fillAdditionalContactInfo(mContext, contactInfo);
+ return contactInfo;
+ } finally {
+ phoneLookupCursor.close();
+ }
+ }
+
+ private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
+ ContactInfo info = new ContactInfo();
+ info.lookupKey = lookupKey;
+ info.lookupUri =
+ Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID), lookupKey);
+ info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
+ info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
+ info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
+ info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
+ info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
+ info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
+ info.formattedNumber = null;
+ info.userType =
+ ContactsUtils.determineUserType(null, phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
+
+ return info;
+ }
+
+ private void fillAdditionalContactInfo(Context context, ContactInfo contactInfo) {
+ if (contactInfo.number == null) {
+ return;
+ }
+ Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(contactInfo.number));
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(uri, PhoneQuery.ADDITIONAL_CONTACT_INFO_PROJECTION, null, null, null)) {
+ if (cursor == null || !cursor.moveToFirst()) {
+ return;
+ }
+ contactInfo.nameAlternative =
+ cursor.getString(PhoneQuery.ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE);
+ contactInfo.carrierPresence =
+ cursor.getInt(PhoneQuery.ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE);
+ }
+ }
+
+ /**
+ * Determines the contact information for the given phone number.
+ *
+ * <p>It returns the contact info if found.
+ *
+ * <p>If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+ *
+ * <p>If the lookup fails for some other reason, it returns null.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private ContactInfo queryContactInfoForPhoneNumber(
+ String number, String countryIso, long directoryId) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
+ if (info != null && info != ContactInfo.EMPTY) {
+ info.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ } else if (mCachedNumberLookupService != null) {
+ CachedContactInfo cacheInfo =
+ mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number);
+ if (cacheInfo != null) {
+ info = cacheInfo.getContactInfo().isBadData ? null : cacheInfo.getContactInfo();
+ } else {
+ info = null;
+ }
+ }
+ return info;
+ }
+
+ /**
+ * Format the given phone number
+ *
+ * @param number the number to be formatted.
+ * @param normalizedNumber the normalized number of the given number.
+ * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
+ * used to format the number if the normalized phone is null.
+ * @return the formatted number, or the given number if it was formatted.
+ */
+ private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
+ if (TextUtils.isEmpty(number)) {
+ return "";
+ }
+ // If "number" is really a SIP address, don't try to do any formatting at all.
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ return number;
+ }
+ if (TextUtils.isEmpty(countryIso)) {
+ countryIso = mCurrentCountryIso;
+ }
+ return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+ }
+
+ /**
+ * Stores differences between the updated contact info and the current call log contact info.
+ *
+ * @param number The number of the contact.
+ * @param countryIso The country associated with this number.
+ * @param updatedInfo The updated contact info.
+ * @param callLogInfo The call log entry's current contact info.
+ */
+ public void updateCallLogContactInfo(
+ String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) {
+ if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
+ return;
+ }
+
+ final ContentValues values = new ContentValues();
+ boolean needsUpdate = false;
+
+ if (callLogInfo != null) {
+ if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ needsUpdate = true;
+ }
+
+ if (updatedInfo.type != callLogInfo.type) {
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ needsUpdate = true;
+ }
+
+ if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ needsUpdate = true;
+ }
+
+ // Only replace the normalized number if the new updated normalized number isn't empty.
+ if (!TextUtils.isEmpty(updatedInfo.normalizedNumber)
+ && !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ needsUpdate = true;
+ }
+
+ if (updatedInfo.photoId != callLogInfo.photoId) {
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ needsUpdate = true;
+ }
+
+ final Uri updatedPhotoUriContactsOnly = UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
+ if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
+ values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(updatedPhotoUriContactsOnly));
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+ } else {
+ // No previous values, store all of them.
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ values.put(
+ Calls.CACHED_PHOTO_URI,
+ UriUtils.uriToString(UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+
+ if (!needsUpdate) {
+ return;
+ }
+
+ try {
+ if (countryIso == null) {
+ mContext
+ .getContentResolver()
+ .update(
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
+ new String[] {number});
+ } else {
+ mContext
+ .getContentResolver()
+ .update(
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
+ new String[] {number, countryIso});
+ }
+ } catch (SQLiteFullException e) {
+ Log.e(TAG, "Unable to update contact info in call log db", e);
+ }
+ }
+
+ public void updateCachedNumberLookupService(ContactInfo updatedInfo) {
+ if (mCachedNumberLookupService != null) {
+ if (hasName(updatedInfo)) {
+ CachedContactInfo cachedContactInfo =
+ mCachedNumberLookupService.buildCachedContactInfo(updatedInfo);
+ mCachedNumberLookupService.addContact(mContext, cachedContactInfo);
+ }
+ }
+ }
+
+ /**
+ * Given a contact's sourceType, return true if the contact is a business
+ *
+ * @param sourceType sourceType of the contact. This is usually populated by {@link
+ * #mCachedNumberLookupService}.
+ */
+ public boolean isBusiness(int sourceType) {
+ return mCachedNumberLookupService != null && mCachedNumberLookupService.isBusiness(sourceType);
+ }
+
+ /**
+ * This function looks at a contact's source and determines if the user can mark caller ids from
+ * this source as invalid.
+ *
+ * @param sourceType The source type to be checked
+ * @param objectId The ID of the Contact object.
+ * @return true if contacts from this source can be marked with an invalid caller id
+ */
+ public boolean canReportAsInvalid(int sourceType, String objectId) {
+ return mCachedNumberLookupService != null
+ && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java b/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java
new file mode 100644
index 0000000..74175e8
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java
@@ -0,0 +1,40 @@
+/*
+ * 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.phonenumbercache;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.PhoneLookup;
+
+public final class PhoneLookupUtil {
+
+ private PhoneLookupUtil() {}
+
+ /** @return the column name that stores contact id for phone lookup query. */
+ public static String getContactIdColumnNameForUri(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PhoneLookup.CONTACT_ID;
+ }
+ // In pre-N, contact id is stored in {@link PhoneLookup#_ID} in non-sip query.
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip) ? PhoneLookup.CONTACT_ID : ContactsContract.PhoneLookup._ID;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java
new file mode 100644
index 0000000..aefa544
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java
@@ -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.
+ */
+
+package com.android.dialer.phonenumbercache;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Accessor for the phone number cache bindings. */
+public class PhoneNumberCache {
+
+ private static PhoneNumberCacheBindings phoneNumberCacheBindings;
+
+ private PhoneNumberCache() {}
+
+ public static PhoneNumberCacheBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (phoneNumberCacheBindings != null) {
+ return phoneNumberCacheBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof PhoneNumberCacheBindingsFactory) {
+ phoneNumberCacheBindings =
+ ((PhoneNumberCacheBindingsFactory) application).newPhoneNumberCacheBindings();
+ }
+
+ if (phoneNumberCacheBindings == null) {
+ phoneNumberCacheBindings = new PhoneNumberCacheBindingsStub();
+ }
+ return phoneNumberCacheBindings;
+ }
+
+ public static void setForTesting(PhoneNumberCacheBindings phoneNumberCacheBindings) {
+ PhoneNumberCache.phoneNumberCacheBindings = phoneNumberCacheBindings;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java
new file mode 100644
index 0000000..6e3ed9d
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java
@@ -0,0 +1,26 @@
+/*
+ * 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.phonenumbercache;
+
+import android.support.annotation.Nullable;
+
+/** Allows the container application provide a number look up service. */
+public interface PhoneNumberCacheBindings {
+
+ @Nullable
+ CachedNumberLookupService getCachedNumberLookupService();
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java
new file mode 100644
index 0000000..3552529
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.phonenumbercache;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows this module to get
+ * references to the PhoneNumberCacheBindings.
+ */
+public interface PhoneNumberCacheBindingsFactory {
+
+ PhoneNumberCacheBindings newPhoneNumberCacheBindings();
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java
new file mode 100644
index 0000000..c7fb978
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java
@@ -0,0 +1,29 @@
+/*
+ * 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.phonenumbercache;
+
+import android.support.annotation.Nullable;
+
+/** Default implementation of PhoneNumberCacheBindings. */
+public class PhoneNumberCacheBindingsStub implements PhoneNumberCacheBindings {
+
+ @Override
+ @Nullable
+ public CachedNumberLookupService getCachedNumberLookupService() {
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneQuery.java b/java/com/android/dialer/phonenumbercache/PhoneQuery.java
new file mode 100644
index 0000000..5ddd5f8
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneQuery.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+
+/** The queries to look up the {@link ContactInfo} for a given number in the Call Log. */
+final class PhoneQuery {
+
+ public static final int PERSON_ID = 0;
+ public static final int NAME = 1;
+ public static final int PHONE_TYPE = 2;
+ public static final int LABEL = 3;
+ public static final int MATCHED_NUMBER = 4;
+ public static final int NORMALIZED_NUMBER = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int LOOKUP_KEY = 7;
+ public static final int PHOTO_URI = 8;
+ /** Projection to look up a contact's DISPLAY_NAME_ALTERNATIVE */
+ public static final String[] DISPLAY_NAME_ALTERNATIVE_PROJECTION =
+ new String[] {
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ };
+
+ public static final int NAME_ALTERNATIVE = 0;
+
+ public static final String[] ADDITIONAL_CONTACT_INFO_PROJECTION =
+ new String[] {Phone.DISPLAY_NAME_ALTERNATIVE, Phone.CARRIER_PRESENCE};
+ public static final int ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE = 0;
+ public static final int ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE = 1;
+
+ /**
+ * Projection to look up the ContactInfo. Does not include DISPLAY_NAME_ALTERNATIVE as that column
+ * isn't available in ContactsCommon.PhoneLookup. We should always use this projection starting
+ * from NYC onward.
+ */
+ private static final String[] PHONE_LOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup.CONTACT_ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.PHOTO_ID,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.PHOTO_URI
+ };
+ /**
+ * Similar to {@link PHONE_LOOKUP_PROJECTION}. In pre-N, contact id is stored in {@link
+ * PhoneLookup#_ID} in non-sip query.
+ */
+ private static final String[] BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.PHOTO_ID,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.PHOTO_URI
+ };
+
+ public static String[] getPhoneLookupProjection(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PHONE_LOOKUP_PROJECTION;
+ }
+ // Pre-N
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip) ? PHONE_LOOKUP_PROJECTION : BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_PROJECTION;
+ }
+}
diff --git a/java/com/android/dialer/phonenumberutil/AndroidManifest.xml b/java/com/android/dialer/phonenumberutil/AndroidManifest.xml
new file mode 100644
index 0000000..f7ee400
--- /dev/null
+++ b/java/com/android/dialer/phonenumberutil/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.phonenumberutil">
+</manifest>
diff --git a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
new file mode 100644
index 0000000..ea4396f
--- /dev/null
+++ b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2013 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.phonenumberutil;
+
+import android.content.Context;
+import android.provider.CallLog;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+public class PhoneNumberHelper {
+
+ private static final String TAG = "PhoneNumberUtil";
+ private static final Set<String> LEGACY_UNKNOWN_NUMBERS =
+ new HashSet<>(Arrays.asList("-1", "-2", "-3"));
+
+ /** Returns true if it is possible to place a call to the given number. */
+ public static boolean canPlaceCallsTo(CharSequence number, int presentation) {
+ return presentation == CallLog.Calls.PRESENTATION_ALLOWED
+ && !TextUtils.isEmpty(number)
+ && !isLegacyUnknownNumbers(number);
+ }
+
+ /**
+ * Returns true if the given number is the number of the configured voicemail. To be able to
+ * mock-out this, it is not a static method.
+ */
+ public static boolean isVoicemailNumber(
+ Context context, PhoneAccountHandle accountHandle, CharSequence number) {
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+ return TelecomUtil.isVoicemailNumber(context, accountHandle, number.toString());
+ }
+
+ /**
+ * Returns true if the given number is a SIP address. To be able to mock-out this, it is not a
+ * static method.
+ */
+ public static boolean isSipNumber(CharSequence number) {
+ return number != null && isUriNumber(number.toString());
+ }
+
+ public static boolean isUnknownNumberThatCanBeLookedUp(
+ Context context, PhoneAccountHandle accountHandle, CharSequence number, int presentation) {
+ if (presentation == CallLog.Calls.PRESENTATION_UNKNOWN) {
+ return false;
+ }
+ if (presentation == CallLog.Calls.PRESENTATION_RESTRICTED) {
+ return false;
+ }
+ if (presentation == CallLog.Calls.PRESENTATION_PAYPHONE) {
+ return false;
+ }
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+ if (isVoicemailNumber(context, accountHandle, number)) {
+ return false;
+ }
+ if (isLegacyUnknownNumbers(number)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean isLegacyUnknownNumbers(CharSequence number) {
+ return number != null && LEGACY_UNKNOWN_NUMBERS.contains(number.toString());
+ }
+
+ /**
+ * @return a geographical description string for the specified number.
+ * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder
+ */
+ public static String getGeoDescription(Context context, String number) {
+ LogUtil.v("PhoneNumberHelper.getGeoDescription", "" + LogUtil.sanitizePii(number));
+
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+ PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+
+ Locale locale = context.getResources().getConfiguration().locale;
+ String countryIso = getCurrentCountryIso(context, locale);
+ Phonenumber.PhoneNumber pn = null;
+ try {
+ LogUtil.v(
+ "PhoneNumberHelper.getGeoDescription",
+ "parsing '" + LogUtil.sanitizePii(number) + "' for countryIso '" + countryIso + "'...");
+ pn = util.parse(number, countryIso);
+ LogUtil.v(
+ "PhoneNumberHelper.getGeoDescription", "- parsed number: " + LogUtil.sanitizePii(pn));
+ } catch (NumberParseException e) {
+ LogUtil.e(
+ "PhoneNumberHelper.getGeoDescription",
+ "getGeoDescription: NumberParseException for incoming number '"
+ + LogUtil.sanitizePii(number)
+ + "'");
+ }
+
+ if (pn != null) {
+ String description = geocoder.getDescriptionForNumber(pn, locale);
+ LogUtil.v("PhoneNumberHelper.getGeoDescription", "- got description: '" + description + "'");
+ return description;
+ }
+
+ return null;
+ }
+
+ /**
+ * @return The ISO 3166-1 two letters country code of the country the user is in based on the
+ * network location. If the network location does not exist, fall back to the locale setting.
+ */
+ public static String getCurrentCountryIso(Context context, Locale locale) {
+ // Without framework function calls, this seems to be the most accurate location service
+ // we can rely on.
+ final TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+
+ String countryIso = telephonyManager.getNetworkCountryIso();
+ if (TextUtils.isEmpty(countryIso)) {
+ countryIso = locale.getCountry();
+ LogUtil.i(
+ "PhoneNumberHelper.getCurrentCountryIso",
+ "No CountryDetector; falling back to countryIso based on locale: " + countryIso);
+ }
+ countryIso = countryIso.toUpperCase();
+
+ return countryIso;
+ }
+
+ /**
+ * @return Formatted phone number. e.g. 1-123-456-7890. Returns the original number if formatting
+ * failed.
+ */
+ public static String formatNumber(@Nullable String number, Context context) {
+ // The number can be null e.g. schema is voicemail and uri content is empty.
+ if (number == null) {
+ return null;
+ }
+ String formattedNumber =
+ PhoneNumberUtils.formatNumber(
+ number, PhoneNumberHelper.getCurrentCountryIso(context, Locale.getDefault()));
+ return formattedNumber != null ? formattedNumber : number;
+ }
+
+ /**
+ * Determines if the specified number is actually a URI (i.e. a SIP address) rather than a regular
+ * PSTN phone number, based on whether or not the number contains an "@" character.
+ *
+ * @param number Phone number
+ * @return true if number contains @
+ * <p>TODO: Remove if PhoneNumberUtils.isUriNumber(String number) is made public.
+ */
+ public static boolean isUriNumber(String number) {
+ // Note we allow either "@" or "%40" to indicate a URI, in case
+ // the passed-in string is URI-escaped. (Neither "@" nor "%40"
+ // will ever be found in a legal PSTN number.)
+ return number != null && (number.contains("@") || number.contains("%40"));
+ }
+
+ /**
+ * @param number SIP address of the form "username@domainname" (or the URI-escaped equivalent
+ * "username%40domainname")
+ * <p>TODO: Remove if PhoneNumberUtils.getUsernameFromUriNumber(String number) is made public.
+ * @return the "username" part of the specified SIP address, i.e. the part before the "@"
+ * character (or "%40").
+ */
+ public static String getUsernameFromUriNumber(String number) {
+ // The delimiter between username and domain name can be
+ // either "@" or "%40" (the URI-escaped equivalent.)
+ int delimiterIndex = number.indexOf('@');
+ if (delimiterIndex < 0) {
+ delimiterIndex = number.indexOf("%40");
+ }
+ if (delimiterIndex < 0) {
+ LogUtil.i(
+ "PhoneNumberHelper.getUsernameFromUriNumber",
+ "getUsernameFromUriNumber: no delimiter found in SIP address: "
+ + LogUtil.sanitizePii(number));
+ return number;
+ }
+ return number.substring(0, delimiterIndex);
+ }
+
+
+ private static boolean isVerizon(Context context) {
+ // Verizon MCC/MNC codes copied from com/android/voicemailomtp/res/xml/vvm_config.xml.
+ // TODO: Need a better way to do per carrier and per OEM configurations.
+ switch (context.getSystemService(TelephonyManager.class).getSimOperator()) {
+ case "310004":
+ case "310010":
+ case "310012":
+ case "310013":
+ case "310590":
+ case "310890":
+ case "310910":
+ case "311110":
+ case "311270":
+ case "311271":
+ case "311272":
+ case "311273":
+ case "311274":
+ case "311275":
+ case "311276":
+ case "311277":
+ case "311278":
+ case "311279":
+ case "311280":
+ case "311281":
+ case "311282":
+ case "311283":
+ case "311284":
+ case "311285":
+ case "311286":
+ case "311287":
+ case "311288":
+ case "311289":
+ case "311390":
+ case "311480":
+ case "311481":
+ case "311482":
+ case "311483":
+ case "311484":
+ case "311485":
+ case "311486":
+ case "311487":
+ case "311488":
+ case "311489":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets the label to display for a phone call where the presentation is set as
+ * PRESENTATION_RESTRICTED. For Verizon we want this to be displayed as "Restricted". For all
+ * other carriers we want this to be be displayed as "Private number".
+ */
+ public static CharSequence getDisplayNameForRestrictedNumber(Context context) {
+ if (isVerizon(context)) {
+ return context.getString(R.string.private_num_verizon);
+ } else {
+ return context.getString(R.string.private_num_non_verizon);
+ }
+ }
+}
diff --git a/java/com/android/dialer/phonenumberutil/res/values/strings.xml b/java/com/android/dialer/phonenumberutil/res/values/strings.xml
new file mode 100644
index 0000000..f31883e
--- /dev/null
+++ b/java/com/android/dialer/phonenumberutil/res/values/strings.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <!-- String used to display calls from private numbers in the call log and in call UI. For
+ example, if the user gets an incoming phone call from an unknown number, the caller ID will
+ read "Restricted". We only show this string if the user is on the Verizon network. -->
+ <string name="private_num_verizon">Restricted</string>
+
+ <!-- String used to display calls from private numbers in the call log. For example, if the user
+ gets an incoming phone call from an unknown number, the caller ID will read "Private number".
+ We only show this string if the user is not on the Verizon network. -->
+ <string name="private_num_non_verizon">Private number</string>
+</resources>
diff --git a/java/com/android/dialer/proguard/UsedByReflection.java b/java/com/android/dialer/proguard/UsedByReflection.java
new file mode 100644
index 0000000..200c33e
--- /dev/null
+++ b/java/com/android/dialer/proguard/UsedByReflection.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.proguard;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the class, constructor, method or field is used for reflection and therefore cannot
+ * be removed by tools like ProGuard. Use the value parameter to mention a file that uses the
+ * component marked as UsedByReflection.
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD})
+public @interface UsedByReflection {
+
+ String value();
+}
diff --git a/java/com/android/dialer/protos/ProtoParsers.java b/java/com/android/dialer/protos/ProtoParsers.java
new file mode 100644
index 0000000..7dfbc7c
--- /dev/null
+++ b/java/com/android/dialer/protos/ProtoParsers.java
@@ -0,0 +1,167 @@
+/*
+ * 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.protos;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.android.dialer.common.Assert;
+import com.google.protobuf.nano.CodedOutputByteBufferNano;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+import com.google.protobuf.nano.MessageNano;
+import java.io.IOException;
+
+/** Useful methods for using Protocol Buffers with Android. */
+public final class ProtoParsers {
+
+ private ProtoParsers() {}
+
+ /** Retrieve a proto from a Bundle */
+ @SuppressWarnings("unchecked") // We want to eventually optimize away parser classes, so cast
+ public static <T extends MessageNano> T get(Bundle bundle, String key, T defaultInstance)
+ throws InvalidProtocolBufferNanoException {
+ InternalDontUse parcelable = bundle.getParcelable(key);
+ return (T) parcelable.getMessageUnsafe(defaultInstance);
+ }
+
+ /**
+ * Retrieve a proto from a trusted bundle
+ *
+ * @throws RuntimeException if the proto cannot be parsed
+ */
+ public static <T extends MessageNano> T getFromInstanceState(
+ Bundle bundle, String key, T defaultInstance) {
+ try {
+ return get(bundle, key, defaultInstance);
+ } catch (InvalidProtocolBufferNanoException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Stores a proto in a Bundle, for later retrieval by {@link #get(Bundle, String, MessageNano)} or
+ * {@link #getFromInstanceState(Bundle, String, MessageNano)}.
+ */
+ public static void put(Bundle bundle, String key, MessageNano message) {
+ bundle.putParcelable(key, new InternalDontUse<>(null, message));
+ }
+
+ /**
+ * Stores a proto in an Intent, for later retrieval by {@link #get(Bundle, String, MessageNano)}.
+ * Needs separate method because Intent has similar to but different API than Bundle.
+ */
+ public static void put(Intent intent, String key, MessageNano message) {
+ intent.putExtra(key, new InternalDontUse<>(null, message));
+ }
+
+ /** Returns a {@linkplain Parcelable} representation of this protobuf message. */
+ public static <T extends MessageNano> ParcelableProto<T> asParcelable(T message) {
+ return new InternalDontUse<>(null, message);
+ }
+
+ /**
+ * A protobuf message that can be stored in a {@link Parcel}.
+ *
+ * <p><b>Note:</b> This <code>Parcelable</code> can only be used in single app. Attempting to send
+ * it to another app through an <code>Intent</code> will result in an exception due to Proguard
+ * obfusation when the target application attempts to load the <code>ParcelableProto</code> class.
+ */
+ public interface ParcelableProto<T extends MessageNano> extends Parcelable {
+ /**
+ * @throws IllegalStateException if the parceled data does not correspond to the defaultInstance
+ * type.
+ */
+ T getMessage(T defaultInstance);
+ }
+
+ /** Public because of Parcelable requirements. Do not use. */
+ public static final class InternalDontUse<T extends MessageNano> implements ParcelableProto<T> {
+ /* One of these two fields is always populated - since the bytes field never escapes this
+ * object, there is no risk of concurrent modification by multiple threads, and volatile
+ * is sufficient to be thread-safe. */
+ private volatile byte[] bytes;
+ private volatile T message;
+
+ /**
+ * Ideally, we would have type safety here. However, a static field {@link Creator} is required
+ * by {@link Parcelable}. Static fields are inherently not type safe, since only 1 exists per
+ * class (rather than 1 per type).
+ */
+ public static final Parcelable.Creator<InternalDontUse<?>> CREATOR =
+ new Creator<InternalDontUse<?>>() {
+ @Override
+ public InternalDontUse<?> createFromParcel(Parcel parcel) {
+ int serializedSize = parcel.readInt();
+ byte[] array = new byte[serializedSize];
+ parcel.readByteArray(array);
+ return new InternalDontUse<>(array, null);
+ }
+
+ @Override
+ public InternalDontUse<?>[] newArray(int i) {
+ return new InternalDontUse[i];
+ }
+ };
+
+ private InternalDontUse(byte[] bytes, T message) {
+ Assert.checkArgument(bytes != null || message != null);
+ this.bytes = bytes;
+ this.message = message;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ if (bytes == null) {
+ final byte[] flatArray = new byte[message.getSerializedSize()];
+ try {
+ message.writeTo(CodedOutputByteBufferNano.newInstance(flatArray));
+ bytes = flatArray;
+ } catch (IOException impossible) {
+ throw new AssertionError(impossible);
+ }
+ }
+ parcel.writeInt(bytes.length);
+ parcel.writeByteArray(bytes);
+ }
+
+ @Override
+ public T getMessage(T defaultInstance) {
+ try {
+ // The proto should never be invalid if it came from our application, so if it is, throw.
+ return getMessageUnsafe(defaultInstance);
+ } catch (InvalidProtocolBufferNanoException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked") // We're being deserialized, so there's no real type safety
+ T getMessageUnsafe(T defaultInstance) throws InvalidProtocolBufferNanoException {
+ // There's a risk that we'll double-parse the bytes, but that's OK, because it'll end up
+ // as the same immutable object anyway.
+ if (message == null) {
+ message = MessageNano.mergeFrom(defaultInstance, bytes);
+ }
+ return message;
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/AndroidManifest.xml b/java/com/android/dialer/shortcuts/AndroidManifest.xml
new file mode 100644
index 0000000..e731a3e
--- /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 0000000..ef995c8
--- /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 0000000..1e9a01b
--- /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 0000000..f2fb330
--- /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 0000000..be9e088
--- /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 0000000..a8c4ada
--- /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 0000000..62c9e37
--- /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 0000000..bfcc3df
--- /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 0000000..086d1dc
--- /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 0000000..cf780bb
--- /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 0000000..f5ff648
--- /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 0000000..50130fc
--- /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 0000000..b6a7fa8
--- /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 0000000..4cfc436
--- /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 0000000..c06aec8
--- /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 0000000..e20309b
--- /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 0000000..2321256
--- /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 0000000..1e2c87f
--- /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 0000000..085854d
--- /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 0000000..5e8f58d
--- /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>
diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java
new file mode 100644
index 0000000..78058a4
--- /dev/null
+++ b/java/com/android/dialer/simulator/Simulator.java
@@ -0,0 +1,27 @@
+/*
+ * 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.simulator;
+
+import android.content.Context;
+import android.view.ActionProvider;
+
+/** Used to add menu items to the Dialer menu to test the app using simulated calls and data. */
+public interface Simulator {
+ boolean shouldShow();
+
+ ActionProvider getActionProvider(Context context);
+}
diff --git a/java/com/android/dialer/simulator/impl/AndroidManifest.xml b/java/com/android/dialer/simulator/impl/AndroidManifest.xml
new file mode 100644
index 0000000..a30504d
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.simulator.impl">
+
+ <application>
+
+ <service
+ android:exported="true"
+ android:name=".SimulatorConnectionService"
+ android:permission="android.permission.BIND_CONNECTION_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecomm.ConnectionService"/>
+ </intent-filter>
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
new file mode 100644
index 0000000..5918198
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
@@ -0,0 +1,160 @@
+/*
+ * 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.simulator.impl;
+
+import android.support.annotation.NonNull;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_SimulatorCallLog_CallEntry extends SimulatorCallLog.CallEntry {
+
+ private final String number;
+ private final int type;
+ private final int presentation;
+ private final long timeMillis;
+
+ private AutoValue_SimulatorCallLog_CallEntry(
+ String number,
+ int type,
+ int presentation,
+ long timeMillis) {
+ this.number = number;
+ this.type = type;
+ this.presentation = presentation;
+ this.timeMillis = timeMillis;
+ }
+
+ @NonNull
+ @Override
+ String getNumber() {
+ return number;
+ }
+
+ @Override
+ int getType() {
+ return type;
+ }
+
+ @Override
+ int getPresentation() {
+ return presentation;
+ }
+
+ @Override
+ long getTimeMillis() {
+ return timeMillis;
+ }
+
+ @Override
+ public String toString() {
+ return "CallEntry{"
+ + "number=" + number + ", "
+ + "type=" + type + ", "
+ + "presentation=" + presentation + ", "
+ + "timeMillis=" + timeMillis
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SimulatorCallLog.CallEntry) {
+ SimulatorCallLog.CallEntry that = (SimulatorCallLog.CallEntry) o;
+ return (this.number.equals(that.getNumber()))
+ && (this.type == that.getType())
+ && (this.presentation == that.getPresentation())
+ && (this.timeMillis == that.getTimeMillis());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.number.hashCode();
+ h *= 1000003;
+ h ^= this.type;
+ h *= 1000003;
+ h ^= this.presentation;
+ h *= 1000003;
+ h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
+ return h;
+ }
+
+ static final class Builder extends SimulatorCallLog.CallEntry.Builder {
+ private String number;
+ private Integer type;
+ private Integer presentation;
+ private Long timeMillis;
+ Builder() {
+ }
+ private Builder(SimulatorCallLog.CallEntry source) {
+ this.number = source.getNumber();
+ this.type = source.getType();
+ this.presentation = source.getPresentation();
+ this.timeMillis = source.getTimeMillis();
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setNumber(String number) {
+ this.number = number;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setType(int type) {
+ this.type = type;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setPresentation(int presentation) {
+ this.presentation = presentation;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setTimeMillis(long timeMillis) {
+ this.timeMillis = timeMillis;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry build() {
+ String missing = "";
+ if (this.number == null) {
+ missing += " number";
+ }
+ if (this.type == null) {
+ missing += " type";
+ }
+ if (this.presentation == null) {
+ missing += " presentation";
+ }
+ if (this.timeMillis == null) {
+ missing += " timeMillis";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_SimulatorCallLog_CallEntry(
+ this.number,
+ this.type,
+ this.presentation,
+ this.timeMillis);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
new file mode 100644
index 0000000..00295f3
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
@@ -0,0 +1,231 @@
+/*
+ * 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.simulator.impl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_SimulatorContacts_Contact extends SimulatorContacts.Contact {
+
+ private final String accountType;
+ private final String accountName;
+ private final String name;
+ private final boolean isStarred;
+ private final ByteArrayOutputStream photoStream;
+ private final List<SimulatorContacts.PhoneNumber> phoneNumbers;
+ private final List<SimulatorContacts.Email> emails;
+
+ private AutoValue_SimulatorContacts_Contact(
+ String accountType,
+ String accountName,
+ @Nullable String name,
+ boolean isStarred,
+ @Nullable ByteArrayOutputStream photoStream,
+ List<SimulatorContacts.PhoneNumber> phoneNumbers,
+ List<SimulatorContacts.Email> emails) {
+ this.accountType = accountType;
+ this.accountName = accountName;
+ this.name = name;
+ this.isStarred = isStarred;
+ this.photoStream = photoStream;
+ this.phoneNumbers = phoneNumbers;
+ this.emails = emails;
+ }
+
+ @NonNull
+ @Override
+ String getAccountType() {
+ return accountType;
+ }
+
+ @NonNull
+ @Override
+ String getAccountName() {
+ return accountName;
+ }
+
+ @Nullable
+ @Override
+ String getName() {
+ return name;
+ }
+
+ @Override
+ boolean getIsStarred() {
+ return isStarred;
+ }
+
+ @Nullable
+ @Override
+ ByteArrayOutputStream getPhotoStream() {
+ return photoStream;
+ }
+
+ @NonNull
+ @Override
+ List<SimulatorContacts.PhoneNumber> getPhoneNumbers() {
+ return phoneNumbers;
+ }
+
+ @NonNull
+ @Override
+ List<SimulatorContacts.Email> getEmails() {
+ return emails;
+ }
+
+ @Override
+ public String toString() {
+ return "Contact{"
+ + "accountType=" + accountType + ", "
+ + "accountName=" + accountName + ", "
+ + "name=" + name + ", "
+ + "isStarred=" + isStarred + ", "
+ + "photoStream=" + photoStream + ", "
+ + "phoneNumbers=" + phoneNumbers + ", "
+ + "emails=" + emails
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SimulatorContacts.Contact) {
+ SimulatorContacts.Contact that = (SimulatorContacts.Contact) o;
+ return (this.accountType.equals(that.getAccountType()))
+ && (this.accountName.equals(that.getAccountName()))
+ && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName()))
+ && (this.isStarred == that.getIsStarred())
+ && ((this.photoStream == null) ? (that.getPhotoStream() == null) : this.photoStream.equals(that.getPhotoStream()))
+ && (this.phoneNumbers.equals(that.getPhoneNumbers()))
+ && (this.emails.equals(that.getEmails()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.accountType.hashCode();
+ h *= 1000003;
+ h ^= this.accountName.hashCode();
+ h *= 1000003;
+ h ^= (name == null) ? 0 : this.name.hashCode();
+ h *= 1000003;
+ h ^= this.isStarred ? 1231 : 1237;
+ h *= 1000003;
+ h ^= (photoStream == null) ? 0 : this.photoStream.hashCode();
+ h *= 1000003;
+ h ^= this.phoneNumbers.hashCode();
+ h *= 1000003;
+ h ^= this.emails.hashCode();
+ return h;
+ }
+
+ static final class Builder extends SimulatorContacts.Contact.Builder {
+ private String accountType;
+ private String accountName;
+ private String name;
+ private Boolean isStarred;
+ private ByteArrayOutputStream photoStream;
+ private List<SimulatorContacts.PhoneNumber> phoneNumbers;
+ private List<SimulatorContacts.Email> emails;
+ Builder() {
+ }
+ private Builder(SimulatorContacts.Contact source) {
+ this.accountType = source.getAccountType();
+ this.accountName = source.getAccountName();
+ this.name = source.getName();
+ this.isStarred = source.getIsStarred();
+ this.photoStream = source.getPhotoStream();
+ this.phoneNumbers = source.getPhoneNumbers();
+ this.emails = source.getEmails();
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setAccountType(String accountType) {
+ this.accountType = accountType;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setAccountName(String accountName) {
+ this.accountName = accountName;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setName(@Nullable String name) {
+ this.name = name;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setIsStarred(boolean isStarred) {
+ this.isStarred = isStarred;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setPhotoStream(@Nullable ByteArrayOutputStream photoStream) {
+ this.photoStream = photoStream;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setPhoneNumbers(List<SimulatorContacts.PhoneNumber> phoneNumbers) {
+ this.phoneNumbers = phoneNumbers;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setEmails(List<SimulatorContacts.Email> emails) {
+ this.emails = emails;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact build() {
+ String missing = "";
+ if (this.accountType == null) {
+ missing += " accountType";
+ }
+ if (this.accountName == null) {
+ missing += " accountName";
+ }
+ if (this.isStarred == null) {
+ missing += " isStarred";
+ }
+ if (this.phoneNumbers == null) {
+ missing += " phoneNumbers";
+ }
+ if (this.emails == null) {
+ missing += " emails";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_SimulatorContacts_Contact(
+ this.accountType,
+ this.accountName,
+ this.name,
+ this.isStarred,
+ this.photoStream,
+ this.phoneNumbers,
+ this.emails);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
new file mode 100644
index 0000000..5893480
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
@@ -0,0 +1,184 @@
+/*
+ * 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.simulator.impl;
+
+import android.support.annotation.NonNull;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_SimulatorVoicemail_Voicemail extends SimulatorVoicemail.Voicemail {
+
+ private final String phoneNumber;
+ private final String transcription;
+ private final long durationSeconds;
+ private final long timeMillis;
+ private final boolean isRead;
+
+ private AutoValue_SimulatorVoicemail_Voicemail(
+ String phoneNumber,
+ String transcription,
+ long durationSeconds,
+ long timeMillis,
+ boolean isRead) {
+ this.phoneNumber = phoneNumber;
+ this.transcription = transcription;
+ this.durationSeconds = durationSeconds;
+ this.timeMillis = timeMillis;
+ this.isRead = isRead;
+ }
+
+ @NonNull
+ @Override
+ String getPhoneNumber() {
+ return phoneNumber;
+ }
+
+ @NonNull
+ @Override
+ String getTranscription() {
+ return transcription;
+ }
+
+ @Override
+ long getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ @Override
+ long getTimeMillis() {
+ return timeMillis;
+ }
+
+ @Override
+ boolean getIsRead() {
+ return isRead;
+ }
+
+ @Override
+ public String toString() {
+ return "Voicemail{"
+ + "phoneNumber=" + phoneNumber + ", "
+ + "transcription=" + transcription + ", "
+ + "durationSeconds=" + durationSeconds + ", "
+ + "timeMillis=" + timeMillis + ", "
+ + "isRead=" + isRead
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SimulatorVoicemail.Voicemail) {
+ SimulatorVoicemail.Voicemail that = (SimulatorVoicemail.Voicemail) o;
+ return (this.phoneNumber.equals(that.getPhoneNumber()))
+ && (this.transcription.equals(that.getTranscription()))
+ && (this.durationSeconds == that.getDurationSeconds())
+ && (this.timeMillis == that.getTimeMillis())
+ && (this.isRead == that.getIsRead());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.phoneNumber.hashCode();
+ h *= 1000003;
+ h ^= this.transcription.hashCode();
+ h *= 1000003;
+ h ^= (this.durationSeconds >>> 32) ^ this.durationSeconds;
+ h *= 1000003;
+ h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
+ h *= 1000003;
+ h ^= this.isRead ? 1231 : 1237;
+ return h;
+ }
+
+ static final class Builder extends SimulatorVoicemail.Voicemail.Builder {
+ private String phoneNumber;
+ private String transcription;
+ private Long durationSeconds;
+ private Long timeMillis;
+ private Boolean isRead;
+ Builder() {
+ }
+ private Builder(SimulatorVoicemail.Voicemail source) {
+ this.phoneNumber = source.getPhoneNumber();
+ this.transcription = source.getTranscription();
+ this.durationSeconds = source.getDurationSeconds();
+ this.timeMillis = source.getTimeMillis();
+ this.isRead = source.getIsRead();
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setPhoneNumber(String phoneNumber) {
+ this.phoneNumber = phoneNumber;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setTranscription(String transcription) {
+ this.transcription = transcription;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setDurationSeconds(long durationSeconds) {
+ this.durationSeconds = durationSeconds;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setTimeMillis(long timeMillis) {
+ this.timeMillis = timeMillis;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setIsRead(boolean isRead) {
+ this.isRead = isRead;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail build() {
+ String missing = "";
+ if (this.phoneNumber == null) {
+ missing += " phoneNumber";
+ }
+ if (this.transcription == null) {
+ missing += " transcription";
+ }
+ if (this.durationSeconds == null) {
+ missing += " durationSeconds";
+ }
+ if (this.timeMillis == null) {
+ missing += " timeMillis";
+ }
+ if (this.isRead == null) {
+ missing += " isRead";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_SimulatorVoicemail_Voicemail(
+ this.phoneNumber,
+ this.transcription,
+ this.durationSeconds,
+ this.timeMillis,
+ this.isRead);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java b/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java
new file mode 100644
index 0000000..6cd5733
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java
@@ -0,0 +1,88 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.view.ActionProvider;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** Implements the simulator submenu. */
+final class SimulatorActionProvider extends ActionProvider {
+ @NonNull private final Context context;
+
+ public SimulatorActionProvider(@NonNull Context context) {
+ super(Assert.isNotNull(context));
+ this.context = context;
+ }
+
+ @Override
+ public View onCreateActionView() {
+ LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(null)");
+ return null;
+ }
+
+ @Override
+ public View onCreateActionView(MenuItem forItem) {
+ LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(MenuItem)");
+ return null;
+ }
+
+ @Override
+ public boolean hasSubMenu() {
+ LogUtil.enterBlock("SimulatorActionProvider.hasSubMenu");
+ return true;
+ }
+
+ @Override
+ public void onPrepareSubMenu(SubMenu subMenu) {
+ super.onPrepareSubMenu(subMenu);
+ LogUtil.enterBlock("SimulatorActionProvider.onPrepareSubMenu");
+ subMenu.clear();
+ subMenu
+ .add("Add call")
+ .setOnMenuItemClickListener(
+ (item) -> {
+ SimulatorVoiceCall.addNewIncomingCall(context);
+ return true;
+ });
+ subMenu
+ .add("Populate database")
+ .setOnMenuItemClickListener(
+ (item) -> {
+ populateDatabase();
+ return true;
+ });
+ }
+
+ private void populateDatabase() {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ SimulatorContacts.populateContacts(context);
+ SimulatorCallLog.populateCallLog(context);
+ SimulatorVoicemail.populateVoicemail(context);
+ return null;
+ }
+ }.execute();
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
new file mode 100644
index 0000000..9ace047
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
@@ -0,0 +1,139 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/** Populates the device database with call log entries. */
+final class SimulatorCallLog {
+ // Phone numbers from https://www.google.com/about/company/facts/locations/
+ private static final CallEntry.Builder[] SIMPLE_CALL_LOG = {
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder()
+ .setType(Calls.MISSED_TYPE)
+ .setNumber("")
+ .setPresentation(Calls.PRESENTATION_UNKNOWN),
+ CallEntry.builder().setType(Calls.REJECTED_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder()
+ .setType(Calls.MISSED_TYPE)
+ .setNumber("1234")
+ .setPresentation(Calls.PRESENTATION_RESTRICTED),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder().setType(Calls.BLOCKED_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("(425) 739-5600"),
+ CallEntry.builder().setType(Calls.ANSWERED_EXTERNALLY_TYPE).setNumber("(425) 739-5600"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("+1 (425) 739-5600"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("739-5600"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("711"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("711"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("(425) 739-5600"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("+44 (0) 20 7031 3000"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1-650-2530000"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1 303-245-0086;123,456"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1 303-245-0086"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+1-650-2530000"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("650-2530000"),
+ CallEntry.builder().setType(Calls.REJECTED_TYPE).setNumber("2530000"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1 404-487-9000"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+61 2 9374 4001"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+33 (0)1 42 68 53 00"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("972-74-746-6245"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+971 4 4509500"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+971 4 4509500"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("55-31-2128-6800"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("611"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("*86 512-343-5283"),
+ };
+
+ @WorkerThread
+ public static void populateCallLog(@NonNull Context context) {
+ Assert.isWorkerThread();
+ ArrayList<ContentProviderOperation> operations = new ArrayList<>();
+ // Do this 4 times to make the call log 4 times bigger.
+ long timeMillis = System.currentTimeMillis();
+ for (int i = 0; i < 4; i++) {
+ for (CallEntry.Builder builder : SIMPLE_CALL_LOG) {
+ CallEntry callEntry = builder.setTimeMillis(timeMillis).build();
+ operations.add(
+ ContentProviderOperation.newInsert(Calls.CONTENT_URI)
+ .withValues(callEntry.getAsContentValues())
+ .withYieldAllowed(true)
+ .build());
+ timeMillis -= TimeUnit.HOURS.toMillis(1);
+ }
+ }
+ try {
+ context.getContentResolver().applyBatch(CallLog.AUTHORITY, operations);
+ } catch (RemoteException | OperationApplicationException e) {
+ Assert.fail("error adding call entries: " + e);
+ }
+ }
+
+
+ abstract static class CallEntry {
+ @NonNull
+ abstract String getNumber();
+
+ abstract int getType();
+
+ abstract int getPresentation();
+
+ abstract long getTimeMillis();
+
+ static Builder builder() {
+ return new AutoValue_SimulatorCallLog_CallEntry.Builder()
+ .setPresentation(Calls.PRESENTATION_ALLOWED);
+ }
+
+ ContentValues getAsContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(Calls.TYPE, getType());
+ values.put(Calls.NUMBER, getNumber());
+ values.put(Calls.NUMBER_PRESENTATION, getPresentation());
+ values.put(Calls.DATE, getTimeMillis());
+ return values;
+ }
+
+
+ abstract static class Builder {
+ abstract Builder setNumber(@NonNull String number);
+
+ abstract Builder setType(int type);
+
+ abstract Builder setPresentation(int presentation);
+
+ abstract Builder setTimeMillis(long timeMillis);
+
+ abstract CallEntry build();
+ }
+ }
+
+ private SimulatorCallLog() {}
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnection.java b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
new file mode 100644
index 0000000..12d0958
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
@@ -0,0 +1,56 @@
+/*
+ * 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.simulator.impl;
+
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import com.android.dialer.common.LogUtil;
+
+/** Represents a single phone call on the device. */
+final class SimulatorConnection extends Connection {
+
+ @Override
+ public void onAnswer() {
+ LogUtil.enterBlock("SimulatorConnection.onAnswer");
+ setActive();
+ }
+
+ @Override
+ public void onReject() {
+ LogUtil.enterBlock("SimulatorConnection.onReject");
+ setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+ }
+
+ @Override
+ public void onHold() {
+ LogUtil.enterBlock("SimulatorConnection.onHold");
+ setOnHold();
+ }
+
+ @Override
+ public void onUnhold() {
+ LogUtil.enterBlock("SimulatorConnection.onUnhold");
+ setActive();
+ }
+
+ @Override
+ public void onDisconnect() {
+ LogUtil.enterBlock("SimulatorConnection.onDisconnect");
+ setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+ destroy();
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
new file mode 100644
index 0000000..3223607
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
@@ -0,0 +1,87 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Simple connection provider to create an incoming call. This is useful for emulators. */
+public final class SimulatorConnectionService extends ConnectionService {
+
+ private static final String PHONE_ACCOUNT_ID = "SIMULATOR_ACCOUNT_ID";
+
+ public static void register(Context context) {
+ LogUtil.enterBlock("SimulatorConnectionService.register");
+ context.getSystemService(TelecomManager.class).registerPhoneAccount(buildPhoneAccount(context));
+ }
+
+ private static PhoneAccount buildPhoneAccount(Context context) {
+ PhoneAccount.Builder builder =
+ new PhoneAccount.Builder(
+ getConnectionServiceHandle(context), "Simulator connection service");
+ List<String> uriSchemes = new ArrayList<>();
+ uriSchemes.add(PhoneAccount.SCHEME_TEL);
+
+ return builder
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setShortDescription("Simulator Connection Service")
+ .setSupportedUriSchemes(uriSchemes)
+ .build();
+ }
+
+ public static PhoneAccountHandle getConnectionServiceHandle(Context context) {
+ return new PhoneAccountHandle(
+ new ComponentName(context, SimulatorConnectionService.class), PHONE_ACCOUNT_ID);
+ }
+
+ private static Uri getPhoneNumber(ConnectionRequest request) {
+ String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
+ }
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ LogUtil.i(
+ "SimulatorConnectionService.onCreateOutgoingConnection",
+ "outgoing calls not supported yet");
+ return null;
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ LogUtil.enterBlock("SimulatorConnectionService.onCreateIncomingConnection");
+ SimulatorConnection connection = new SimulatorConnection();
+ connection.setRinging();
+ connection.setAddress(getPhoneNumber(request), TelecomManager.PRESENTATION_ALLOWED);
+ connection.setConnectionCapabilities(
+ Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
+ return connection;
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorContacts.java b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
new file mode 100644
index 0000000..8931509
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
@@ -0,0 +1,319 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ContentProviderOperation;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Populates the device database with contacts. */
+final class SimulatorContacts {
+ // Phone numbers from https://www.google.com/about/company/facts/locations/
+ private static final Contact[] SIMPLE_CONTACTS = {
+ // US, contact with e164 number.
+ Contact.builder()
+ .setName("Michelangelo")
+ .addPhoneNumber(new PhoneNumber("+1-302-6365454", Phone.TYPE_MOBILE))
+ .addEmail(new Email("m@example.com"))
+ .setIsStarred(true)
+ .setOrangePhoto()
+ .build(),
+ // US, contact with a non-e164 number.
+ Contact.builder()
+ .setName("Leonardo da Vinci")
+ .addPhoneNumber(new PhoneNumber("(425) 739-5600", Phone.TYPE_MOBILE))
+ .addEmail(new Email("l@example.com"))
+ .setIsStarred(true)
+ .setBluePhoto()
+ .build(),
+ // UK, number where the (0) should be dropped.
+ Contact.builder()
+ .setName("Raphael")
+ .addPhoneNumber(new PhoneNumber("+44 (0) 20 7031 3000", Phone.TYPE_MOBILE))
+ .addEmail(new Email("r@example.com"))
+ .setIsStarred(true)
+ .setRedPhoto()
+ .build(),
+ // US and Australia, contact with a long name and multiple phone numbers.
+ Contact.builder()
+ .setName("Donatello di Niccolò di Betto Bardi")
+ .addPhoneNumber(new PhoneNumber("+1-650-2530000", Phone.TYPE_HOME))
+ .addPhoneNumber(new PhoneNumber("+1 404-487-9000", Phone.TYPE_WORK))
+ .addPhoneNumber(new PhoneNumber("+61 2 9374 4001", Phone.TYPE_FAX_HOME))
+ .setIsStarred(true)
+ .setPurplePhoto()
+ .build(),
+ // US, phone number shared with another contact and 2nd phone number with wait and pause.
+ Contact.builder()
+ .setName("Splinter")
+ .addPhoneNumber(new PhoneNumber("+1-650-2530000", Phone.TYPE_HOME))
+ .addPhoneNumber(new PhoneNumber("+1 303-245-0086;123,456", Phone.TYPE_WORK))
+ .build(),
+ // France, number with Japanese name.
+ Contact.builder()
+ .setName("スパイク・スピーゲル")
+ .addPhoneNumber(new PhoneNumber("+33 (0)1 42 68 53 00", Phone.TYPE_MOBILE))
+ .build(),
+ // Israel, RTL name and non-e164 number.
+ Contact.builder()
+ .setName("עקב אריה טברסק")
+ .addPhoneNumber(new PhoneNumber("+33 (0)1 42 68 53 00", Phone.TYPE_MOBILE))
+ .build(),
+ // UAE, RTL name.
+ Contact.builder()
+ .setName("سلام دنیا")
+ .addPhoneNumber(new PhoneNumber("+971 4 4509500", Phone.TYPE_MOBILE))
+ .build(),
+ // Brazil, contact with no name.
+ Contact.builder()
+ .addPhoneNumber(new PhoneNumber("+55-31-2128-6800", Phone.TYPE_MOBILE))
+ .build(),
+ // Short number, contact with no name.
+ Contact.builder().addPhoneNumber(new PhoneNumber("611", Phone.TYPE_MOBILE)).build(),
+ // US, number with an anonymous prefix.
+ Contact.builder()
+ .setName("Anonymous")
+ .addPhoneNumber(new PhoneNumber("*86 512-343-5283", Phone.TYPE_MOBILE))
+ .build(),
+ // None, contact with no phone number.
+ Contact.builder()
+ .setName("No Phone Number")
+ .addEmail(new Email("no@example.com"))
+ .setIsStarred(true)
+ .build(),
+ };
+
+ @WorkerThread
+ static void populateContacts(@NonNull Context context) {
+ Assert.isWorkerThread();
+ ArrayList<ContentProviderOperation> operations = new ArrayList<>();
+ for (Contact contact : SIMPLE_CONTACTS) {
+ addContact(contact, operations);
+ }
+ try {
+ context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
+ } catch (RemoteException | OperationApplicationException e) {
+ Assert.fail("error adding contacts: " + e);
+ }
+ }
+
+ private static void addContact(Contact contact, List<ContentProviderOperation> operations) {
+ int index = operations.size();
+
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, contact.getAccountType())
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, contact.getAccountName())
+ .withValue(ContactsContract.RawContacts.STARRED, contact.getIsStarred())
+ .withYieldAllowed(true)
+ .build());
+
+ if (!TextUtils.isEmpty(contact.getName())) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(
+ ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, contact.getName())
+ .build());
+ }
+
+ if (contact.getPhotoStream() != null) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(
+ ContactsContract.CommonDataKinds.Photo.PHOTO,
+ contact.getPhotoStream().toByteArray())
+ .build());
+ }
+
+ for (PhoneNumber phoneNumber : contact.getPhoneNumbers()) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber.value)
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneNumber.type)
+ .withValue(ContactsContract.CommonDataKinds.Phone.LABEL, phoneNumber.label)
+ .build());
+ }
+
+ for (Email email : contact.getEmails()) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Email.DATA, email.value)
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, email.type)
+ .withValue(ContactsContract.CommonDataKinds.Email.LABEL, email.label)
+ .build());
+ }
+ }
+
+
+ abstract static class Contact {
+ @NonNull
+ abstract String getAccountType();
+
+ @NonNull
+ abstract String getAccountName();
+
+ @Nullable
+ abstract String getName();
+
+ abstract boolean getIsStarred();
+
+ @Nullable
+ abstract ByteArrayOutputStream getPhotoStream();
+
+ @NonNull
+ abstract List<PhoneNumber> getPhoneNumbers();
+
+ @NonNull
+ abstract List<Email> getEmails();
+
+ static Builder builder() {
+ return new AutoValue_SimulatorContacts_Contact.Builder()
+ .setAccountType("com.google")
+ .setAccountName("foo@example")
+ .setIsStarred(false)
+ .setPhoneNumbers(new ArrayList<>())
+ .setEmails(new ArrayList<>());
+ }
+
+
+ abstract static class Builder {
+ @NonNull private final List<PhoneNumber> phoneNumbers = new ArrayList<>();
+ @NonNull private final List<Email> emails = new ArrayList<>();
+
+ abstract Builder setAccountType(@NonNull String accountType);
+
+ abstract Builder setAccountName(@NonNull String accountName);
+
+ abstract Builder setName(@NonNull String name);
+
+ abstract Builder setIsStarred(boolean isStarred);
+
+ abstract Builder setPhotoStream(ByteArrayOutputStream photoStream);
+
+ abstract Builder setPhoneNumbers(@NonNull List<PhoneNumber> phoneNumbers);
+
+ abstract Builder setEmails(@NonNull List<Email> emails);
+
+ abstract Contact build();
+
+ Builder addPhoneNumber(PhoneNumber phoneNumber) {
+ phoneNumbers.add(phoneNumber);
+ return setPhoneNumbers(phoneNumbers);
+ }
+
+ Builder addEmail(Email email) {
+ emails.add(email);
+ return setEmails(emails);
+ }
+
+ Builder setRedPhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0xe3, 0x33, 0x1c)));
+ return this;
+ }
+
+ Builder setBluePhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0x00, 0xaa, 0xe6)));
+ return this;
+ }
+
+ Builder setOrangePhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0xea, 0x95, 0x00)));
+ return this;
+ }
+
+ Builder setPurplePhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0x99, 0x5a, 0xa0)));
+ return this;
+ }
+
+ /** Creates a contact photo with a green background and a circle of the given color. */
+ private static ByteArrayOutputStream getPhotoStreamWithColor(int color) {
+ int width = 300;
+ int height = 300;
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(Color.argb(0xff, 0x4c, 0x9c, 0x23));
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(width / 2, height / 2, width / 3, paint);
+
+ ByteArrayOutputStream photoStream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 75, photoStream);
+ return photoStream;
+ }
+ }
+ }
+
+ static class PhoneNumber {
+ public final String value;
+ public final int type;
+ public final String label;
+
+ PhoneNumber(String value, int type) {
+ this.value = value;
+ this.type = type;
+ label = "simulator phone number";
+ }
+ }
+
+ static class Email {
+ public final String value;
+ public final int type;
+ public final String label;
+
+ Email(String simpleEmail) {
+ value = simpleEmail;
+ type = ContactsContract.CommonDataKinds.Email.TYPE_WORK;
+ label = "simulator email";
+ }
+ }
+
+ private SimulatorContacts() {}
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorModule.java b/java/com/android/dialer/simulator/impl/SimulatorModule.java
new file mode 100644
index 0000000..0f8ad39
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorModule.java
@@ -0,0 +1,34 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.Context;
+import android.view.ActionProvider;
+import com.android.dialer.simulator.Simulator;
+
+/** The entry point for the simulator module. */
+public final class SimulatorModule implements Simulator {
+ @Override
+ public boolean shouldShow() {
+ return true;
+ }
+
+ @Override
+ public ActionProvider getActionProvider(Context context) {
+ return new SimulatorActionProvider(context);
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
new file mode 100644
index 0000000..39c1d02
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
@@ -0,0 +1,47 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** Utilities to simulate phone calls. */
+final class SimulatorVoiceCall {
+ public static void addNewIncomingCall(@NonNull Context context) {
+ LogUtil.enterBlock("SimulatorVoiceCall.addNewIncomingCall");
+ SimulatorConnectionService.register(context);
+
+ Bundle bundle = new Bundle();
+ // Set the caller ID to the Google London office.
+ bundle.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, "+44 (0) 20 7031 3000");
+ try {
+ context
+ .getSystemService(TelecomManager.class)
+ .addNewIncomingCall(
+ SimulatorConnectionService.getConnectionServiceHandle(context), bundle);
+ } catch (SecurityException e) {
+ Assert.fail("unable to add call: " + e);
+ }
+ }
+
+ private SimulatorVoiceCall() {}
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
new file mode 100644
index 0000000..ffb9191
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
@@ -0,0 +1,154 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+
+import java.util.concurrent.TimeUnit;
+
+/** Populates the device database with voicemail entries. */
+final class SimulatorVoicemail {
+ private static final String ACCOUNT_ID = "ACCOUNT_ID";
+
+ private static final Voicemail.Builder[] SIMPLE_VOICEMAILS = {
+ // Long transcription with an embedded phone number.
+ Voicemail.builder()
+ .setPhoneNumber("+1-302-6365454")
+ .setTranscription(
+ "Hi, this is a very long voicemail. Please call me back at 650 253 0000. "
+ + "I hope you listen to all of it. This is very important. "
+ + "Hi, this is a very long voicemail. "
+ + "I hope you listen to all of it. It's very important.")
+ .setDurationSeconds(10)
+ .setIsRead(false),
+ // RTL transcription.
+ Voicemail.builder()
+ .setPhoneNumber("+1-302-6365454")
+ .setTranscription("هزاران دوست کم اند و یک دشمن زیاد")
+ .setDurationSeconds(60)
+ .setIsRead(true),
+ // Empty number.
+ Voicemail.builder()
+ .setPhoneNumber("")
+ .setTranscription("")
+ .setDurationSeconds(60)
+ .setIsRead(true),
+ // No duration.
+ Voicemail.builder()
+ .setPhoneNumber("+1-302-6365454")
+ .setTranscription("")
+ .setDurationSeconds(0)
+ .setIsRead(true),
+ // Short number.
+ Voicemail.builder()
+ .setPhoneNumber("711")
+ .setTranscription("This is a short voicemail.")
+ .setDurationSeconds(12)
+ .setIsRead(true),
+ };
+
+ @WorkerThread
+ public static void populateVoicemail(@NonNull Context context) {
+ Assert.isWorkerThread();
+ enableVoicemail(context);
+
+ // Do this 4 times to make the voicemail database 4 times bigger.
+ long timeMillis = System.currentTimeMillis();
+ for (int i = 0; i < 4; i++) {
+ for (Voicemail.Builder builder : SIMPLE_VOICEMAILS) {
+ Voicemail voicemail = builder.setTimeMillis(timeMillis).build();
+ context
+ .getContentResolver()
+ .insert(
+ Voicemails.buildSourceUri(context.getPackageName()),
+ voicemail.getAsContentValues(context));
+ timeMillis -= TimeUnit.HOURS.toMillis(2);
+ }
+ }
+ }
+
+ private static void enableVoicemail(@NonNull Context context) {
+ PhoneAccountHandle handle =
+ new PhoneAccountHandle(new ComponentName(context, SimulatorVoicemail.class), ACCOUNT_ID);
+
+ ContentValues values = new ContentValues();
+ values.put(Status.SOURCE_PACKAGE, handle.getComponentName().getPackageName());
+ values.put(Status.SOURCE_TYPE, TelephonyManager.VVM_TYPE_OMTP);
+ values.put(Status.PHONE_ACCOUNT_COMPONENT_NAME, handle.getComponentName().flattenToString());
+ values.put(Status.PHONE_ACCOUNT_ID, handle.getId());
+ values.put(Status.CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+ values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_OK);
+ values.put(Status.NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
+ context.getContentResolver().insert(Status.buildSourceUri(context.getPackageName()), values);
+ }
+
+
+ abstract static class Voicemail {
+ @NonNull
+ abstract String getPhoneNumber();
+
+ @NonNull
+ abstract String getTranscription();
+
+ abstract long getDurationSeconds();
+
+ abstract long getTimeMillis();
+
+ abstract boolean getIsRead();
+
+ static Builder builder() {
+ return new AutoValue_SimulatorVoicemail_Voicemail.Builder();
+ }
+
+ ContentValues getAsContentValues(Context context) {
+ ContentValues values = new ContentValues();
+ values.put(Voicemails.DATE, getTimeMillis());
+ values.put(Voicemails.NUMBER, getPhoneNumber());
+ values.put(Voicemails.DURATION, getDurationSeconds());
+ values.put(Voicemails.SOURCE_PACKAGE, context.getPackageName());
+ values.put(Voicemails.IS_READ, getIsRead() ? 1 : 0);
+ values.put(Voicemails.TRANSCRIPTION, getTranscription());
+ return values;
+ }
+
+
+ abstract static class Builder {
+ abstract Builder setPhoneNumber(@NonNull String phoneNumber);
+
+ abstract Builder setTranscription(@NonNull String transcription);
+
+ abstract Builder setDurationSeconds(long durationSeconds);
+
+ abstract Builder setTimeMillis(long timeMillis);
+
+ abstract Builder setIsRead(boolean isRead);
+
+ abstract Voicemail build();
+ }
+ }
+
+ private SimulatorVoicemail() {}
+}
diff --git a/java/com/android/dialer/smartdial/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
new file mode 100644
index 0000000..c512c5d
--- /dev/null
+++ b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
@@ -0,0 +1,784 @@
+/*
+ * 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.smartdial;
+
+public class LatinSmartDialMap implements SmartDialMap {
+
+ private static final char[] LATIN_LETTERS_TO_DIGITS = {
+ '2',
+ '2',
+ '2', // A,B,C -> 2
+ '3',
+ '3',
+ '3', // D,E,F -> 3
+ '4',
+ '4',
+ '4', // G,H,I -> 4
+ '5',
+ '5',
+ '5', // J,K,L -> 5
+ '6',
+ '6',
+ '6', // M,N,O -> 6
+ '7',
+ '7',
+ '7',
+ '7', // P,Q,R,S -> 7
+ '8',
+ '8',
+ '8', // T,U,V -> 8
+ '9',
+ '9',
+ '9',
+ '9' // W,X,Y,Z -> 9
+ };
+
+ @Override
+ public boolean isValidDialpadAlphabeticChar(char ch) {
+ return (ch >= 'a' && ch <= 'z');
+ }
+
+ @Override
+ public boolean isValidDialpadNumericChar(char ch) {
+ return (ch >= '0' && ch <= '9');
+ }
+
+ @Override
+ public boolean isValidDialpadCharacter(char ch) {
+ return (isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch));
+ }
+
+ /*
+ * The switch statement in this function was generated using the python code:
+ * from unidecode import unidecode
+ * for i in range(192, 564):
+ * char = unichr(i)
+ * decoded = unidecode(char)
+ * # Unicode characters that decompose into multiple characters i.e.
+ * # into ss are not supported for now
+ * if (len(decoded) == 1 and decoded.isalpha()):
+ * print "case '" + char + "': return '" + unidecode(char) + "';"
+ *
+ * This gives us a way to map characters containing accents/diacritics to their
+ * alphabetic equivalents. The unidecode library can be found at:
+ * http://pypi.python.org/pypi/Unidecode/0.04.1
+ *
+ * Also remaps all upper case latin characters to their lower case equivalents.
+ */
+ @Override
+ public char normalizeCharacter(char ch) {
+ switch (ch) {
+ case 'À':
+ return 'a';
+ case 'Á':
+ return 'a';
+ case 'Â':
+ return 'a';
+ case 'Ã':
+ return 'a';
+ case 'Ä':
+ return 'a';
+ case 'Å':
+ return 'a';
+ case 'Ç':
+ return 'c';
+ case 'È':
+ return 'e';
+ case 'É':
+ return 'e';
+ case 'Ê':
+ return 'e';
+ case 'Ë':
+ return 'e';
+ case 'Ì':
+ return 'i';
+ case 'Í':
+ return 'i';
+ case 'Î':
+ return 'i';
+ case 'Ï':
+ return 'i';
+ case 'Ð':
+ return 'd';
+ case 'Ñ':
+ return 'n';
+ case 'Ò':
+ return 'o';
+ case 'Ó':
+ return 'o';
+ case 'Ô':
+ return 'o';
+ case 'Õ':
+ return 'o';
+ case 'Ö':
+ return 'o';
+ case '×':
+ return 'x';
+ case 'Ø':
+ return 'o';
+ case 'Ù':
+ return 'u';
+ case 'Ú':
+ return 'u';
+ case 'Û':
+ return 'u';
+ case 'Ü':
+ return 'u';
+ case 'Ý':
+ return 'u';
+ case 'à':
+ return 'a';
+ case 'á':
+ return 'a';
+ case 'â':
+ return 'a';
+ case 'ã':
+ return 'a';
+ case 'ä':
+ return 'a';
+ case 'å':
+ return 'a';
+ case 'ç':
+ return 'c';
+ case 'è':
+ return 'e';
+ case 'é':
+ return 'e';
+ case 'ê':
+ return 'e';
+ case 'ë':
+ return 'e';
+ case 'ì':
+ return 'i';
+ case 'í':
+ return 'i';
+ case 'î':
+ return 'i';
+ case 'ï':
+ return 'i';
+ case 'ð':
+ return 'd';
+ case 'ñ':
+ return 'n';
+ case 'ò':
+ return 'o';
+ case 'ó':
+ return 'o';
+ case 'ô':
+ return 'o';
+ case 'õ':
+ return 'o';
+ case 'ö':
+ return 'o';
+ case 'ø':
+ return 'o';
+ case 'ù':
+ return 'u';
+ case 'ú':
+ return 'u';
+ case 'û':
+ return 'u';
+ case 'ü':
+ return 'u';
+ case 'ý':
+ return 'y';
+ case 'ÿ':
+ return 'y';
+ case 'Ā':
+ return 'a';
+ case 'ā':
+ return 'a';
+ case 'Ă':
+ return 'a';
+ case 'ă':
+ return 'a';
+ case 'Ą':
+ return 'a';
+ case 'ą':
+ return 'a';
+ case 'Ć':
+ return 'c';
+ case 'ć':
+ return 'c';
+ case 'Ĉ':
+ return 'c';
+ case 'ĉ':
+ return 'c';
+ case 'Ċ':
+ return 'c';
+ case 'ċ':
+ return 'c';
+ case 'Č':
+ return 'c';
+ case 'č':
+ return 'c';
+ case 'Ď':
+ return 'd';
+ case 'ď':
+ return 'd';
+ case 'Đ':
+ return 'd';
+ case 'đ':
+ return 'd';
+ case 'Ē':
+ return 'e';
+ case 'ē':
+ return 'e';
+ case 'Ĕ':
+ return 'e';
+ case 'ĕ':
+ return 'e';
+ case 'Ė':
+ return 'e';
+ case 'ė':
+ return 'e';
+ case 'Ę':
+ return 'e';
+ case 'ę':
+ return 'e';
+ case 'Ě':
+ return 'e';
+ case 'ě':
+ return 'e';
+ case 'Ĝ':
+ return 'g';
+ case 'ĝ':
+ return 'g';
+ case 'Ğ':
+ return 'g';
+ case 'ğ':
+ return 'g';
+ case 'Ġ':
+ return 'g';
+ case 'ġ':
+ return 'g';
+ case 'Ģ':
+ return 'g';
+ case 'ģ':
+ return 'g';
+ case 'Ĥ':
+ return 'h';
+ case 'ĥ':
+ return 'h';
+ case 'Ħ':
+ return 'h';
+ case 'ħ':
+ return 'h';
+ case 'Ĩ':
+ return 'i';
+ case 'ĩ':
+ return 'i';
+ case 'Ī':
+ return 'i';
+ case 'ī':
+ return 'i';
+ case 'Ĭ':
+ return 'i';
+ case 'ĭ':
+ return 'i';
+ case 'Į':
+ return 'i';
+ case 'į':
+ return 'i';
+ case 'İ':
+ return 'i';
+ case 'ı':
+ return 'i';
+ case 'Ĵ':
+ return 'j';
+ case 'ĵ':
+ return 'j';
+ case 'Ķ':
+ return 'k';
+ case 'ķ':
+ return 'k';
+ case 'ĸ':
+ return 'k';
+ case 'Ĺ':
+ return 'l';
+ case 'ĺ':
+ return 'l';
+ case 'Ļ':
+ return 'l';
+ case 'ļ':
+ return 'l';
+ case 'Ľ':
+ return 'l';
+ case 'ľ':
+ return 'l';
+ case 'Ŀ':
+ return 'l';
+ case 'ŀ':
+ return 'l';
+ case 'Ł':
+ return 'l';
+ case 'ł':
+ return 'l';
+ case 'Ń':
+ return 'n';
+ case 'ń':
+ return 'n';
+ case 'Ņ':
+ return 'n';
+ case 'ņ':
+ return 'n';
+ case 'Ň':
+ return 'n';
+ case 'ň':
+ return 'n';
+ case 'Ō':
+ return 'o';
+ case 'ō':
+ return 'o';
+ case 'Ŏ':
+ return 'o';
+ case 'ŏ':
+ return 'o';
+ case 'Ő':
+ return 'o';
+ case 'ő':
+ return 'o';
+ case 'Ŕ':
+ return 'r';
+ case 'ŕ':
+ return 'r';
+ case 'Ŗ':
+ return 'r';
+ case 'ŗ':
+ return 'r';
+ case 'Ř':
+ return 'r';
+ case 'ř':
+ return 'r';
+ case 'Ś':
+ return 's';
+ case 'ś':
+ return 's';
+ case 'Ŝ':
+ return 's';
+ case 'ŝ':
+ return 's';
+ case 'Ş':
+ return 's';
+ case 'ş':
+ return 's';
+ case 'Š':
+ return 's';
+ case 'š':
+ return 's';
+ case 'Ţ':
+ return 't';
+ case 'ţ':
+ return 't';
+ case 'Ť':
+ return 't';
+ case 'ť':
+ return 't';
+ case 'Ŧ':
+ return 't';
+ case 'ŧ':
+ return 't';
+ case 'Ũ':
+ return 'u';
+ case 'ũ':
+ return 'u';
+ case 'Ū':
+ return 'u';
+ case 'ū':
+ return 'u';
+ case 'Ŭ':
+ return 'u';
+ case 'ŭ':
+ return 'u';
+ case 'Ů':
+ return 'u';
+ case 'ů':
+ return 'u';
+ case 'Ű':
+ return 'u';
+ case 'ű':
+ return 'u';
+ case 'Ų':
+ return 'u';
+ case 'ų':
+ return 'u';
+ case 'Ŵ':
+ return 'w';
+ case 'ŵ':
+ return 'w';
+ case 'Ŷ':
+ return 'y';
+ case 'ŷ':
+ return 'y';
+ case 'Ÿ':
+ return 'y';
+ case 'Ź':
+ return 'z';
+ case 'ź':
+ return 'z';
+ case 'Ż':
+ return 'z';
+ case 'ż':
+ return 'z';
+ case 'Ž':
+ return 'z';
+ case 'ž':
+ return 'z';
+ case 'ſ':
+ return 's';
+ case 'ƀ':
+ return 'b';
+ case 'Ɓ':
+ return 'b';
+ case 'Ƃ':
+ return 'b';
+ case 'ƃ':
+ return 'b';
+ case 'Ɔ':
+ return 'o';
+ case 'Ƈ':
+ return 'c';
+ case 'ƈ':
+ return 'c';
+ case 'Ɖ':
+ return 'd';
+ case 'Ɗ':
+ return 'd';
+ case 'Ƌ':
+ return 'd';
+ case 'ƌ':
+ return 'd';
+ case 'ƍ':
+ return 'd';
+ case 'Ɛ':
+ return 'e';
+ case 'Ƒ':
+ return 'f';
+ case 'ƒ':
+ return 'f';
+ case 'Ɠ':
+ return 'g';
+ case 'Ɣ':
+ return 'g';
+ case 'Ɩ':
+ return 'i';
+ case 'Ɨ':
+ return 'i';
+ case 'Ƙ':
+ return 'k';
+ case 'ƙ':
+ return 'k';
+ case 'ƚ':
+ return 'l';
+ case 'ƛ':
+ return 'l';
+ case 'Ɯ':
+ return 'w';
+ case 'Ɲ':
+ return 'n';
+ case 'ƞ':
+ return 'n';
+ case 'Ɵ':
+ return 'o';
+ case 'Ơ':
+ return 'o';
+ case 'ơ':
+ return 'o';
+ case 'Ƥ':
+ return 'p';
+ case 'ƥ':
+ return 'p';
+ case 'ƫ':
+ return 't';
+ case 'Ƭ':
+ return 't';
+ case 'ƭ':
+ return 't';
+ case 'Ʈ':
+ return 't';
+ case 'Ư':
+ return 'u';
+ case 'ư':
+ return 'u';
+ case 'Ʊ':
+ return 'y';
+ case 'Ʋ':
+ return 'v';
+ case 'Ƴ':
+ return 'y';
+ case 'ƴ':
+ return 'y';
+ case 'Ƶ':
+ return 'z';
+ case 'ƶ':
+ return 'z';
+ case 'ƿ':
+ return 'w';
+ case 'Ǎ':
+ return 'a';
+ case 'ǎ':
+ return 'a';
+ case 'Ǐ':
+ return 'i';
+ case 'ǐ':
+ return 'i';
+ case 'Ǒ':
+ return 'o';
+ case 'ǒ':
+ return 'o';
+ case 'Ǔ':
+ return 'u';
+ case 'ǔ':
+ return 'u';
+ case 'Ǖ':
+ return 'u';
+ case 'ǖ':
+ return 'u';
+ case 'Ǘ':
+ return 'u';
+ case 'ǘ':
+ return 'u';
+ case 'Ǚ':
+ return 'u';
+ case 'ǚ':
+ return 'u';
+ case 'Ǜ':
+ return 'u';
+ case 'ǜ':
+ return 'u';
+ case 'Ǟ':
+ return 'a';
+ case 'ǟ':
+ return 'a';
+ case 'Ǡ':
+ return 'a';
+ case 'ǡ':
+ return 'a';
+ case 'Ǥ':
+ return 'g';
+ case 'ǥ':
+ return 'g';
+ case 'Ǧ':
+ return 'g';
+ case 'ǧ':
+ return 'g';
+ case 'Ǩ':
+ return 'k';
+ case 'ǩ':
+ return 'k';
+ case 'Ǫ':
+ return 'o';
+ case 'ǫ':
+ return 'o';
+ case 'Ǭ':
+ return 'o';
+ case 'ǭ':
+ return 'o';
+ case 'ǰ':
+ return 'j';
+ case 'Dz':
+ return 'd';
+ case 'Ǵ':
+ return 'g';
+ case 'ǵ':
+ return 'g';
+ case 'Ƿ':
+ return 'w';
+ case 'Ǹ':
+ return 'n';
+ case 'ǹ':
+ return 'n';
+ case 'Ǻ':
+ return 'a';
+ case 'ǻ':
+ return 'a';
+ case 'Ǿ':
+ return 'o';
+ case 'ǿ':
+ return 'o';
+ case 'Ȁ':
+ return 'a';
+ case 'ȁ':
+ return 'a';
+ case 'Ȃ':
+ return 'a';
+ case 'ȃ':
+ return 'a';
+ case 'Ȅ':
+ return 'e';
+ case 'ȅ':
+ return 'e';
+ case 'Ȇ':
+ return 'e';
+ case 'ȇ':
+ return 'e';
+ case 'Ȉ':
+ return 'i';
+ case 'ȉ':
+ return 'i';
+ case 'Ȋ':
+ return 'i';
+ case 'ȋ':
+ return 'i';
+ case 'Ȍ':
+ return 'o';
+ case 'ȍ':
+ return 'o';
+ case 'Ȏ':
+ return 'o';
+ case 'ȏ':
+ return 'o';
+ case 'Ȑ':
+ return 'r';
+ case 'ȑ':
+ return 'r';
+ case 'Ȓ':
+ return 'r';
+ case 'ȓ':
+ return 'r';
+ case 'Ȕ':
+ return 'u';
+ case 'ȕ':
+ return 'u';
+ case 'Ȗ':
+ return 'u';
+ case 'ȗ':
+ return 'u';
+ case 'Ș':
+ return 's';
+ case 'ș':
+ return 's';
+ case 'Ț':
+ return 't';
+ case 'ț':
+ return 't';
+ case 'Ȝ':
+ return 'y';
+ case 'ȝ':
+ return 'y';
+ case 'Ȟ':
+ return 'h';
+ case 'ȟ':
+ return 'h';
+ case 'Ȥ':
+ return 'z';
+ case 'ȥ':
+ return 'z';
+ case 'Ȧ':
+ return 'a';
+ case 'ȧ':
+ return 'a';
+ case 'Ȩ':
+ return 'e';
+ case 'ȩ':
+ return 'e';
+ case 'Ȫ':
+ return 'o';
+ case 'ȫ':
+ return 'o';
+ case 'Ȭ':
+ return 'o';
+ case 'ȭ':
+ return 'o';
+ case 'Ȯ':
+ return 'o';
+ case 'ȯ':
+ return 'o';
+ case 'Ȱ':
+ return 'o';
+ case 'ȱ':
+ return 'o';
+ case 'Ȳ':
+ return 'y';
+ case 'ȳ':
+ return 'y';
+ case 'A':
+ return 'a';
+ case 'B':
+ return 'b';
+ case 'C':
+ return 'c';
+ case 'D':
+ return 'd';
+ case 'E':
+ return 'e';
+ case 'F':
+ return 'f';
+ case 'G':
+ return 'g';
+ case 'H':
+ return 'h';
+ case 'I':
+ return 'i';
+ case 'J':
+ return 'j';
+ case 'K':
+ return 'k';
+ case 'L':
+ return 'l';
+ case 'M':
+ return 'm';
+ case 'N':
+ return 'n';
+ case 'O':
+ return 'o';
+ case 'P':
+ return 'p';
+ case 'Q':
+ return 'q';
+ case 'R':
+ return 'r';
+ case 'S':
+ return 's';
+ case 'T':
+ return 't';
+ case 'U':
+ return 'u';
+ case 'V':
+ return 'v';
+ case 'W':
+ return 'w';
+ case 'X':
+ return 'x';
+ case 'Y':
+ return 'y';
+ case 'Z':
+ return 'z';
+ default:
+ return ch;
+ }
+ }
+
+ @Override
+ public byte getDialpadIndex(char ch) {
+ if (ch >= '0' && ch <= '9') {
+ return (byte) (ch - '0');
+ } else if (ch >= 'a' && ch <= 'z') {
+ return (byte) (LATIN_LETTERS_TO_DIGITS[ch - 'a'] - '0');
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public char getDialpadNumericCharacter(char ch) {
+ if (ch >= 'a' && ch <= 'z') {
+ return LATIN_LETTERS_TO_DIGITS[ch - 'a'];
+ }
+ return ch;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialMap.java b/java/com/android/dialer/smartdial/SmartDialMap.java
new file mode 100644
index 0000000..9638929
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialMap.java
@@ -0,0 +1,60 @@
+/*
+ * 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.smartdial;
+
+/**
+ * Note: These methods currently take characters as arguments. For future planned language support,
+ * they will need to be changed to use codepoints instead of characters.
+ *
+ * <p>http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#codePointAt(int)
+ *
+ * <p>If/when this change is made, LatinSmartDialMap(which operates on chars) will continue to work
+ * by simply casting from a codepoint to a character.
+ */
+public interface SmartDialMap {
+
+ /*
+ * Returns true if the provided character can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadCharacter(char ch);
+
+ /*
+ * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadAlphabeticChar(char ch);
+
+ /*
+ * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadNumericChar(char ch);
+
+ /*
+ * Get the index of the key on the dialpad which the character corresponds to
+ */
+ byte getDialpadIndex(char ch);
+
+ /*
+ * Get the actual numeric character on the dialpad which the character corresponds to
+ */
+ char getDialpadNumericCharacter(char ch);
+
+ /*
+ * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents
+ * from accented characters.
+ */
+ char normalizeCharacter(char ch);
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialMatchPosition.java b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java
new file mode 100644
index 0000000..8056ad7
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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.smartdial;
+
+import android.util.Log;
+import java.util.ArrayList;
+
+/**
+ * Stores information about a range of characters matched in a display name The integers start and
+ * end indicate that the range start to end (exclusive) correspond to some characters in the query.
+ * Used to highlight certain parts of the contact's display name to indicate that those ranges
+ * matched the user's query.
+ */
+public class SmartDialMatchPosition {
+
+ private static final String TAG = SmartDialMatchPosition.class.getSimpleName();
+
+ public int start;
+ public int end;
+
+ public SmartDialMatchPosition(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Used by {@link SmartDialNameMatcher} to advance the positions of a match position found in a
+ * sub query.
+ *
+ * @param inList ArrayList of SmartDialMatchPositions to modify.
+ * @param toAdvance Offset to modify by.
+ */
+ public static void advanceMatchPositions(
+ ArrayList<SmartDialMatchPosition> inList, int toAdvance) {
+ for (int i = 0; i < inList.size(); i++) {
+ inList.get(i).advance(toAdvance);
+ }
+ }
+
+ /**
+ * Used mainly for debug purposes. Displays contents of an ArrayList of SmartDialMatchPositions.
+ *
+ * @param list ArrayList of SmartDialMatchPositions to print out in a human readable fashion.
+ */
+ public static void print(ArrayList<SmartDialMatchPosition> list) {
+ for (int i = 0; i < list.size(); i++) {
+ SmartDialMatchPosition m = list.get(i);
+ Log.d(TAG, "[" + m.start + "," + m.end + "]");
+ }
+ }
+
+ private void advance(int toAdvance) {
+ this.start += toAdvance;
+ this.end += toAdvance;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
new file mode 100644
index 0000000..a1580a0
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2012 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.smartdial;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.dialer.smartdial.SmartDialPrefix.PhoneNumberTokens;
+import java.util.ArrayList;
+
+/**
+ * {@link #SmartDialNameMatcher} contains utility functions to remove accents from accented
+ * characters and normalize a phone number. It also contains the matching logic that determines if a
+ * contact's display name matches a numeric query. The boolean variable {@link #ALLOW_INITIAL_MATCH}
+ * controls the behavior of the matching logic and determines whether we allow matches like 57 -
+ * (J)ohn (S)mith.
+ */
+public class SmartDialNameMatcher {
+
+ public static final SmartDialMap LATIN_SMART_DIAL_MAP = new LatinSmartDialMap();
+ // Whether or not we allow matches like 57 - (J)ohn (S)mith
+ private static final boolean ALLOW_INITIAL_MATCH = true;
+
+ // The maximum length of the initial we will match - typically set to 1 to minimize false
+ // positives
+ private static final int INITIAL_LENGTH_LIMIT = 1;
+
+ private final ArrayList<SmartDialMatchPosition> mMatchPositions = new ArrayList<>();
+ private final SmartDialMap mMap;
+ private String mQuery;
+ private String mNameMatchMask = "";
+ private String mPhoneNumberMatchMask = "";
+
+ // Controls whether to treat an empty query as a match (with anything).
+ private boolean mShouldMatchEmptyQuery = false;
+
+ @VisibleForTesting
+ public SmartDialNameMatcher(String query) {
+ this(query, LATIN_SMART_DIAL_MAP);
+ }
+
+ public SmartDialNameMatcher(String query, SmartDialMap map) {
+ mQuery = query;
+ mMap = map;
+ }
+
+ /**
+ * Strips a phone number of unnecessary characters (spaces, dashes, etc.)
+ *
+ * @param number Phone number we want to normalize
+ * @return Phone number consisting of digits from 0-9
+ */
+ public static String normalizeNumber(String number, SmartDialMap map) {
+ return normalizeNumber(number, 0, map);
+ }
+
+ /**
+ * Strips a phone number of unnecessary characters (spaces, dashes, etc.)
+ *
+ * @param number Phone number we want to normalize
+ * @param offset Offset to start from
+ * @return Phone number consisting of digits from 0-9
+ */
+ public static String normalizeNumber(String number, int offset, SmartDialMap map) {
+ final StringBuilder s = new StringBuilder();
+ for (int i = offset; i < number.length(); i++) {
+ char ch = number.charAt(i);
+ if (map.isValidDialpadNumericChar(ch)) {
+ s.append(ch);
+ }
+ }
+ return s.toString();
+ }
+
+ /**
+ * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means there
+ * is a match and should be highlighted in the TextView.
+ *
+ * @param builder StringBuilder object
+ * @param length Length of the desired mask.
+ */
+ private void constructEmptyMask(StringBuilder builder, int length) {
+ for (int i = 0; i < length; ++i) {
+ builder.append("0");
+ }
+ }
+
+ /**
+ * Replaces the 0-bit at a position with 1-bit, indicating that there is a match.
+ *
+ * @param builder StringBuilder object.
+ * @param matchPos Match Positions to mask as 1.
+ */
+ private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) {
+ for (int i = matchPos.start; i < matchPos.end; ++i) {
+ builder.replace(i, i + 1, "1");
+ }
+ }
+
+ /**
+ * Matches a phone number against a query. Let the test application overwrite the NANP setting.
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @param useNanp - Overwriting nanp setting boolean, used for testing.
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ @VisibleForTesting
+ @Nullable
+ public SmartDialMatchPosition matchesNumber(String phoneNumber, String query, boolean useNanp) {
+ if (TextUtils.isEmpty(phoneNumber)) {
+ return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null;
+ }
+ StringBuilder builder = new StringBuilder();
+ constructEmptyMask(builder, phoneNumber.length());
+ mPhoneNumberMatchMask = builder.toString();
+
+ // Try matching the number as is
+ SmartDialMatchPosition matchPos = matchesNumberWithOffset(phoneNumber, query, 0);
+ if (matchPos == null) {
+ final PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(phoneNumber);
+
+ if (phoneNumberTokens == null) {
+ return matchPos;
+ }
+ if (phoneNumberTokens.countryCodeOffset != 0) {
+ matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.countryCodeOffset);
+ }
+ if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0 && useNanp) {
+ matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.nanpCodeOffset);
+ }
+ }
+ if (matchPos != null) {
+ replaceBitInMask(builder, matchPos);
+ mPhoneNumberMatchMask = builder.toString();
+ }
+ return matchPos;
+ }
+
+ /**
+ * Matches a phone number against the saved query, taking care of formatting characters and also
+ * taking into account country code prefixes and special NANP number treatment.
+ *
+ * @param phoneNumber - Raw phone number
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ public SmartDialMatchPosition matchesNumber(String phoneNumber) {
+ return matchesNumber(phoneNumber, mQuery, true);
+ }
+
+ /**
+ * Matches a phone number against a query, taking care of formatting characters and also taking
+ * into account country code prefixes and special NANP number treatment.
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ public SmartDialMatchPosition matchesNumber(String phoneNumber, String query) {
+ return matchesNumber(phoneNumber, query, true);
+ }
+
+ /**
+ * Matches a phone number against a query, taking care of formatting characters
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @param offset - The position in the number to start the match against (used to ignore leading
+ * prefixes/country codes)
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ private SmartDialMatchPosition matchesNumberWithOffset(
+ String phoneNumber, String query, int offset) {
+ if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) {
+ return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null;
+ }
+ int queryAt = 0;
+ int numberAt = offset;
+ for (int i = offset; i < phoneNumber.length(); i++) {
+ if (queryAt == query.length()) {
+ break;
+ }
+ char ch = phoneNumber.charAt(i);
+ if (mMap.isValidDialpadNumericChar(ch)) {
+ if (ch != query.charAt(queryAt)) {
+ return null;
+ }
+ queryAt++;
+ } else {
+ if (queryAt == 0) {
+ // Found a separator before any part of the query was matched, so advance the
+ // offset to avoid prematurely highlighting separators before the rest of the
+ // query.
+ // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with
+ // '510'.
+ // However, if the current offset is 0, just include the beginning separators
+ // anyway, otherwise the highlighting ends up looking weird.
+ // E.g. if we're matching (510)-111-1111 with '510', we should include the
+ // first '('.
+ if (offset != 0) {
+ offset++;
+ }
+ }
+ }
+ numberAt++;
+ }
+ return new SmartDialMatchPosition(0 + offset, numberAt);
+ }
+
+ /**
+ * This function iterates through each token in the display name, trying to match the query to the
+ * numeric equivalent of the token.
+ *
+ * <p>A token is defined as a range in the display name delimited by characters that have no latin
+ * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters
+ * - '王'). Transliteration from non-latin characters to latin character will be done on a best
+ * effort basis - e.g. 'Ü' - 'u'.
+ *
+ * <p>For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips",
+ * "thomas", and "jr".
+ *
+ * <p>A match must begin at the start of a token. For example, typing 846(Tho) would match
+ * "Phillips Thomas", but 466(hom) would not.
+ *
+ * <p>Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred
+ * S)mith.
+ *
+ * @param displayName The normalized(no accented characters) display name we intend to match
+ * against.
+ * @param query The string of digits that we want to match the display name to.
+ * @param matchList An array list of {@link SmartDialMatchPosition}s that we add matched positions
+ * to.
+ * @return Returns true if a combination of the tokens in displayName match the query string
+ * contained in query. If the function returns true, matchList will contain an ArrayList of
+ * match positions (multiple matches correspond to initial matches).
+ */
+ @VisibleForTesting
+ boolean matchesCombination(
+ String displayName, String query, ArrayList<SmartDialMatchPosition> matchList) {
+ StringBuilder builder = new StringBuilder();
+ constructEmptyMask(builder, displayName.length());
+ mNameMatchMask = builder.toString();
+ final int nameLength = displayName.length();
+ final int queryLength = query.length();
+
+ if (nameLength < queryLength) {
+ return false;
+ }
+
+ if (queryLength == 0) {
+ return false;
+ }
+
+ // The current character index in displayName
+ // E.g. 3 corresponds to 'd' in "Fred Smith"
+ int nameStart = 0;
+
+ // The current character in the query we are trying to match the displayName against
+ int queryStart = 0;
+
+ // The start position of the current token we are inspecting
+ int tokenStart = 0;
+
+ // The number of non-alphabetic characters we've encountered so far in the current match.
+ // E.g. if we've currently matched 3733764849 to (Fred Smith W)illiam, then the
+ // seperatorCount should be 2. This allows us to correctly calculate offsets for the match
+ // positions
+ int seperatorCount = 0;
+
+ ArrayList<SmartDialMatchPosition> partial = new ArrayList<SmartDialMatchPosition>();
+ // Keep going until we reach the end of displayName
+ while (nameStart < nameLength && queryStart < queryLength) {
+ char ch = displayName.charAt(nameStart);
+ // Strip diacritics from accented characters if any
+ ch = mMap.normalizeCharacter(ch);
+ if (mMap.isValidDialpadCharacter(ch)) {
+ if (mMap.isValidDialpadAlphabeticChar(ch)) {
+ ch = mMap.getDialpadNumericCharacter(ch);
+ }
+ if (ch != query.charAt(queryStart)) {
+ // Failed to match the current character in the query.
+
+ // Case 1: Failed to match the first character in the query. Skip to the next
+ // token since there is no chance of this token matching the query.
+
+ // Case 2: Previous characters in the query matched, but the current character
+ // failed to match. This happened in the middle of a token. Skip to the next
+ // token since there is no chance of this token matching the query.
+
+ // Case 3: Previous characters in the query matched, but the current character
+ // failed to match. This happened right at the start of the current token. In
+ // this case, we should restart the query and try again with the current token.
+ // Otherwise, we would fail to match a query like "964"(yog) against a name
+ // Yo-Yoghurt because the query match would fail on the 3rd character, and
+ // then skip to the end of the "Yoghurt" token.
+
+ if (queryStart == 0
+ || mMap.isValidDialpadCharacter(
+ mMap.normalizeCharacter(displayName.charAt(nameStart - 1)))) {
+ // skip to the next token, in the case of 1 or 2.
+ while (nameStart < nameLength
+ && mMap.isValidDialpadCharacter(
+ mMap.normalizeCharacter(displayName.charAt(nameStart)))) {
+ nameStart++;
+ }
+ nameStart++;
+ }
+
+ // Restart the query and set the correct token position
+ queryStart = 0;
+ seperatorCount = 0;
+ tokenStart = nameStart;
+ } else {
+ if (queryStart == queryLength - 1) {
+
+ // As much as possible, we prioritize a full token match over a sub token
+ // one so if we find a full token match, we can return right away
+ matchList.add(
+ new SmartDialMatchPosition(tokenStart, queryLength + tokenStart + seperatorCount));
+ for (SmartDialMatchPosition match : matchList) {
+ replaceBitInMask(builder, match);
+ }
+ mNameMatchMask = builder.toString();
+ return true;
+ } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) {
+ // we matched the first character.
+ // branch off and see if we can find another match with the remaining
+ // characters in the query string and the remaining tokens
+ // find the next separator in the query string
+ int j;
+ for (j = nameStart; j < nameLength; j++) {
+ if (!mMap.isValidDialpadCharacter(mMap.normalizeCharacter(displayName.charAt(j)))) {
+ break;
+ }
+ }
+ // this means there is at least one character left after the separator
+ if (j < nameLength - 1) {
+ final String remainder = displayName.substring(j + 1);
+ final ArrayList<SmartDialMatchPosition> partialTemp = new ArrayList<>();
+ if (matchesCombination(remainder, query.substring(queryStart + 1), partialTemp)) {
+
+ // store the list of possible match positions
+ SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1);
+ partialTemp.add(0, new SmartDialMatchPosition(nameStart, nameStart + 1));
+ // we found a partial token match, store the data in a
+ // temp buffer and return it if we end up not finding a full
+ // token match
+ partial = partialTemp;
+ }
+ }
+ }
+ nameStart++;
+ queryStart++;
+ // we matched the current character in the name against one in the query,
+ // continue and see if the rest of the characters match
+ }
+ } else {
+ // found a separator, we skip this character and continue to the next one
+ nameStart++;
+ if (queryStart == 0) {
+ // This means we found a separator before the start of a token,
+ // so we should increment the token's start position to reflect its true
+ // start position
+ tokenStart = nameStart;
+ } else {
+ // Otherwise this separator was found in the middle of a token being matched,
+ // so increase the separator count
+ seperatorCount++;
+ }
+ }
+ }
+ // if we have no complete match at this point, then we attempt to fall back to the partial
+ // token match(if any). If we don't allow initial matching (ALLOW_INITIAL_MATCH = false)
+ // then partial will always be empty.
+ if (!partial.isEmpty()) {
+ matchList.addAll(partial);
+ for (SmartDialMatchPosition match : matchList) {
+ replaceBitInMask(builder, match);
+ }
+ mNameMatchMask = builder.toString();
+ return true;
+ }
+ return false;
+ }
+
+ public boolean matches(String displayName) {
+ mMatchPositions.clear();
+ return matchesCombination(displayName, mQuery, mMatchPositions);
+ }
+
+ public ArrayList<SmartDialMatchPosition> getMatchPositions() {
+ // Return a clone of mMatchPositions so that the caller can use it without
+ // worrying about it changing
+ return new ArrayList<SmartDialMatchPosition>(mMatchPositions);
+ }
+
+ public String getNameMatchPositionsInString() {
+ return mNameMatchMask;
+ }
+
+ public String getNumberMatchPositionsInString() {
+ return mPhoneNumberMatchMask;
+ }
+
+ public String getQuery() {
+ return mQuery;
+ }
+
+ public void setQuery(String query) {
+ mQuery = query;
+ }
+
+ public void setShouldMatchEmptyQuery(boolean matches) {
+ mShouldMatchEmptyQuery = matches;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialPrefix.java b/java/com/android/dialer/smartdial/SmartDialPrefix.java
new file mode 100644
index 0000000..a000e21
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialPrefix.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright (C) 2013 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.smartdial;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported
+ * prefix combinations for contact names, and also methods to find supported prefix combinations for
+ * contacts' phone numbers. Each contact name is separated into several tokens, such as first name,
+ * middle name, family name etc. Each phone number is also separated into country code, NANP area
+ * code, and local number if such separation is possible.
+ */
+public class SmartDialPrefix {
+
+ /**
+ * The number of starting and ending tokens in a contact's name considered for initials. For
+ * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed
+ * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced
+ * by their initials in contact name matching. Users can look up this contact by combinations of
+ * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such
+ * as "CF" "DF" "ACF" "ADF" etc.
+ */
+ private static final int LAST_TOKENS_FOR_INITIALS = 2;
+
+ private static final int FIRST_TOKENS_FOR_INITIALS = 2;
+
+ /** The country code of the user's sim card obtained by calling getSimCountryIso */
+ private static final String PREF_USER_SIM_COUNTRY_CODE =
+ "DialtactsActivity_user_sim_country_code";
+
+ private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
+ /** Dialpad mapping. */
+ private static final SmartDialMap mMap = new LatinSmartDialMap();
+
+ private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;
+ /** Indicates whether user is in NANP regions. */
+ private static boolean sUserInNanpRegion = false;
+ /** Set of country names that use NANP code. */
+ private static Set<String> sNanpCountries = null;
+ /** Set of supported country codes in front of the phone number. */
+ private static Set<String> sCountryCodes = null;
+
+ private static boolean sNanpInitialized = false;
+
+ /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */
+ public static void initializeNanpSettings(Context context) {
+ final TelephonyManager manager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (manager != null) {
+ sUserSimCountryCode = manager.getSimCountryIso();
+ }
+
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ if (sUserSimCountryCode != null) {
+ /** Updates shared preferences with the latest country obtained from getSimCountryIso. */
+ prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply();
+ } else {
+ /** Uses previously stored country code if loading fails. */
+ sUserSimCountryCode =
+ prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT);
+ }
+ /** Queries the NANP country list to find out whether user is in a NANP region. */
+ sUserInNanpRegion = isCountryNanp(sUserSimCountryCode);
+ sNanpInitialized = true;
+ }
+
+ /**
+ * Parses a contact's name into a list of separated tokens.
+ *
+ * @param contactName Contact's name stored in string.
+ * @return A list of name tokens, for example separated first names, last name, etc.
+ */
+ public static ArrayList<String> parseToIndexTokens(String contactName) {
+ final int length = contactName.length();
+ final ArrayList<String> result = new ArrayList<>();
+ char c;
+ final StringBuilder currentIndexToken = new StringBuilder();
+ /**
+ * Iterates through the whole name string. If the current character is a valid character, append
+ * it to the current token. If the current character is not a valid character, for example space
+ * " ", mark the current token as complete and add it to the list of tokens.
+ */
+ for (int i = 0; i < length; i++) {
+ c = mMap.normalizeCharacter(contactName.charAt(i));
+ if (mMap.isValidDialpadCharacter(c)) {
+ /** Converts a character into the number on dialpad that represents the character. */
+ currentIndexToken.append(mMap.getDialpadIndex(c));
+ } else {
+ if (currentIndexToken.length() != 0) {
+ result.add(currentIndexToken.toString());
+ }
+ currentIndexToken.delete(0, currentIndexToken.length());
+ }
+ }
+
+ /** Adds the last token in case it has not been added. */
+ if (currentIndexToken.length() != 0) {
+ result.add(currentIndexToken.toString());
+ }
+ return result;
+ }
+
+ /**
+ * Generates a list of strings that any prefix of any string in the list can be used to look up
+ * the contact's name.
+ *
+ * @param index The contact's name in string.
+ * @return A List of strings, whose prefix can be used to look up the contact.
+ */
+ public static ArrayList<String> generateNamePrefixes(String index) {
+ final ArrayList<String> result = new ArrayList<>();
+
+ /** Parses the name into a list of tokens. */
+ final ArrayList<String> indexTokens = parseToIndexTokens(index);
+
+ if (indexTokens.size() > 0) {
+ /**
+ * Adds the full token combinations to the list. For example, a contact with name "Albert Ben
+ * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster"
+ * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only
+ * one token, and that spans multiple continuous tokens.
+ */
+ final StringBuilder fullNameToken = new StringBuilder();
+ for (int i = indexTokens.size() - 1; i >= 0; i--) {
+ fullNameToken.insert(0, indexTokens.get(i));
+ result.add(fullNameToken.toString());
+ }
+
+ /**
+ * Adds initial combinations to the list, with the number of initials restricted by {@link
+ * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact
+ * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings
+ * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers
+ * all cases of initial lookup.
+ */
+ ArrayList<String> fullNames = new ArrayList<>();
+ fullNames.add(indexTokens.get(indexTokens.size() - 1));
+ final int recursiveNameStart = result.size();
+ int recursiveNameEnd = result.size();
+ String initial = "";
+ for (int i = indexTokens.size() - 2; i >= 0; i--) {
+ if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS)
+ || (i < FIRST_TOKENS_FOR_INITIALS)) {
+ initial = indexTokens.get(i).substring(0, 1);
+
+ /** Recursively adds initial combinations to the list. */
+ for (int j = 0; j < fullNames.size(); ++j) {
+ result.add(initial + fullNames.get(j));
+ }
+ for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) {
+ result.add(initial + result.get(j));
+ }
+ recursiveNameEnd = result.size();
+ final String currentFullName = fullNames.get(fullNames.size() - 1);
+ fullNames.add(indexTokens.get(i) + currentFullName);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Computes a list of number strings based on tokens of a given phone number. Any prefix of any
+ * string in the list can be used to look up the phone number. The list include the full phone
+ * number, the national number if there is a country code in the phone number, and the local
+ * number if there is an area code in the phone number following the NANP format. For example, if
+ * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any
+ * prefix to either of the strings can be used to look up the phone number. If a user has a phone
+ * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and
+ * 3023029.
+ *
+ * @param number String of user's phone number.
+ * @return A list of strings where any prefix of any entry can be used to look up the number.
+ */
+ public static ArrayList<String> parseToNumberTokens(String number) {
+ final ArrayList<String> result = new ArrayList<>();
+ if (!TextUtils.isEmpty(number)) {
+ /** Adds the full number to the list. */
+ result.add(SmartDialNameMatcher.normalizeNumber(number, mMap));
+
+ final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(number);
+ if (phoneNumberTokens == null) {
+ return result;
+ }
+
+ if (phoneNumberTokens.countryCodeOffset != 0) {
+ result.add(
+ SmartDialNameMatcher.normalizeNumber(
+ number, phoneNumberTokens.countryCodeOffset, mMap));
+ }
+
+ if (phoneNumberTokens.nanpCodeOffset != 0) {
+ result.add(
+ SmartDialNameMatcher.normalizeNumber(number, phoneNumberTokens.nanpCodeOffset, mMap));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Parses a phone number to find out whether it has country code and NANP area code.
+ *
+ * @param number Raw phone number.
+ * @return a PhoneNumberToken instance with country code, NANP code information.
+ */
+ public static PhoneNumberTokens parsePhoneNumber(String number) {
+ String countryCode = "";
+ int countryCodeOffset = 0;
+ int nanpNumberOffset = 0;
+
+ if (!TextUtils.isEmpty(number)) {
+ String normalizedNumber = SmartDialNameMatcher.normalizeNumber(number, mMap);
+ if (number.charAt(0) == '+') {
+ /** If the number starts with '+', tries to find valid country code. */
+ for (int i = 1; i <= 1 + 3; i++) {
+ if (number.length() <= i) {
+ break;
+ }
+ countryCode = number.substring(1, i);
+ if (isValidCountryCode(countryCode)) {
+ countryCodeOffset = i;
+ break;
+ }
+ }
+ } else {
+ /**
+ * If the number does not start with '+', finds out whether it is in NANP format and has '1'
+ * preceding the number.
+ */
+ if ((normalizedNumber.length() == 11)
+ && (normalizedNumber.charAt(0) == '1')
+ && (sUserInNanpRegion)) {
+ countryCode = "1";
+ countryCodeOffset = number.indexOf(normalizedNumber.charAt(1));
+ if (countryCodeOffset == -1) {
+ countryCodeOffset = 0;
+ }
+ }
+ }
+
+ /** If user is in NANP region, finds out whether a number is in NANP format. */
+ if (sUserInNanpRegion) {
+ String areaCode = "";
+ if (countryCode.equals("") && normalizedNumber.length() == 10) {
+ /**
+ * if the number has no country code but fits the NANP format, extracts the NANP area
+ * code, and finds out offset of the local number.
+ */
+ areaCode = normalizedNumber.substring(0, 3);
+ } else if (countryCode.equals("1") && normalizedNumber.length() == 11) {
+ /**
+ * If the number has country code '1', finds out area code and offset of the local number.
+ */
+ areaCode = normalizedNumber.substring(1, 4);
+ }
+ if (!areaCode.equals("")) {
+ final int areaCodeIndex = number.indexOf(areaCode);
+ if (areaCodeIndex != -1) {
+ nanpNumberOffset = number.indexOf(areaCode) + 3;
+ }
+ }
+ }
+ }
+ return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset);
+ }
+
+ /** Checkes whether a country code is valid. */
+ private static boolean isValidCountryCode(String countryCode) {
+ if (sCountryCodes == null) {
+ sCountryCodes = initCountryCodes();
+ }
+ return sCountryCodes.contains(countryCode);
+ }
+
+ private static Set<String> initCountryCodes() {
+ final HashSet<String> result = new HashSet<String>();
+ result.add("1");
+ result.add("7");
+ result.add("20");
+ result.add("27");
+ result.add("30");
+ result.add("31");
+ result.add("32");
+ result.add("33");
+ result.add("34");
+ result.add("36");
+ result.add("39");
+ result.add("40");
+ result.add("41");
+ result.add("43");
+ result.add("44");
+ result.add("45");
+ result.add("46");
+ result.add("47");
+ result.add("48");
+ result.add("49");
+ result.add("51");
+ result.add("52");
+ result.add("53");
+ result.add("54");
+ result.add("55");
+ result.add("56");
+ result.add("57");
+ result.add("58");
+ result.add("60");
+ result.add("61");
+ result.add("62");
+ result.add("63");
+ result.add("64");
+ result.add("65");
+ result.add("66");
+ result.add("81");
+ result.add("82");
+ result.add("84");
+ result.add("86");
+ result.add("90");
+ result.add("91");
+ result.add("92");
+ result.add("93");
+ result.add("94");
+ result.add("95");
+ result.add("98");
+ result.add("211");
+ result.add("212");
+ result.add("213");
+ result.add("216");
+ result.add("218");
+ result.add("220");
+ result.add("221");
+ result.add("222");
+ result.add("223");
+ result.add("224");
+ result.add("225");
+ result.add("226");
+ result.add("227");
+ result.add("228");
+ result.add("229");
+ result.add("230");
+ result.add("231");
+ result.add("232");
+ result.add("233");
+ result.add("234");
+ result.add("235");
+ result.add("236");
+ result.add("237");
+ result.add("238");
+ result.add("239");
+ result.add("240");
+ result.add("241");
+ result.add("242");
+ result.add("243");
+ result.add("244");
+ result.add("245");
+ result.add("246");
+ result.add("247");
+ result.add("248");
+ result.add("249");
+ result.add("250");
+ result.add("251");
+ result.add("252");
+ result.add("253");
+ result.add("254");
+ result.add("255");
+ result.add("256");
+ result.add("257");
+ result.add("258");
+ result.add("260");
+ result.add("261");
+ result.add("262");
+ result.add("263");
+ result.add("264");
+ result.add("265");
+ result.add("266");
+ result.add("267");
+ result.add("268");
+ result.add("269");
+ result.add("290");
+ result.add("291");
+ result.add("297");
+ result.add("298");
+ result.add("299");
+ result.add("350");
+ result.add("351");
+ result.add("352");
+ result.add("353");
+ result.add("354");
+ result.add("355");
+ result.add("356");
+ result.add("357");
+ result.add("358");
+ result.add("359");
+ result.add("370");
+ result.add("371");
+ result.add("372");
+ result.add("373");
+ result.add("374");
+ result.add("375");
+ result.add("376");
+ result.add("377");
+ result.add("378");
+ result.add("379");
+ result.add("380");
+ result.add("381");
+ result.add("382");
+ result.add("385");
+ result.add("386");
+ result.add("387");
+ result.add("389");
+ result.add("420");
+ result.add("421");
+ result.add("423");
+ result.add("500");
+ result.add("501");
+ result.add("502");
+ result.add("503");
+ result.add("504");
+ result.add("505");
+ result.add("506");
+ result.add("507");
+ result.add("508");
+ result.add("509");
+ result.add("590");
+ result.add("591");
+ result.add("592");
+ result.add("593");
+ result.add("594");
+ result.add("595");
+ result.add("596");
+ result.add("597");
+ result.add("598");
+ result.add("599");
+ result.add("670");
+ result.add("672");
+ result.add("673");
+ result.add("674");
+ result.add("675");
+ result.add("676");
+ result.add("677");
+ result.add("678");
+ result.add("679");
+ result.add("680");
+ result.add("681");
+ result.add("682");
+ result.add("683");
+ result.add("685");
+ result.add("686");
+ result.add("687");
+ result.add("688");
+ result.add("689");
+ result.add("690");
+ result.add("691");
+ result.add("692");
+ result.add("800");
+ result.add("808");
+ result.add("850");
+ result.add("852");
+ result.add("853");
+ result.add("855");
+ result.add("856");
+ result.add("870");
+ result.add("878");
+ result.add("880");
+ result.add("881");
+ result.add("882");
+ result.add("883");
+ result.add("886");
+ result.add("888");
+ result.add("960");
+ result.add("961");
+ result.add("962");
+ result.add("963");
+ result.add("964");
+ result.add("965");
+ result.add("966");
+ result.add("967");
+ result.add("968");
+ result.add("970");
+ result.add("971");
+ result.add("972");
+ result.add("973");
+ result.add("974");
+ result.add("975");
+ result.add("976");
+ result.add("977");
+ result.add("979");
+ result.add("992");
+ result.add("993");
+ result.add("994");
+ result.add("995");
+ result.add("996");
+ result.add("998");
+ return result;
+ }
+
+ public static SmartDialMap getMap() {
+ return mMap;
+ }
+
+ /**
+ * Indicates whether the given country uses NANP numbers
+ *
+ * @param country ISO 3166 country code (case doesn't matter)
+ * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
+ * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan">
+ * https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a>
+ */
+ @VisibleForTesting
+ public static boolean isCountryNanp(String country) {
+ if (TextUtils.isEmpty(country)) {
+ return false;
+ }
+ if (sNanpCountries == null) {
+ sNanpCountries = initNanpCountries();
+ }
+ return sNanpCountries.contains(country.toUpperCase());
+ }
+
+ private static Set<String> initNanpCountries() {
+ final HashSet<String> result = new HashSet<String>();
+ result.add("US"); // United States
+ result.add("CA"); // Canada
+ result.add("AS"); // American Samoa
+ result.add("AI"); // Anguilla
+ result.add("AG"); // Antigua and Barbuda
+ result.add("BS"); // Bahamas
+ result.add("BB"); // Barbados
+ result.add("BM"); // Bermuda
+ result.add("VG"); // British Virgin Islands
+ result.add("KY"); // Cayman Islands
+ result.add("DM"); // Dominica
+ result.add("DO"); // Dominican Republic
+ result.add("GD"); // Grenada
+ result.add("GU"); // Guam
+ result.add("JM"); // Jamaica
+ result.add("PR"); // Puerto Rico
+ result.add("MS"); // Montserrat
+ result.add("MP"); // Northern Mariana Islands
+ result.add("KN"); // Saint Kitts and Nevis
+ result.add("LC"); // Saint Lucia
+ result.add("VC"); // Saint Vincent and the Grenadines
+ result.add("TT"); // Trinidad and Tobago
+ result.add("TC"); // Turks and Caicos Islands
+ result.add("VI"); // U.S. Virgin Islands
+ return result;
+ }
+
+ /**
+ * Returns whether the user is in a region that uses Nanp format based on the sim location.
+ *
+ * @return Whether user is in Nanp region.
+ */
+ public static boolean getUserInNanpRegion() {
+ return sUserInNanpRegion;
+ }
+
+ /** Explicitly setting the user Nanp to the given boolean */
+ @VisibleForTesting
+ public static void setUserInNanpRegion(boolean userInNanpRegion) {
+ sUserInNanpRegion = userInNanpRegion;
+ }
+
+ /** Class to record phone number parsing information. */
+ public static class PhoneNumberTokens {
+
+ /** Country code of the phone number. */
+ final String countryCode;
+
+ /** Offset of national number after the country code. */
+ final int countryCodeOffset;
+
+ /** Offset of local number after NANP area code. */
+ final int nanpCodeOffset;
+
+ public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) {
+ this.countryCode = countryCode;
+ this.countryCodeOffset = countryCodeOffset;
+ this.nanpCodeOffset = nanpCodeOffset;
+ }
+ }
+}
diff --git a/java/com/android/dialer/spam/Spam.java b/java/com/android/dialer/spam/Spam.java
new file mode 100644
index 0000000..692a1a0
--- /dev/null
+++ b/java/com/android/dialer/spam/Spam.java
@@ -0,0 +1,49 @@
+/*
+ * 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.spam;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Accessor for the spam bindings. */
+public class Spam {
+
+ private static SpamBindings spamBindings;
+
+ private Spam() {}
+
+ public static SpamBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (spamBindings != null) {
+ return spamBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof SpamBindingsFactory) {
+ spamBindings = ((SpamBindingsFactory) application).newSpamBindings();
+ }
+
+ if (spamBindings == null) {
+ spamBindings = new SpamBindingsStub();
+ }
+ return spamBindings;
+ }
+
+ public static void setForTesting(SpamBindings spamBindings) {
+ Spam.spamBindings = spamBindings;
+ }
+}
diff --git a/java/com/android/dialer/spam/SpamBindings.java b/java/com/android/dialer/spam/SpamBindings.java
new file mode 100644
index 0000000..b5d18b8
--- /dev/null
+++ b/java/com/android/dialer/spam/SpamBindings.java
@@ -0,0 +1,146 @@
+/*
+ * 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.spam;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/** Allows the container application to mark calls as spam. */
+public interface SpamBindings {
+
+ boolean isSpamEnabled();
+
+ boolean isSpamNotificationEnabled();
+
+ boolean isDialogEnabledForSpamNotification();
+
+ boolean isDialogReportSpamCheckedByDefault();
+
+ /** @return what percentage of aftercall notifications to show to the user */
+ int percentOfSpamNotificationsToShow();
+
+ int percentOfNonSpamNotificationsToShow();
+
+ /**
+ * Checks if the given number is suspected of being a spamer.
+ *
+ * @param number The phone number of the call.
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkSpamStatus(String number, String countryIso, Listener listener);
+
+ /**
+ * @param number The number to check if the number is in the user's white list (non spam list)
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkUserMarkedNonSpamStatus(
+ String number, @Nullable String countryIso, @NonNull Listener listener);
+
+ /**
+ * @param number The number to check if it is in user's spam list
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkUserMarkedSpamStatus(
+ String number, @Nullable String countryIso, @NonNull Listener listener);
+
+ /**
+ * @param number The number to check if it is in the global spam list
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkGlobalSpamListStatus(
+ String number, @Nullable String countryIso, @NonNull Listener listener);
+
+ /**
+ * Synchronously checks if the given number is suspected of being a spamer.
+ *
+ * @param number The phone number of the call.
+ * @param countryIso The country ISO of the call.
+ * @return True if the number is spam.
+ */
+ boolean checkSpamStatusSynchronous(String number, String countryIso);
+
+ /**
+ * Reports number as spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactLookupResultType The result of the contact lookup for this phone number. Must be
+ * one of {@link com.android.dialer.logging.nano.ContactLookupResult}.
+ */
+ void reportSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType);
+
+ /**
+ * Reports number as spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactSourceType If we have cached contact information for the phone number, this
+ * indicates its source. Must be one of {@link com.android.dialer.logging.nano.ContactSource}.
+ */
+ void reportSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType);
+
+ /**
+ * Reports number as not spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactLookupResultType The result of the contact lookup for this phone number. Must be
+ * one of {@link com.android.dialer.logging.nano.ContactLookupResult}.
+ */
+ void reportNotSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType);
+
+ /**
+ * Reports number as not spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactSourceType If we have cached contact information for the phone number, this
+ * indicates its source. Must be one of {@link com.android.dialer.logging.nano.ContactSource}.
+ */
+ void reportNotSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType);
+
+ /** Callback to be invoked when data is fetched. */
+ interface Listener {
+
+ /** Called when data is fetched. */
+ void onComplete(boolean isSpam);
+ }
+}
diff --git a/java/com/android/dialer/spam/SpamBindingsFactory.java b/java/com/android/dialer/spam/SpamBindingsFactory.java
new file mode 100644
index 0000000..41144e1
--- /dev/null
+++ b/java/com/android/dialer/spam/SpamBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.spam;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows this module to get
+ * references to the SpamBindings.
+ */
+public interface SpamBindingsFactory {
+
+ SpamBindings newSpamBindings();
+}
diff --git a/java/com/android/dialer/spam/SpamBindingsStub.java b/java/com/android/dialer/spam/SpamBindingsStub.java
new file mode 100644
index 0000000..0893953
--- /dev/null
+++ b/java/com/android/dialer/spam/SpamBindingsStub.java
@@ -0,0 +1,92 @@
+/*
+ * 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.spam;
+
+/** Default implementation of SpamBindings. */
+public class SpamBindingsStub implements SpamBindings {
+
+ @Override
+ public boolean isSpamEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isSpamNotificationEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDialogEnabledForSpamNotification() {
+ return false;
+ }
+
+ @Override
+ public boolean isDialogReportSpamCheckedByDefault() {
+ return false;
+ }
+
+ @Override
+ public int percentOfSpamNotificationsToShow() {
+ return 0;
+ }
+
+ @Override
+ public int percentOfNonSpamNotificationsToShow() {
+ return 0;
+ }
+
+ @Override
+ public void checkSpamStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public void checkUserMarkedNonSpamStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public void checkUserMarkedSpamStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public void checkGlobalSpamListStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public boolean checkSpamStatusSynchronous(String number, String countryIso) {
+ return false;
+ }
+
+ @Override
+ public void reportSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType) {}
+
+ @Override
+ public void reportSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType) {}
+
+ @Override
+ public void reportNotSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType) {}
+
+ @Override
+ public void reportNotSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType) {}
+}
diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java
new file mode 100644
index 0000000..a11e7f7
--- /dev/null
+++ b/java/com/android/dialer/telecom/TelecomUtil.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2015 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.telecom;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Performs permission checks before calling into TelecomManager. Each method is self-explanatory -
+ * perform the required check and return the fallback default if the permission is missing,
+ * otherwise return the value from TelecomManager.
+ */
+public class TelecomUtil {
+
+ private static final String TAG = "TelecomUtil";
+ private static boolean sWarningLogged = false;
+
+ public static void showInCallScreen(Context context, boolean showDialpad) {
+ if (hasReadPhoneStatePermission(context)) {
+ try {
+ getTelecomManager(context).showInCallScreen(showDialpad);
+ } catch (SecurityException e) {
+ // Just in case
+ Log.w(TAG, "TelecomManager.showInCallScreen called without permission.");
+ }
+ }
+ }
+
+ public static void silenceRinger(Context context) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ getTelecomManager(context).silenceRinger();
+ } catch (SecurityException e) {
+ // Just in case
+ Log.w(TAG, "TelecomManager.silenceRinger called without permission.");
+ }
+ }
+ }
+
+ public static void cancelMissedCallsNotification(Context context) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ getTelecomManager(context).cancelMissedCallsNotification();
+ } catch (SecurityException e) {
+ Log.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
+ }
+ }
+ }
+
+ public static Uri getAdnUriForPhoneAccount(Context context, PhoneAccountHandle handle) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ return getTelecomManager(context).getAdnUriForPhoneAccount(handle);
+ } catch (SecurityException e) {
+ Log.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
+ }
+ }
+ return null;
+ }
+
+ public static boolean handleMmi(
+ Context context, String dialString, @Nullable PhoneAccountHandle handle) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ if (handle == null) {
+ return getTelecomManager(context).handleMmi(dialString);
+ } else {
+ return getTelecomManager(context).handleMmi(dialString, handle);
+ }
+ } catch (SecurityException e) {
+ Log.w(TAG, "TelecomManager.handleMmi called without permission.");
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
+ Context context, String uriScheme) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).getDefaultOutgoingPhoneAccount(uriScheme);
+ }
+ return null;
+ }
+
+ public static PhoneAccount getPhoneAccount(Context context, PhoneAccountHandle handle) {
+ return getTelecomManager(context).getPhoneAccount(handle);
+ }
+
+ public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).getCallCapablePhoneAccounts();
+ }
+ return new ArrayList<>();
+ }
+
+ public static boolean isInCall(Context context) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).isInCall();
+ }
+ return false;
+ }
+
+ public static boolean isVoicemailNumber(
+ Context context, PhoneAccountHandle accountHandle, String number) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).isVoiceMailNumber(accountHandle, number);
+ }
+ return false;
+ }
+
+ @Nullable
+ public static String getVoicemailNumber(Context context, PhoneAccountHandle accountHandle) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).getVoiceMailNumber(accountHandle);
+ }
+ return null;
+ }
+
+ /**
+ * Tries to place a call using the {@link TelecomManager}.
+ *
+ * @param context context.
+ * @param intent the call intent.
+ * @return {@code true} if we successfully attempted to place the call, {@code false} if it failed
+ * due to a permission check.
+ */
+ public static boolean placeCall(Context context, Intent intent) {
+ if (hasCallPhonePermission(context)) {
+ getTelecomManager(context).placeCall(intent.getData(), intent.getExtras());
+ return true;
+ }
+ return false;
+ }
+
+ public static Uri getCallLogUri(Context context) {
+ return hasReadWriteVoicemailPermissions(context)
+ ? Calls.CONTENT_URI_WITH_VOICEMAIL
+ : Calls.CONTENT_URI;
+ }
+
+ public static boolean hasReadWriteVoicemailPermissions(Context context) {
+ return isDefaultDialer(context)
+ || (hasPermission(context, Manifest.permission.READ_VOICEMAIL)
+ && hasPermission(context, Manifest.permission.WRITE_VOICEMAIL));
+ }
+
+ public static boolean hasModifyPhoneStatePermission(Context context) {
+ return isDefaultDialer(context)
+ || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE);
+ }
+
+ public static boolean hasReadPhoneStatePermission(Context context) {
+ return isDefaultDialer(context) || hasPermission(context, Manifest.permission.READ_PHONE_STATE);
+ }
+
+ public static boolean hasCallPhonePermission(Context context) {
+ return isDefaultDialer(context) || hasPermission(context, Manifest.permission.CALL_PHONE);
+ }
+
+ private static boolean hasPermission(Context context, String permission) {
+ return ContextCompat.checkSelfPermission(context, permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static boolean isDefaultDialer(Context context) {
+ final boolean result =
+ TextUtils.equals(
+ context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage());
+ if (result) {
+ sWarningLogged = false;
+ } else {
+ if (!sWarningLogged) {
+ // Log only once to prevent spam.
+ Log.w(TAG, "Dialer is not currently set to be default dialer");
+ sWarningLogged = true;
+ }
+ }
+ return result;
+ }
+
+ private static TelecomManager getTelecomManager(Context context) {
+ return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ }
+}
diff --git a/java/com/android/dialer/theme/AndroidManifest.xml b/java/com/android/dialer/theme/AndroidManifest.xml
new file mode 100644
index 0000000..7c1e4ef
--- /dev/null
+++ b/java/com/android/dialer/theme/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.theme">
+</manifest>
diff --git a/java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml b/java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml
new file mode 100644
index 0000000..3098645
--- /dev/null
+++ b/java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <set
+ android:ordering="sequentially">
+ <objectAnimator
+ android:duration="500"
+ android:propertyName="rotation"
+ android:valueFrom="0.0"
+ android:valueTo="-180.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ </set>
+</set>
\ No newline at end of file
diff --git a/java/com/android/dialer/theme/res/animator/activated_button_elevation.xml b/java/com/android/dialer/theme/res/animator/activated_button_elevation.xml
new file mode 100644
index 0000000..b8ea4e8
--- /dev/null
+++ b/java/com/android/dialer/theme/res/animator/activated_button_elevation.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_activated="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="0dp"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="4dp"
+ android:valueTo="0dp"
+ android:valueType="floatType"/>
+ </item>
+</selector>
diff --git a/java/com/android/dialer/theme/res/animator/button_elevation.xml b/java/com/android/dialer/theme/res/animator/button_elevation.xml
new file mode 100644
index 0000000..8dd019e
--- /dev/null
+++ b/java/com/android/dialer/theme/res/animator/button_elevation.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_pressed="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="0dp"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="4dp"
+ android:valueTo="0dp"
+ android:valueType="floatType"/>
+ </item>
+</selector>
diff --git a/java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml b/java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml
new file mode 100644
index 0000000..2dc3eb1
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="front_back_switch_button"
+ android:width="56dp"
+ android:viewportWidth="56"
+ android:height="56dp"
+ android:viewportHeight="56">
+ <group
+ android:name="layer_3_outlines"
+ android:translateX="32.0015"
+ android:translateY="27.00208">
+ <group
+ android:name="layer_3_outlines_pivot"
+ android:translateX="-4.25"
+ android:translateY="-7.25">
+ <group
+ android:name="group_1"
+ android:translateX="4.25"
+ android:translateY="7.25001">
+ <path
+ android:name="path_1"
+ android:pathData="M 2.0,-5.0 c 0.0,0.0 -2.16999816895,0.0 -2.16999816895,0.0 c 0.0,0.0 -1.83000183105,-2.0 -1.83000183105,-2.0 c 0.0,0.0 -2.0,0.0 -2.0,0.0 c 0.0,0.0 0.0,5.0 0.0,5.0 c 1.65600585938,0.0 3.0,1.34399414062 3.0,3.0 c 0.0,1.65600585938 -1.34399414062,3.0 -3.0,3.0 c 0.0,0.0 -3.0,0.00181579589844 -3.0,0.00181579589844 c 0.0,0.0 0.001953125,2.99415588379 0.001953125,2.99415588379 c 0.0,0.0 2.998046875,0.0040283203125 2.998046875,0.0040283203125 c 0.0,0.0 6.0,0.0 6.0,0.0 c 1.10000610352,0.0 2.0,-0.899993896484 2.0,-2.0 c 0.0,0.0 0.0,-8.0 0.0,-8.0 c 0.0,-1.10000610352 -0.899993896484,-2.0 -2.0,-2.0 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="layer_1_outlines"
+ android:translateX="24.00099"
+ android:translateY="26.99992">
+ <group
+ android:name="layer_1_outlines_pivot"
+ android:translateX="-4.249"
+ android:translateY="-7.25">
+ <group
+ android:name="group_2"
+ android:translateX="4.249"
+ android:translateY="7.25">
+ <path
+ android:name="path_2"
+ android:pathData="M 3.99900817871,4.0 c -1.65501403809,-0.00099182128906 -2.99800109863,-1.34399414062 -2.99800109863,-3.0 c 0.0,-1.6549987793 1.34298706055,-2.99899291992 2.99800109863,-3.0 c 0.0,0.0 1.0,0.00008 1.0,0.00008 c 0.0,0.0 0.0,-5.0 0.0,-5.0 c 0.0,0.0 -1.0,-0.00008 -1.0,-0.00008 c 0.0,0.0 -1.99800109863,0.0 -1.99800109863,0.0 c 0.0,0.0 -1.83000183105,2.0 -1.83000183105,2.0 c 0.0,0.0 -2.17001342773,0.0 -2.17001342773,0.0 c -1.1009979248,0.0 -2.0,0.899993896484 -2.0,2.0 c 0.0,0.0 0.0,8.0 0.0,8.0 c 0.0,1.10000610352 0.899002075195,2.0 2.0,2.0 c 0.0,0.0 5.99801635742,0.0 5.99801635742,0.0 c 0.0,0.0 0.0,-3.0 0.0,-3.0 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="layer_10_outlines"
+ android:translateX="28.00001"
+ android:translateY="27.99999">
+ <group
+ android:name="layer_10_outlines_pivot"
+ android:translateX="-19.25"
+ android:translateY="-19.25005">
+ <group
+ android:name="group_3"
+ android:translateX="20.25"
+ android:translateY="9.75001">
+ <path
+ android:name="path_3"
+ android:pathData="M 12.4349975586,-3.93499755859 c -3.70999145508,-3.71000671387 -8.57299804688,-5.56500244141 -13.4349975586,-5.56500244141 c -4.8630065918,0.0 -9.72500610352,1.85499572754 -13.4349975586,5.56500244141 c -0.337005615234,0.336990356445 -0.652008056641,0.688003540039 -0.956008911133,1.04399108887 c 0.0,0.0 -2.60899353027,-2.60899353027 -2.60899353027,-2.60899353027 c 0.0,0.0 0.0,7.0 0.0,7.0 c 0.0,0.0 7.0,0.0 7.0,0.0 c 0.0,0.0 -2.97300720215,-2.97300720215 -2.97300720215,-2.97300720215 c 0.300003051758,-0.360992431641 0.616012573242,-0.711990356445 0.952011108398,-1.0479888916 c 3.21099853516,-3.21099853516 7.47999572754,-4.97900390625 12.0209960938,-4.97900390625 c 4.54100036621,0.0 8.80999755859,1.76800537109 12.0209960938,4.97900390625 c 3.31401062012,3.31399536133 4.97100830078,7.66799926758 4.97100830078,12.0209960938 c 0.0,0.0 2.00799560547,0.0 2.00799560547,0.0 c 0.0,-4.86199951172 -1.85499572754,-9.72500610352 -5.56500244141,-13.4349975586 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ <group
+ android:name="group_4"
+ android:translateX="18.25"
+ android:translateY="28.75011">
+ <path
+ android:name="path_4"
+ android:pathData="M 18.0,-1.5 c 0.0,0.0 -7.0,0.0 -7.0,0.0 c 0.0,0.0 2.97300720215,2.97300720215 2.97300720215,2.97300720215 c -0.300003051758,0.360992431641 -0.616012573242,0.711990356445 -0.952011108398,1.0479888916 c -3.21099853516,3.21099853516 -7.47999572754,4.97900390625 -12.0209960938,4.97900390625 c -4.54100036621,0.0 -8.80999755859,-1.76800537109 -12.0209960938,-4.97900390625 c -3.31401062012,-3.31399536133 -4.97100830078,-7.66799926758 -4.97100830078,-12.0209960938 c 0.0,0.0 -2.00799560547,0.0 -2.00799560547,0.0 c 0.0,4.86199951172 1.85499572754,9.72500610352 5.56500244141,13.4349975586 c 3.70999145508,3.71000671387 8.57299804688,5.56500244141 13.4349975586,5.56500244141 c 4.8630065918,0.0 9.72500610352,-1.85499572754 13.4349975586,-5.56500244141 c 0.337005615234,-0.336990356445 0.652008056641,-0.688003540039 0.956008911133,-1.04400634766 c 0.0,0.0 2.60899353027,2.60900878906 2.60899353027,2.60900878906 c 0.0,0.0 0.0,-7.0 0.0,-7.0 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ </group>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml b/java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml
new file mode 100644
index 0000000..14cda1b
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<animated-vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/front_back_switch_button">
+ <target
+ android:name="layer_10_outlines"
+ android:animation="@anim/front_back_switch_button_animation"/>
+</animated-vector>
\ No newline at end of file
diff --git a/java/com/android/dialer/theme/res/values/colors.xml b/java/com/android/dialer/theme/res/values/colors.xml
new file mode 100644
index 0000000..bf43e01
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/colors.xml
@@ -0,0 +1,64 @@
+<!--
+ ~ Copyright (C) 2012 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>
+ <!-- Note: The following colors are used in the Dialer settings screens. Since Dialer's settings
+ link into the Telephony settings as well, changes to these colors should be mirrored in
+ Telephony:
+
+ Android source path: packages/apps/PhoneCommon/res/values/colors.xml
+ - Local: dialer_theme_color Android Source: dialer_theme_color
+ - Local: dialer_theme_color_dark Android Source: dialer_theme_color_dark
+ Android source path: packages/services/Telecomm/res/values/colors.xml
+ - Local: dialer_theme_color Android Source: theme_color
+ - Local: dialer_theme_color_dark Android Source: dialer_settings_color_dark
+ -->
+ <color name="dialer_theme_color">#2A56C6</color>
+ <color name="dialer_theme_color_dark">#1C3AA9</color>
+
+ <color name="dialer_snackbar_action_text_color">#4285F4</color>
+ <color name="dialer_theme_color_20pct">#332A56C6</color>
+
+ <color name="dialer_secondary_color">#e91e63</color>
+
+ <!-- Primary text color in the Phone app -->
+ <color name="dialer_primary_text_color">#333333</color>
+ <color name="dialer_edit_text_hint_color">#DE78909C</color>
+
+ <!-- Secondary text color in the Phone app -->
+ <color name="dialer_secondary_text_color">#636363</color>
+
+ <!-- Color of the theme of the Dialer app -->
+ <color name="dialtacts_theme_color">@color/dialer_theme_color</color>
+
+ <!-- White background for dialer -->
+ <color name="background_dialer_white">#ffffff</color>
+ <color name="background_dialer_call_log_list_item">@color/background_dialer_white</color>
+
+ <!-- Colors for the notification actions -->
+ <color name="notification_action_accept">#097138</color>
+ <color name="notification_action_dismiss">#A52714</color>
+ <color name="notification_action_end_call">#FFFFFF</color>
+ <color name="notification_action_answer_video">#097138</color>
+
+ <!-- Background color of action bars -->
+ <color name="actionbar_background_color">@color/dialer_theme_color</color>
+
+ <!-- Background color of title bars in recents -->
+ <color name="titlebar_in_recents_background_color">@color/dialer_theme_color_dark</color>
+
+ <color name="blue_grey_100">#CFD8DC</color>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/dimens.xml b/java/com/android/dialer/theme/res/values/dimens.xml
new file mode 100644
index 0000000..2d11ecc
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/dimens.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="call_log_action_icon_margin_start">16dp</dimen>
+ <dimen name="call_log_action_icon_dimen">24dp</dimen>
+ <dimen name="call_log_action_horizontal_padding">24dp</dimen>
+
+ <dimen name="call_log_actions_left_padding">64dp</dimen>
+ <dimen name="call_log_actions_top_padding">8dp</dimen>
+ <dimen name="call_log_actions_bottom_padding">8dp</dimen>
+ <dimen name="call_log_primary_text_size">16sp</dimen>
+ <dimen name="call_log_detail_text_size">12sp</dimen>
+ <dimen name="call_log_day_group_heading_size">14sp</dimen>
+ <dimen name="call_log_voicemail_transcription_text_size">14sp</dimen>
+ <!-- Height of the call log actions section for each call log entry -->
+ <dimen name="call_log_action_height">48dp</dimen>
+ <dimen name="call_log_day_group_padding_top">15dp</dimen>
+ <dimen name="call_log_day_group_padding_bottom">9dp</dimen>
+
+ <!-- Height of the actionBar - this is 8dps bigger than the platform standard to give more
+ room to the search box-->
+ <dimen name="action_bar_height">56dp</dimen>
+ <dimen name="action_bar_height_large">64dp</dimen>
+ <dimen name="action_bar_elevation">3dp</dimen>
+ <dimen name="tab_height">48dp</dimen>
+ <!-- actionbar height + tab height -->
+ <dimen name="actionbar_and_tab_height">107dp</dimen>
+ <dimen name="actionbar_contentInsetStart">72dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/strings.xml b/java/com/android/dialer/theme/res/values/strings.xml
new file mode 100644
index 0000000..3a954ae
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/strings.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ 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>
+ <!-- String used to display calls from unknown numbers in the call log -->
+ <string name="unknown">Unknown</string>
+
+ <!-- String used to display calls from pay phone in the call log -->
+ <string name="payphone">Payphone</string>
+
+ <!-- Title for the activity that dials the phone. This is the name
+ used in the Launcher icon. -->
+ <string name="launcherActivityLabel">Phone</string>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/styles.xml b/java/com/android/dialer/theme/res/values/styles.xml
new file mode 100644
index 0000000..ac94d06
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/styles.xml
@@ -0,0 +1,56 @@
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <style name="CallLogCardStyle" parent="CardView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_margin">4dp</item>
+ <item name="android:baselineAligned">false</item>
+ <item name="cardCornerRadius">2dp</item>
+ <item name="cardBackgroundColor">@color/background_dialer_call_log_list_item</item>
+ </style>
+
+ <!-- Inherit from Theme.Material.Light.Dialog instead of Theme.Material.Light.Dialog.Alert
+ since the Alert dialog is private. They are identical anyway. -->
+ <style name="AlertDialogTheme" parent="@android:style/Theme.Material.Light.Dialog">
+ <item name="android:colorAccent">@color/dialtacts_theme_color</item>
+ </style>
+
+ <style name="TextActionStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">@dimen/call_log_action_height</item>
+ <item name="android:gravity">end|center_vertical</item>
+ <item name="android:paddingStart">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:textColor">@color/dialtacts_theme_color</item>
+ <item name="android:fontFamily">"sans-serif-medium"</item>
+ <item name="android:focusable">true</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+ <style name="DialerButtonTextStyle" parent="@android:style/TextAppearance.Material.Widget.Button">
+ <item name="android:textColor">#fff</item>
+ </style>
+
+ <style name="DialerActionBarBaseStyle"
+ parent="@style/Widget.AppCompat.Light.ActionBar.Solid.Inverse">
+ <item name="android:background">@color/actionbar_background_color</item>
+ <item name="background">@color/actionbar_background_color</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/themes.xml b/java/com/android/dialer/theme/res/values/themes.xml
new file mode 100644
index 0000000..452b369
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/themes.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="DialerThemeBase" parent="@style/Theme.AppCompat.Light.DarkActionBar">
+ <item name="android:textColorPrimary">@color/dialer_primary_text_color</item>
+ <item name="android:textColorSecondary">@color/dialer_secondary_text_color</item>
+ <!-- This is used for title bar color in recents -->
+ <item name="android:colorPrimary">@color/titlebar_in_recents_background_color</item>
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="android:colorControlActivated">@color/dialer_theme_color</item>
+ <item name="android:colorButtonNormal">@color/dialer_theme_color</item>
+ <item name="android:colorAccent">@color/dialtacts_theme_color</item>
+ <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
+ <item name="android:textAppearanceButton">@style/DialerButtonTextStyle</item>
+
+ <item name="android:actionBarStyle">@style/DialerActionBarBaseStyle</item>
+ <item name="actionBarStyle">@style/DialerActionBarBaseStyle</item>
+ <item name="android:actionBarSize">@dimen/action_bar_height</item>
+ <item name="actionBarSize">@dimen/action_bar_height</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/util/AndroidManifest.xml b/java/com/android/dialer/util/AndroidManifest.xml
new file mode 100644
index 0000000..499df9b
--- /dev/null
+++ b/java/com/android/dialer/util/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.util">
+</manifest>
diff --git a/java/com/android/dialer/util/CallUtil.java b/java/com/android/dialer/util/CallUtil.java
new file mode 100644
index 0000000..81a4bb2
--- /dev/null
+++ b/java/com/android/dialer/util/CallUtil.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2015 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.util;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import java.util.List;
+
+/** Utilities related to calls that can be used by non system apps. */
+public class CallUtil {
+
+ /** Indicates that the video calling is not available. */
+ public static final int VIDEO_CALLING_DISABLED = 0;
+
+ /** Indicates that video calling is enabled, regardless of presence status. */
+ public static final int VIDEO_CALLING_ENABLED = 1;
+
+ /**
+ * Indicates that video calling is enabled, but the availability of video call affordances is
+ * determined by the presence status associated with contacts.
+ */
+ public static final int VIDEO_CALLING_PRESENCE = 2;
+
+ /** Return Uri with an appropriate scheme, accepting both SIP and usual phone call numbers. */
+ public static Uri getCallUri(String number) {
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ return Uri.fromParts(PhoneAccount.SCHEME_SIP, number, null);
+ }
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
+ }
+
+ /** @return Uri that directly dials a user's voicemail inbox. */
+ public static Uri getVoicemailUri() {
+ return Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null);
+ }
+
+ /**
+ * Determines if video calling is available, and if so whether presence checking is available as
+ * well.
+ *
+ * <p>Returns a bitmask with {@link #VIDEO_CALLING_ENABLED} to indicate that video calling is
+ * available, and {@link #VIDEO_CALLING_PRESENCE} if presence indication is also available.
+ *
+ * @param context The context
+ * @return A bit-mask describing the current video capabilities.
+ */
+ public static int getVideoCallingAvailability(Context context) {
+ if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)
+ || !CompatUtils.isVideoCompatible()) {
+ return VIDEO_CALLING_DISABLED;
+ }
+ TelecomManager telecommMgr = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ if (telecommMgr == null) {
+ return VIDEO_CALLING_DISABLED;
+ }
+
+ List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
+ for (PhoneAccountHandle accountHandle : accountHandles) {
+ PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
+ if (account != null) {
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
+ // Builds prior to N do not have presence support.
+ if (!CompatUtils.isVideoPresenceCompatible()) {
+ return VIDEO_CALLING_ENABLED;
+ }
+
+ int videoCapabilities = VIDEO_CALLING_ENABLED;
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)) {
+ videoCapabilities |= VIDEO_CALLING_PRESENCE;
+ }
+ return videoCapabilities;
+ }
+ }
+ }
+ return VIDEO_CALLING_DISABLED;
+ }
+
+ /**
+ * Determines if one of the call capable phone accounts defined supports video calling.
+ *
+ * @param context The context.
+ * @return {@code true} if one of the call capable phone accounts supports video calling, {@code
+ * false} otherwise.
+ */
+ public static boolean isVideoEnabled(Context context) {
+ return (getVideoCallingAvailability(context) & VIDEO_CALLING_ENABLED) != 0;
+ }
+
+ /**
+ * Determines if one of the call capable phone accounts defined supports calling with a subject
+ * specified.
+ *
+ * @param context The context.
+ * @return {@code true} if one of the call capable phone accounts supports calling with a subject
+ * specified, {@code false} otherwise.
+ */
+ public static boolean isCallWithSubjectSupported(Context context) {
+ if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)
+ || !CompatUtils.isCallSubjectCompatible()) {
+ return false;
+ }
+ TelecomManager telecommMgr = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ if (telecommMgr == null) {
+ return false;
+ }
+
+ List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
+ for (PhoneAccountHandle accountHandle : accountHandles) {
+ PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
+ if (account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/util/DialerUtils.java b/java/com/android/dialer/util/DialerUtils.java
new file mode 100644
index 0000000..63f870e
--- /dev/null
+++ b/java/com/android/dialer/util/DialerUtils.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2014 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.util;
+
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Point;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Toast;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import java.io.File;
+import java.util.Iterator;
+import java.util.Random;
+
+/** General purpose utility methods for the Dialer. */
+public class DialerUtils {
+
+ /**
+ * Prefix on a dialed number that indicates that the call should be placed through the Wireless
+ * Priority Service.
+ */
+ private static final String WPS_PREFIX = "*272";
+
+ public static final String FILE_PROVIDER_CACHE_DIR = "my_cache";
+
+ private static final Random RANDOM = new Random();
+
+ /**
+ * Attempts to start an activity and displays a toast with the default error message if the
+ * activity is not found, instead of throwing an exception.
+ *
+ * @param context to start the activity with.
+ * @param intent to start the activity with.
+ */
+ public static void startActivityWithErrorToast(Context context, Intent intent) {
+ startActivityWithErrorToast(context, intent, R.string.activity_not_available);
+ }
+
+ /**
+ * Attempts to start an activity and displays a toast with a provided error message if the
+ * activity is not found, instead of throwing an exception.
+ *
+ * @param context to start the activity with.
+ * @param intent to start the activity with.
+ * @param msgId Resource ID of the string to display in an error message if the activity is not
+ * found.
+ */
+ public static void startActivityWithErrorToast(
+ final Context context, final Intent intent, int msgId) {
+ try {
+ if ((Intent.ACTION_CALL.equals(intent.getAction()))) {
+ // All dialer-initiated calls should pass the touch point to the InCallUI
+ Point touchPoint = TouchPointManager.getInstance().getPoint();
+ if (touchPoint.x != 0 || touchPoint.y != 0) {
+ Bundle extras;
+ // Make sure to not accidentally clobber any existing extras
+ if (intent.hasExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)) {
+ extras = intent.getParcelableExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
+ } else {
+ extras = new Bundle();
+ }
+ extras.putParcelable(TouchPointManager.TOUCH_POINT, touchPoint);
+ intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
+ }
+
+ if (shouldWarnForOutgoingWps(context, intent.getData().getSchemeSpecificPart())) {
+ LogUtil.i(
+ "DialUtils.startActivityWithErrorToast",
+ "showing outgoing WPS dialog before placing call");
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.outgoing_wps_warning);
+ builder.setPositiveButton(
+ R.string.dialog_continue,
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ placeCallOrMakeToast(context, intent);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.create().show();
+ } else {
+ placeCallOrMakeToast(context, intent);
+ }
+ } else {
+ context.startActivity(intent);
+ }
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(context, msgId, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private static void placeCallOrMakeToast(Context context, Intent intent) {
+ final boolean hasCallPermission = TelecomUtil.placeCall(context, intent);
+ if (!hasCallPermission) {
+ // TODO: Make calling activity show request permission dialog and handle
+ // callback results appropriately.
+ Toast.makeText(context, "Cannot place call without Phone permission", Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ /**
+ * Returns whether the user should be warned about an outgoing WPS call. This checks if there is a
+ * currently active call over LTE. Regardless of the country or carrier, the radio will drop an
+ * active LTE call if a WPS number is dialed, so this warning is necessary.
+ */
+ private static boolean shouldWarnForOutgoingWps(Context context, String number) {
+ if (number != null && number.startsWith(WPS_PREFIX)) {
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ boolean isOnVolte =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ && telephonyManager.getVoiceNetworkType() == TelephonyManager.NETWORK_TYPE_LTE;
+ boolean hasCurrentActiveCall =
+ telephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK;
+ return isOnVolte && hasCurrentActiveCall;
+ }
+ return false;
+ }
+
+ /**
+ * Closes an {@link AutoCloseable}, silently ignoring any checked exceptions. Does nothing if
+ * null.
+ *
+ * @param closeable to close.
+ */
+ public static void closeQuietly(AutoCloseable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ /**
+ * Joins a list of {@link CharSequence} into a single {@link CharSequence} seperated by ", ".
+ *
+ * @param list List of char sequences to join.
+ * @return Joined char sequences.
+ */
+ public static CharSequence join(Iterable<CharSequence> list) {
+ StringBuilder sb = new StringBuilder();
+ final BidiFormatter formatter = BidiFormatter.getInstance();
+ final CharSequence separator = ", ";
+
+ Iterator<CharSequence> itr = list.iterator();
+ boolean firstTime = true;
+ while (itr.hasNext()) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(separator);
+ }
+ // Unicode wrap the elements of the list to respect RTL for individual strings.
+ sb.append(
+ formatter.unicodeWrap(itr.next().toString(), TextDirectionHeuristics.FIRSTSTRONG_LTR));
+ }
+
+ // Unicode wrap the joined value, to respect locale's RTL ordering for the whole list.
+ return formatter.unicodeWrap(sb.toString());
+ }
+
+ public static void showInputMethod(View view) {
+ final InputMethodManager imm =
+ (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(view, 0);
+ }
+ }
+
+ public static void hideInputMethod(View view) {
+ final InputMethodManager imm =
+ (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+
+ /**
+ * Create a File in the cache directory that Dialer's FileProvider knows about so they can be
+ * shared to other apps.
+ */
+ public static File createShareableFile(Context context) {
+ long fileId = Math.abs(RANDOM.nextLong());
+ File parentDir = new File(context.getCacheDir(), FILE_PROVIDER_CACHE_DIR);
+ if (!parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ return new File(parentDir, String.valueOf(fileId));
+ }
+
+ /**
+ * Returns default preference for context accessing device protected storage. This is used when
+ * directBoot is enabled (before device unlocked after boot) since the default shared preference
+ * used normally is not available at this moment for N devices. Returns regular default shared
+ * preference for pre-N devices.
+ */
+ @NonNull
+ public static SharedPreferences getDefaultSharedPreferenceForDeviceProtectedStorageContext(
+ @NonNull Context context) {
+ Assert.isNotNull(context);
+ Context deviceProtectedContext =
+ ContextCompat.isDeviceProtectedStorage(context)
+ ? context
+ : ContextCompat.createDeviceProtectedStorageContext(context);
+ // ContextCompat.createDeviceProtectedStorageContext(context) returns null on pre-N, thus fall
+ // back to regular default shared preference for pre-N devices since devices protected context
+ // is not available.
+ return PreferenceManager.getDefaultSharedPreferences(
+ deviceProtectedContext != null ? deviceProtectedContext : context);
+ }
+}
diff --git a/java/com/android/dialer/util/DrawableConverter.java b/java/com/android/dialer/util/DrawableConverter.java
new file mode 100644
index 0000000..5670315
--- /dev/null
+++ b/java/com/android/dialer/util/DrawableConverter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2012 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.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import com.android.dialer.common.LogUtil;
+
+/** Provides utilities for bitmaps and drawables. */
+public class DrawableConverter {
+
+ private DrawableConverter() {}
+
+ /** Converts the provided drawable to a bitmap using the drawable's intrinsic width and height. */
+ @Nullable
+ public static Bitmap drawableToBitmap(@Nullable Drawable drawable) {
+ return drawableToBitmap(drawable, 0, 0);
+ }
+
+ /**
+ * Converts the provided drawable to a bitmap with the specified width and height.
+ *
+ * <p>If both width and height are 0, the drawable's intrinsic width and height are used (but in
+ * that case {@link #drawableToBitmap(Drawable)} should be used).
+ */
+ @Nullable
+ public static Bitmap drawableToBitmap(@Nullable Drawable drawable, int width, int height) {
+ if (drawable == null) {
+ return null;
+ }
+
+ Bitmap bitmap;
+ if (drawable instanceof BitmapDrawable) {
+ bitmap = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ if (width > 0 || height > 0) {
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ } else if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+ // Needed for drawables that are just a colour.
+ bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ } else {
+ bitmap =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ }
+
+ LogUtil.i(
+ "DrawableConverter.drawableToBitmap",
+ "created bitmap with width: %d, height: %d",
+ bitmap.getWidth(),
+ bitmap.getHeight());
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ }
+ return bitmap;
+ }
+
+ @Nullable
+ public static Drawable getRoundedDrawable(
+ @NonNull Context context, @Nullable Drawable photo, int width, int height) {
+ Bitmap bitmap = drawableToBitmap(photo);
+ if (bitmap != null) {
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, width, height, false);
+ RoundedBitmapDrawable drawable =
+ RoundedBitmapDrawableFactory.create(context.getResources(), scaledBitmap);
+ drawable.setAntiAlias(true);
+ drawable.setCornerRadius(drawable.getIntrinsicHeight() / 2);
+ return drawable;
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/util/ExpirableCache.java b/java/com/android/dialer/util/ExpirableCache.java
new file mode 100644
index 0000000..2778a57
--- /dev/null
+++ b/java/com/android/dialer/util/ExpirableCache.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2011 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.util;
+
+import android.util.LruCache;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An LRU cache in which all items can be marked as expired at a given time and it is possible to
+ * query whether a particular cached value is expired or not.
+ *
+ * <p>A typical use case for this is caching of values which are expensive to compute but which are
+ * still useful when out of date.
+ *
+ * <p>Consider a cache for contact information:
+ *
+ * <pre>{@code
+ * private ExpirableCache<String, Contact> mContactCache;
+ * }</pre>
+ *
+ * which stores the contact information for a given phone number.
+ *
+ * <p>When we need to store contact information for a given phone number, we can look up the info in
+ * the cache:
+ *
+ * <pre>{@code
+ * CachedValue<Contact> cachedContact = mContactCache.getCachedValue(phoneNumber);
+ * }</pre>
+ *
+ * We might also want to fetch the contact information again if the item is expired.
+ *
+ * <pre>
+ * if (cachedContact.isExpired()) {
+ * fetchContactForNumber(phoneNumber,
+ * new FetchListener() {
+ * @Override
+ * public void onFetched(Contact contact) {
+ * mContactCache.put(phoneNumber, contact);
+ * }
+ * });
+ * }</pre>
+ *
+ * and insert it back into the cache when the fetch completes.
+ *
+ * <p>At a certain point we want to expire the content of the cache because we know the content may
+ * no longer be up-to-date, for instance, when resuming the activity this is shown into:
+ *
+ * <pre>
+ * @Override
+ * protected onResume() {
+ * // We were paused for some time, the cached value might no longer be up to date.
+ * mContactCache.expireAll();
+ * super.onResume();
+ * }
+ * </pre>
+ *
+ * The values will be still available from the cache, but they will be expired.
+ *
+ * <p>If interested only in the value itself, not whether it is expired or not, one should use the
+ * {@link #getPossiblyExpired(Object)} method. If interested only in non-expired values, one should
+ * use the {@link #get(Object)} method instead.
+ *
+ * <p>This class wraps around an {@link LruCache} instance: it follows the {@link LruCache} behavior
+ * for evicting items when the cache is full. It is possible to supply your own subclass of LruCache
+ * by using the {@link #create(LruCache)} method, which can define a custom expiration policy. Since
+ * the underlying cache maps keys to cached values it can determine which items are expired and
+ * which are not, allowing for an implementation that evicts expired items before non expired ones.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @param <K> the type of the keys
+ * @param <V> the type of the values
+ */
+@ThreadSafe
+public class ExpirableCache<K, V> {
+
+ /**
+ * The current generation of items added to the cache.
+ *
+ * <p>Items in the cache can belong to a previous generation, but in that case they would be
+ * expired.
+ *
+ * @see ExpirableCache.CachedValue#isExpired()
+ */
+ private final AtomicInteger mGeneration;
+ /** The underlying cache used to stored the cached values. */
+ private LruCache<K, CachedValue<V>> mCache;
+
+ private ExpirableCache(LruCache<K, CachedValue<V>> cache) {
+ mCache = cache;
+ mGeneration = new AtomicInteger(0);
+ }
+
+ /**
+ * Creates a new {@link ExpirableCache} that wraps the given {@link LruCache}.
+ *
+ * <p>The created cache takes ownership of the cache passed in as an argument.
+ *
+ * @param <K> the type of the keys
+ * @param <V> the type of the values
+ * @param cache the cache to store the value in
+ * @return the newly created expirable cache
+ * @throws IllegalArgumentException if the cache is not empty
+ */
+ public static <K, V> ExpirableCache<K, V> create(LruCache<K, CachedValue<V>> cache) {
+ return new ExpirableCache<K, V>(cache);
+ }
+
+ /**
+ * Creates a new {@link ExpirableCache} with the given maximum size.
+ *
+ * @param <K> the type of the keys
+ * @param <V> the type of the values
+ * @return the newly created expirable cache
+ */
+ public static <K, V> ExpirableCache<K, V> create(int maxSize) {
+ return create(new LruCache<K, CachedValue<V>>(maxSize));
+ }
+
+ /**
+ * Returns the cached value for the given key, or null if no value exists.
+ *
+ * <p>The cached value gives access both to the value associated with the key and whether it is
+ * expired or not.
+ *
+ * <p>If not interested in whether the value is expired, use {@link #getPossiblyExpired(Object)}
+ * instead.
+ *
+ * <p>If only wants values that are not expired, use {@link #get(Object)} instead.
+ *
+ * @param key the key to look up
+ */
+ public CachedValue<V> getCachedValue(K key) {
+ return mCache.get(key);
+ }
+
+ /**
+ * Returns the value for the given key, or null if no value exists.
+ *
+ * <p>When using this method, it is not possible to determine whether the value is expired or not.
+ * Use {@link #getCachedValue(Object)} to achieve that instead. However, if using {@link
+ * #getCachedValue(Object)} to determine if an item is expired, one should use the item within the
+ * {@link CachedValue} and not call {@link #getPossiblyExpired(Object)} to get the value
+ * afterwards, since that is not guaranteed to return the same value or that the newly returned
+ * value is in the same state.
+ *
+ * @param key the key to look up
+ */
+ public V getPossiblyExpired(K key) {
+ CachedValue<V> cachedValue = getCachedValue(key);
+ return cachedValue == null ? null : cachedValue.getValue();
+ }
+
+ /**
+ * Returns the value for the given key only if it is not expired, or null if no value exists or is
+ * expired.
+ *
+ * <p>This method will return null if either there is no value associated with this key or if the
+ * associated value is expired.
+ *
+ * @param key the key to look up
+ */
+ public V get(K key) {
+ CachedValue<V> cachedValue = getCachedValue(key);
+ return cachedValue == null || cachedValue.isExpired() ? null : cachedValue.getValue();
+ }
+
+ /**
+ * Puts an item in the cache.
+ *
+ * <p>Newly added item will not be expired until {@link #expireAll()} is next called.
+ *
+ * @param key the key to look up
+ * @param value the value to associate with the key
+ */
+ public void put(K key, V value) {
+ mCache.put(key, newCachedValue(value));
+ }
+
+ /**
+ * Mark all items currently in the cache as expired.
+ *
+ * <p>Newly added items after this call will be marked as not expired.
+ *
+ * <p>Expiring the items in the cache does not imply they will be evicted.
+ */
+ public void expireAll() {
+ mGeneration.incrementAndGet();
+ }
+
+ /**
+ * Creates a new {@link CachedValue} instance to be stored in this cache.
+ *
+ * <p>Implementation of {@link LruCache#create(K)} can use this method to create a new entry.
+ */
+ public CachedValue<V> newCachedValue(V value) {
+ return new GenerationalCachedValue<V>(value, mGeneration);
+ }
+
+ /**
+ * A cached value stored inside the cache.
+ *
+ * <p>It provides access to the value stored in the cache but also allows to check whether the
+ * value is expired.
+ *
+ * @param <V> the type of value stored in the cache
+ */
+ public interface CachedValue<V> {
+
+ /** Returns the value stored in the cache for a given key. */
+ V getValue();
+
+ /**
+ * Checks whether the value, while still being present in the cache, is expired.
+ *
+ * @return true if the value is expired
+ */
+ boolean isExpired();
+ }
+
+ /** Cached values storing the generation at which they were added. */
+ @Immutable
+ private static class GenerationalCachedValue<V> implements ExpirableCache.CachedValue<V> {
+
+ /** The value stored in the cache. */
+ public final V mValue;
+ /** The generation at which the value was added to the cache. */
+ private final int mGeneration;
+ /** The atomic integer storing the current generation of the cache it belongs to. */
+ private final AtomicInteger mCacheGeneration;
+
+ /**
+ * @param cacheGeneration the atomic integer storing the generation of the cache in which this
+ * value will be stored
+ */
+ public GenerationalCachedValue(V value, AtomicInteger cacheGeneration) {
+ mValue = value;
+ mCacheGeneration = cacheGeneration;
+ // Snapshot the current generation.
+ mGeneration = mCacheGeneration.get();
+ }
+
+ @Override
+ public V getValue() {
+ return mValue;
+ }
+
+ @Override
+ public boolean isExpired() {
+ return mGeneration != mCacheGeneration.get();
+ }
+ }
+}
diff --git a/java/com/android/dialer/util/IntentUtil.java b/java/com/android/dialer/util/IntentUtil.java
new file mode 100644
index 0000000..2f265b5
--- /dev/null
+++ b/java/com/android/dialer/util/IntentUtil.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 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.util;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+/** Utilities for creation of intents in Dialer. */
+public class IntentUtil {
+
+ private static final String SMS_URI_PREFIX = "sms:";
+ private static final int NO_PHONE_TYPE = -1;
+
+ public static Intent getSendSmsIntent(CharSequence phoneNumber) {
+ return new Intent(Intent.ACTION_SENDTO, Uri.parse(SMS_URI_PREFIX + phoneNumber));
+ }
+
+ public static Intent getNewContactIntent() {
+ return new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);
+ }
+
+ public static Intent getNewContactIntent(CharSequence phoneNumber) {
+ return getNewContactIntent(null /* name */, phoneNumber /* phoneNumber */, NO_PHONE_TYPE);
+ }
+
+ public static Intent getNewContactIntent(
+ CharSequence name, CharSequence phoneNumber, int phoneNumberType) {
+ Intent intent = getNewContactIntent();
+ populateContactIntent(intent, name, phoneNumber, phoneNumberType);
+ return intent;
+ }
+
+ public static Intent getAddToExistingContactIntent() {
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
+ return intent;
+ }
+
+ public static Intent getAddToExistingContactIntent(CharSequence phoneNumber) {
+ return getAddToExistingContactIntent(
+ null /* name */, phoneNumber /* phoneNumber */, NO_PHONE_TYPE);
+ }
+
+ public static Intent getAddToExistingContactIntent(
+ CharSequence name, CharSequence phoneNumber, int phoneNumberType) {
+ Intent intent = getAddToExistingContactIntent();
+ populateContactIntent(intent, name, phoneNumber, phoneNumberType);
+ return intent;
+ }
+
+ private static void populateContactIntent(
+ Intent intent, CharSequence name, CharSequence phoneNumber, int phoneNumberType) {
+ if (phoneNumber != null) {
+ intent.putExtra(ContactsContract.Intents.Insert.PHONE, phoneNumber);
+ }
+ if (name != null) {
+ intent.putExtra(ContactsContract.Intents.Insert.NAME, name);
+ }
+ if (phoneNumberType != NO_PHONE_TYPE) {
+ intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE, phoneNumberType);
+ }
+ }
+}
diff --git a/java/com/android/dialer/util/MoreStrings.java b/java/com/android/dialer/util/MoreStrings.java
new file mode 100644
index 0000000..5a43b1d
--- /dev/null
+++ b/java/com/android/dialer/util/MoreStrings.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2013 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.util;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+/** Static utility methods for Strings. */
+public class MoreStrings {
+
+ /**
+ * Returns the given string if it is non-null; the empty string otherwise.
+ *
+ * @param string the string to test and possibly return
+ * @return {@code string} itself if it is non-null; {@code ""} if it is null
+ */
+ public static String nullToEmpty(@Nullable String string) {
+ return (string == null) ? "" : string;
+ }
+
+ /**
+ * Returns the given string if it is nonempty; {@code null} otherwise.
+ *
+ * @param string the string to test and possibly return
+ * @return {@code string} itself if it is nonempty; {@code null} if it is empty or null
+ */
+ @Nullable
+ public static String emptyToNull(@Nullable String string) {
+ return TextUtils.isEmpty(string) ? null : string;
+ }
+
+ public static String toSafeString(String value) {
+ if (value == null) {
+ return null;
+ }
+
+ // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+ // sanitized phone numbers.
+ final StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ final char c = value.charAt(i);
+ if (c == '-' || c == '@' || c == '.') {
+ builder.append(c);
+ } else {
+ builder.append('x');
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/java/com/android/dialer/util/OrientationUtil.java b/java/com/android/dialer/util/OrientationUtil.java
new file mode 100644
index 0000000..5a8d1ae
--- /dev/null
+++ b/java/com/android/dialer/util/OrientationUtil.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2012 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.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+
+/** Static methods related to device orientation. */
+public class OrientationUtil {
+
+ /** @return if the context is in landscape orientation. */
+ public static boolean isLandscape(Context context) {
+ return context.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ }
+}
diff --git a/java/com/android/dialer/util/PermissionsUtil.java b/java/com/android/dialer/util/PermissionsUtil.java
new file mode 100644
index 0000000..70b96df
--- /dev/null
+++ b/java/com/android/dialer/util/PermissionsUtil.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 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.util;
+
+import android.Manifest.permission;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.LocalBroadcastManager;
+import com.android.dialer.common.LogUtil;
+
+/** Utility class to help with runtime permissions. */
+public class PermissionsUtil {
+
+ private static final String PERMISSION_PREFERENCE = "dialer_permissions";
+
+ public static boolean hasPhonePermissions(Context context) {
+ return hasPermission(context, permission.CALL_PHONE);
+ }
+
+ public static boolean hasContactsPermissions(Context context) {
+ return hasPermission(context, permission.READ_CONTACTS);
+ }
+
+ public static boolean hasLocationPermissions(Context context) {
+ return hasPermission(context, permission.ACCESS_FINE_LOCATION);
+ }
+
+ public static boolean hasCameraPermissions(Context context) {
+ return hasPermission(context, permission.CAMERA);
+ }
+
+ public static boolean hasPermission(Context context, String permission) {
+ return ContextCompat.checkSelfPermission(context, permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Checks {@link android.content.SharedPreferences} if a permission has been requested before.
+ *
+ * <p>It is important to note that this method only works if you call {@link
+ * PermissionsUtil#permissionRequested(Context, String)} in {@link
+ * android.app.Activity#onRequestPermissionsResult(int, String[], int[])}.
+ */
+ public static boolean isFirstRequest(Context context, String permission) {
+ return context
+ .getSharedPreferences(PERMISSION_PREFERENCE, Context.MODE_PRIVATE)
+ .getBoolean(permission, true);
+ }
+
+ /**
+ * Records in {@link android.content.SharedPreferences} that the specified permission has been
+ * requested at least once.
+ *
+ * <p>This method should be called in {@link android.app.Activity#onRequestPermissionsResult(int,
+ * String[], int[])}.
+ */
+ public static void permissionRequested(Context context, String permission) {
+ context
+ .getSharedPreferences(PERMISSION_PREFERENCE, Context.MODE_PRIVATE)
+ .edit()
+ .putBoolean(permission, false)
+ .apply();
+ }
+
+ /**
+ * Rudimentary methods wrapping the use of a LocalBroadcastManager to simplify the process of
+ * notifying other classes when a particular fragment is notified that a permission is granted.
+ *
+ * <p>To be notified when a permission has been granted, create a new broadcast receiver and
+ * register it using {@link #registerPermissionReceiver(Context, BroadcastReceiver, String)}
+ *
+ * <p>E.g.
+ *
+ * <p>final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void
+ * onReceive(Context context, Intent intent) { refreshContactsView(); } }
+ *
+ * <p>PermissionsUtil.registerPermissionReceiver(getActivity(), receiver, READ_CONTACTS);
+ *
+ * <p>If you register to listen for multiple permissions, you can identify which permission was
+ * granted by inspecting {@link Intent#getAction()}.
+ *
+ * <p>In the fragment that requests for the permission, be sure to call {@link
+ * #notifyPermissionGranted(Context, String)} when the permission is granted so that any
+ * interested listeners are notified of the change.
+ */
+ public static void registerPermissionReceiver(
+ Context context, BroadcastReceiver receiver, String permission) {
+ LogUtil.i("PermissionsUtil.registerPermissionReceiver", permission);
+ final IntentFilter filter = new IntentFilter(permission);
+ LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);
+ }
+
+ public static void unregisterPermissionReceiver(Context context, BroadcastReceiver receiver) {
+ LogUtil.i("PermissionsUtil.unregisterPermissionReceiver", null);
+ LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
+ }
+
+ public static void notifyPermissionGranted(Context context, String permission) {
+ LogUtil.i("PermissionsUtil.notifyPermissionGranted", permission);
+ final Intent intent = new Intent(permission);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+}
diff --git a/java/com/android/dialer/util/SettingsUtil.java b/java/com/android/dialer/util/SettingsUtil.java
new file mode 100644
index 0000000..c61c09b
--- /dev/null
+++ b/java/com/android/dialer/util/SettingsUtil.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2014 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.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteException;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+public class SettingsUtil {
+
+ private static final String DEFAULT_NOTIFICATION_URI_STRING =
+ Settings.System.DEFAULT_NOTIFICATION_URI.toString();
+
+ /**
+ * Queries for a ringtone name, and sets the name using a handler. This is a method was originally
+ * copied from com.android.settings.SoundSettings.
+ *
+ * @param context The application context.
+ * @param handler The handler, which takes the name of the ringtone as a String as a parameter.
+ * @param type The type of sound.
+ * @param key The key to the shared preferences entry being updated.
+ * @param msg An integer identifying the message sent to the handler.
+ */
+ public static void updateRingtoneName(
+ Context context, Handler handler, int type, String key, int msg) {
+ final Uri ringtoneUri;
+ boolean defaultRingtone = false;
+ if (type == RingtoneManager.TYPE_RINGTONE) {
+ // For ringtones, we can just lookup the system default because changing the settings
+ // in Call Settings changes the system default.
+ ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+ } else {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ // For voicemail notifications, we use the value saved in Phone's shared preferences.
+ String uriString = prefs.getString(key, DEFAULT_NOTIFICATION_URI_STRING);
+ if (TextUtils.isEmpty(uriString)) {
+ // silent ringtone
+ ringtoneUri = null;
+ } else {
+ if (uriString.equals(DEFAULT_NOTIFICATION_URI_STRING)) {
+ // If it turns out that the voicemail notification is set to the system
+ // default notification, we retrieve the actual URI to prevent it from showing
+ // up as "Unknown Ringtone".
+ defaultRingtone = true;
+ ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+ } else {
+ ringtoneUri = Uri.parse(uriString);
+ }
+ }
+ }
+ CharSequence summary = context.getString(R.string.ringtone_unknown);
+ // Is it a silent ringtone?
+ if (ringtoneUri == null) {
+ summary = context.getString(R.string.ringtone_silent);
+ } else {
+ // Fetch the ringtone title from the media provider
+ final Ringtone ringtone = RingtoneManager.getRingtone(context, ringtoneUri);
+ if (ringtone != null) {
+ try {
+ final String title = ringtone.getTitle(context);
+ if (!TextUtils.isEmpty(title)) {
+ summary = title;
+ }
+ } catch (SQLiteException sqle) {
+ // Unknown title for the ringtone
+ }
+ }
+ }
+ if (defaultRingtone) {
+ summary = context.getString(R.string.default_notification_description, summary);
+ }
+ handler.sendMessage(handler.obtainMessage(msg, summary));
+ }
+}
diff --git a/java/com/android/dialer/util/TouchPointManager.java b/java/com/android/dialer/util/TouchPointManager.java
new file mode 100644
index 0000000..74f87c4
--- /dev/null
+++ b/java/com/android/dialer/util/TouchPointManager.java
@@ -0,0 +1,60 @@
+/*
+ * 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.util;
+
+import android.graphics.Point;
+
+/**
+ * Singleton class to keep track of where the user last touched the screen.
+ *
+ * <p>Used to pass on to the InCallUI for animation.
+ */
+public class TouchPointManager {
+
+ public static final String TOUCH_POINT = "touchPoint";
+
+ private static TouchPointManager sInstance = new TouchPointManager();
+
+ private Point mPoint = new Point();
+
+ /** Private constructor. Instance should only be acquired through getInstance(). */
+ private TouchPointManager() {}
+
+ public static TouchPointManager getInstance() {
+ return sInstance;
+ }
+
+ public Point getPoint() {
+ return mPoint;
+ }
+
+ public void setPoint(int x, int y) {
+ mPoint.set(x, y);
+ }
+
+ /**
+ * When a point is initialized, its value is (0,0). Since it is highly unlikely a user will touch
+ * at that exact point, if the point in TouchPointManager is (0,0), it is safe to assume that the
+ * TouchPointManager has not yet collected a touch.
+ *
+ * @return True if there is a valid point saved. Define a valid point as any point that is not
+ * (0,0).
+ */
+ public boolean hasValidPoint() {
+ return mPoint.x != 0 || mPoint.y != 0;
+ }
+}
diff --git a/java/com/android/dialer/util/TransactionSafeActivity.java b/java/com/android/dialer/util/TransactionSafeActivity.java
new file mode 100644
index 0000000..9b5e92b
--- /dev/null
+++ b/java/com/android/dialer/util/TransactionSafeActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 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.util;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+/**
+ * A common superclass that keeps track of whether an {@link Activity} has saved its state yet or
+ * not.
+ */
+public abstract class TransactionSafeActivity extends AppCompatActivity {
+
+ private boolean mIsSafeToCommitTransactions;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mIsSafeToCommitTransactions = false;
+ }
+
+ /**
+ * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+ * whether {@link Activity#onSaveInstanceState} has been called or not.
+ *
+ * <p>Make sure that the current activity calls into {@link super.onSaveInstanceState(Bundle
+ * outState)} (if that method is overridden), so the flag is properly set.
+ */
+ public boolean isSafeToCommitTransactions() {
+ return mIsSafeToCommitTransactions;
+ }
+}
diff --git a/java/com/android/dialer/util/ViewUtil.java b/java/com/android/dialer/util/ViewUtil.java
new file mode 100644
index 0000000..de08e41
--- /dev/null
+++ b/java/com/android/dialer/util/ViewUtil.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012 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.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Paint;
+import android.os.PowerManager;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.widget.TextView;
+import java.util.Locale;
+
+/** Provides static functions to work with views */
+public class ViewUtil {
+
+ private ViewUtil() {}
+
+ /** Similar to {@link Runnable} but takes a View parameter to operate on */
+ public interface ViewRunnable {
+ void run(@NonNull View view);
+ }
+
+ /**
+ * Returns the width as specified in the LayoutParams
+ *
+ * @throws IllegalStateException Thrown if the view's width is unknown before a layout pass s
+ */
+ public static int getConstantPreLayoutWidth(View view) {
+ // We haven't been layed out yet, so get the size from the LayoutParams
+ final ViewGroup.LayoutParams p = view.getLayoutParams();
+ if (p.width < 0) {
+ throw new IllegalStateException(
+ "Expecting view's width to be a constant rather " + "than a result of the layout pass");
+ }
+ return p.width;
+ }
+
+ /**
+ * Returns a boolean indicating whether or not the view's layout direction is RTL
+ *
+ * @param view - A valid view
+ * @return True if the view's layout direction is RTL
+ */
+ public static boolean isViewLayoutRtl(View view) {
+ return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ public static boolean isRtl() {
+ return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ public static void resizeText(TextView textView, int originalTextSize, int minTextSize) {
+ final Paint paint = textView.getPaint();
+ final int width = textView.getWidth();
+ if (width == 0) {
+ return;
+ }
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize);
+ float ratio = width / paint.measureText(textView.getText().toString());
+ if (ratio <= 1.0f) {
+ textView.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX, Math.max(minTextSize, originalTextSize * ratio));
+ }
+ }
+
+ /** Runs a piece of code just before the next draw, after layout and measurement */
+ public static void doOnPreDraw(
+ @NonNull final View view, final boolean drawNextFrame, final Runnable runnable) {
+ view.getViewTreeObserver()
+ .addOnPreDrawListener(
+ new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ view.getViewTreeObserver().removeOnPreDrawListener(this);
+ runnable.run();
+ return drawNextFrame;
+ }
+ });
+ }
+
+ public static void doOnPreDraw(
+ @NonNull final View view, final boolean drawNextFrame, final ViewRunnable runnable) {
+ view.getViewTreeObserver()
+ .addOnPreDrawListener(
+ new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ view.getViewTreeObserver().removeOnPreDrawListener(this);
+ runnable.run(view);
+ return drawNextFrame;
+ }
+ });
+ }
+
+ /**
+ * Returns {@code true} if animations should be disabled.
+ *
+ * <p>Animations should be disabled if {@link
+ * android.provider.Settings.Global#ANIMATOR_DURATION_SCALE} is set to 0 through system settings
+ * or the device is in power save mode.
+ */
+ public static boolean areAnimationsDisabled(Context context) {
+ ContentResolver contentResolver = context.getContentResolver();
+ PowerManager powerManager = context.getSystemService(PowerManager.class);
+ return Settings.Global.getFloat(contentResolver, Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0
+ || powerManager.isPowerSaveMode();
+ }
+}
diff --git a/java/com/android/dialer/util/res/values/strings.xml b/java/com/android/dialer/util/res/values/strings.xml
new file mode 100644
index 0000000..43ea6e3
--- /dev/null
+++ b/java/com/android/dialer/util/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- The string used to describe a notification if it is the default one in the system. For
+ example, if the user selects the default notification, it will appear as something like
+ Default sound(Capella) in the notification summary.
+ [CHAR LIMIT=40] -->
+ <string name="default_notification_description">Default sound (<xliff:g id="default_sound_title">%1$s</xliff:g>)</string>
+
+ <!-- Choice in the ringtone picker. If chosen, there will be silence instead of a ringtone played. -->
+ <string name="ringtone_silent">None</string>
+
+ <!-- If there is ever a ringtone set for some setting, but that ringtone can no longer be resolved, this is shown instead. For example, if the ringtone was on a SD card and it had been removed, this would be shown for ringtones on that SD card. -->
+ <string name="ringtone_unknown">Unknown ringtone</string>
+
+ <!-- Message displayed when there is no application available to handle a particular action.
+ [CHAR LIMIT=NONE] -->
+ <string name="activity_not_available">No app for that on this device</string>
+
+ <!-- Text of warning to be shown when the user attempts to make an outgoing Wireless
+ Preferred Service call when there is an VoLTE call in progress -->
+ <string name="outgoing_wps_warning">Placing a WPS call will disconnect your existing call.</string>
+
+ <!-- Text for button which indicates that the user wants to proceed with an action. -->
+ <string name="dialog_continue">Continue</string>
+
+</resources>
diff --git a/java/com/android/dialer/voicemailstatus/AndroidManifest.xml b/java/com/android/dialer/voicemailstatus/AndroidManifest.xml
new file mode 100644
index 0000000..a39894c
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.voicemailstatus">
+</manifest>
diff --git a/java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java b/java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java
new file mode 100644
index 0000000..142bb63
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java
@@ -0,0 +1,111 @@
+/*
+ * 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.voicemailstatus;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import com.android.dialer.database.CallLogQueryHandler;
+
+/**
+ * Helper class to check whether visual voicemail is enabled.
+ *
+ * <p>Call isVisualVoicemailEnabled() to retrieve the result.
+ *
+ * <p>The result is cached and saved in a SharedPreferences, stored as a boolean in
+ * PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER. Every time a new instance is created, it will try to
+ * restore the cached result from the SharedPreferences.
+ *
+ * <p>Call asyncUpdate() to make a CallLogQuery to check the actual status. This is a async call so
+ * isVisualVoicemailEnabled() will not be affected immediately.
+ *
+ * <p>If the status has changed as a result of asyncUpdate(),
+ * Callback.onVisualVoicemailEnabledStatusChanged() will be called with the new value.
+ */
+public class VisualVoicemailEnabledChecker implements CallLogQueryHandler.Listener {
+
+ public static final String PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER =
+ "has_active_voicemail_provider";
+ private SharedPreferences mPrefs;
+ private boolean mHasActiveVoicemailProvider;
+ private CallLogQueryHandler mCallLogQueryHandler;
+ private VoicemailStatusHelper mVoicemailStatusHelper;
+ private Context mContext;
+ private Callback mCallback;
+
+ public VisualVoicemailEnabledChecker(Context context, @Nullable Callback callback) {
+ mContext = context;
+ mCallback = callback;
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+ mHasActiveVoicemailProvider = mPrefs.getBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, false);
+ }
+
+ /**
+ * @return whether visual voicemail is enabled. Result is cached, call asyncUpdate() to update the
+ * result.
+ */
+ public boolean isVisualVoicemailEnabled() {
+ return mHasActiveVoicemailProvider;
+ }
+
+ /**
+ * Perform an async query into the system to check the status of visual voicemail. If the status
+ * has changed, Callback.onVisualVoicemailEnabledStatusChanged() will be called.
+ */
+ public void asyncUpdate() {
+ mCallLogQueryHandler = new CallLogQueryHandler(mContext, mContext.getContentResolver(), this);
+ mCallLogQueryHandler.fetchVoicemailStatus();
+ }
+
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ boolean hasActiveVoicemailProvider =
+ mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0;
+ if (hasActiveVoicemailProvider != mHasActiveVoicemailProvider) {
+ mHasActiveVoicemailProvider = hasActiveVoicemailProvider;
+ mPrefs.edit().putBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, mHasActiveVoicemailProvider);
+ if (mCallback != null) {
+ mCallback.onVisualVoicemailEnabledStatusChanged(mHasActiveVoicemailProvider);
+ }
+ }
+ }
+
+ @Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public boolean onCallsFetched(Cursor combinedCursor) {
+ // Do nothing
+ return false;
+ }
+
+ public interface Callback {
+
+ /** Callback to notify enabled status has changed to the @param newValue */
+ void onVisualVoicemailEnabledStatusChanged(boolean newValue);
+ }
+}
diff --git a/java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java
new file mode 100644
index 0000000..16bfe70
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 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.voicemailstatus;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.VisibleForTesting;
+import java.util.List;
+
+/**
+ * Interface used by the call log UI to determine what user message, if any, related to voicemail
+ * source status needs to be shown. The messages are returned in the order of importance.
+ *
+ * <p>The implementation of this interface interacts with the voicemail content provider to fetch
+ * statuses of all the registered voicemail sources and determines if any status message needs to be
+ * shown. The user of this interface must observe/listen to provider changes and invoke this class
+ * to check if any message needs to be shown.
+ */
+public interface VoicemailStatusHelper {
+
+ /**
+ * Returns a list of messages, in the order or priority that should be shown to the user. An empty
+ * list is returned if no message needs to be shown.
+ *
+ * @param cursor The cursor pointing to the query on {@link Status#CONTENT_URI}. The projection to
+ * be used is defined by the implementation class of this interface.
+ */
+ @VisibleForTesting
+ List<StatusMessage> getStatusMessages(Cursor cursor);
+
+ /**
+ * Returns the number of active voicemail sources installed.
+ *
+ * <p>The number of sources is counted by querying the voicemail status table.
+ */
+ int getNumberActivityVoicemailSources(Cursor cursor);
+
+ @VisibleForTesting
+ class StatusMessage {
+
+ /** Package of the source on behalf of which this message has to be shown. */
+ public final String sourcePackage;
+ /**
+ * The string resource id of the status message that should be shown in the call log page. Set
+ * to -1, if this message is not to be shown in call log.
+ */
+ public final int callLogMessageId;
+ /**
+ * The string resource id of the status message that should be shown in the call details page.
+ * Set to -1, if this message is not to be shown in call details page.
+ */
+ public final int callDetailsMessageId;
+ /** The string resource id of the action message that should be shown. */
+ public final int actionMessageId;
+ /** URI for the corrective action, where applicable. Null if no action URI is available. */
+ public final Uri actionUri;
+
+ public StatusMessage(
+ String sourcePackage,
+ int callLogMessageId,
+ int callDetailsMessageId,
+ int actionMessageId,
+ Uri actionUri) {
+ this.sourcePackage = sourcePackage;
+ this.callLogMessageId = callLogMessageId;
+ this.callDetailsMessageId = callDetailsMessageId;
+ this.actionMessageId = actionMessageId;
+ this.actionUri = actionUri;
+ }
+
+ /** Whether this message should be shown in the call log page. */
+ public boolean showInCallLog() {
+ return callLogMessageId != -1;
+ }
+
+ /** Whether this message should be shown in the call details page. */
+ public boolean showInCallDetails() {
+ return callDetailsMessageId != -1;
+ }
+ }
+}
diff --git a/java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java
new file mode 100644
index 0000000..404897f
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2011 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.voicemailstatus;
+
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_CAN_BE_CONFIGURED;
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_OK;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_OK;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_OK;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.database.VoicemailStatusQuery;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/** Implementation of {@link VoicemailStatusHelper}. */
+public class VoicemailStatusHelperImpl implements VoicemailStatusHelper {
+
+ @Override
+ public List<StatusMessage> getStatusMessages(Cursor cursor) {
+ List<MessageStatusWithPriority> messages =
+ new ArrayList<VoicemailStatusHelperImpl.MessageStatusWithPriority>();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ MessageStatusWithPriority message = getMessageForStatusEntry(cursor);
+ if (message != null) {
+ messages.add(message);
+ }
+ }
+ // Finally reorder the messages by their priority.
+ return reorderMessages(messages);
+ }
+
+ @Override
+ public int getNumberActivityVoicemailSources(Cursor cursor) {
+ int count = 0;
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ if (isVoicemailSourceActive(cursor)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Returns whether the source status in the cursor corresponds to an active source. A source is
+ * active if its' configuration state is not NOT_CONFIGURED. For most voicemail sources, only OK
+ * and NOT_CONFIGURED are used. The OMTP visual voicemail client has the same behavior pre-NMR1.
+ * NMR1 visual voicemail will only set it to NOT_CONFIGURED when it is deactivated. As soon as
+ * activation is attempted, it will transition into CONFIGURING then into OK or other error state,
+ * NOT_CONFIGURED is never set through an error.
+ */
+ private boolean isVoicemailSourceActive(Cursor cursor) {
+ return cursor.getString(VoicemailStatusQuery.SOURCE_PACKAGE_INDEX) != null
+ && cursor.getInt(VoicemailStatusQuery.CONFIGURATION_STATE_INDEX)
+ != Status.CONFIGURATION_STATE_NOT_CONFIGURED;
+ }
+
+ private List<StatusMessage> reorderMessages(List<MessageStatusWithPriority> messageWrappers) {
+ Collections.sort(
+ messageWrappers,
+ new Comparator<MessageStatusWithPriority>() {
+ @Override
+ public int compare(MessageStatusWithPriority msg1, MessageStatusWithPriority msg2) {
+ return msg1.mPriority - msg2.mPriority;
+ }
+ });
+ List<StatusMessage> reorderMessages = new ArrayList<VoicemailStatusHelper.StatusMessage>();
+ // Copy the ordered message objects into the final list.
+ for (MessageStatusWithPriority messageWrapper : messageWrappers) {
+ reorderMessages.add(messageWrapper.mMessage);
+ }
+ return reorderMessages;
+ }
+
+ /** Returns the message for the status entry pointed to by the cursor. */
+ private MessageStatusWithPriority getMessageForStatusEntry(Cursor cursor) {
+ final String sourcePackage = cursor.getString(VoicemailStatusQuery.SOURCE_PACKAGE_INDEX);
+ if (sourcePackage == null) {
+ return null;
+ }
+ final OverallState overallState =
+ getOverallState(
+ cursor.getInt(VoicemailStatusQuery.CONFIGURATION_STATE_INDEX),
+ cursor.getInt(VoicemailStatusQuery.DATA_CHANNEL_STATE_INDEX),
+ cursor.getInt(VoicemailStatusQuery.NOTIFICATION_CHANNEL_STATE_INDEX));
+ final Action action = overallState.getAction();
+
+ // No source package or no action, means no message shown.
+ if (action == Action.NONE) {
+ return null;
+ }
+
+ Uri actionUri = null;
+ if (action == Action.CALL_VOICEMAIL) {
+ actionUri =
+ UriUtils.parseUriOrNull(
+ cursor.getString(VoicemailStatusQuery.VOICEMAIL_ACCESS_URI_INDEX));
+ // Even if actionUri is null, it is still be useful to show the notification.
+ } else if (action == Action.CONFIGURE_VOICEMAIL) {
+ actionUri =
+ UriUtils.parseUriOrNull(cursor.getString(VoicemailStatusQuery.SETTINGS_URI_INDEX));
+ // If there is no settings URI, there is no point in showing the notification.
+ if (actionUri == null) {
+ return null;
+ }
+ }
+ return new MessageStatusWithPriority(
+ new StatusMessage(
+ sourcePackage,
+ overallState.getCallLogMessageId(),
+ overallState.getCallDetailsMessageId(),
+ action.getMessageId(),
+ actionUri),
+ overallState.getPriority());
+ }
+
+ private OverallState getOverallState(
+ int configurationState, int dataChannelState, int notificationChannelState) {
+ if (configurationState == CONFIGURATION_STATE_OK) {
+ // Voicemail is configured. Let's see how is the data channel.
+ if (dataChannelState == DATA_CHANNEL_STATE_OK) {
+ // Data channel is fine. What about notification channel?
+ if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+ return OverallState.OK;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+ return OverallState.NO_DETAILED_NOTIFICATION;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+ return OverallState.NO_NOTIFICATIONS;
+ }
+ } else if (dataChannelState == DATA_CHANNEL_STATE_NO_CONNECTION) {
+ // Data channel is not working. What about notification channel?
+ if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+ return OverallState.NO_DATA;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+ return OverallState.MESSAGE_WAITING;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+ return OverallState.NO_CONNECTION;
+ }
+ }
+ } else if (configurationState == CONFIGURATION_STATE_CAN_BE_CONFIGURED) {
+ // Voicemail not configured. data/notification channel states are irrelevant.
+ return OverallState.INVITE_FOR_CONFIGURATION;
+ } else if (configurationState == Status.CONFIGURATION_STATE_NOT_CONFIGURED) {
+ // Voicemail not configured. data/notification channel states are irrelevant.
+ return OverallState.NOT_CONFIGURED;
+ }
+ // Will reach here only if the source has set an invalid value.
+ return OverallState.INVALID;
+ }
+
+ /** Possible user actions. */
+ public enum Action {
+ NONE(-1),
+ CALL_VOICEMAIL(R.string.voicemail_status_action_call_server),
+ CONFIGURE_VOICEMAIL(R.string.voicemail_status_action_configure);
+
+ private final int mMessageId;
+
+ Action(int messageId) {
+ mMessageId = messageId;
+ }
+
+ public int getMessageId() {
+ return mMessageId;
+ }
+ }
+
+ /**
+ * Overall state of the source status. Each state is associated with the corresponding display
+ * string and the corrective action. The states are also assigned a relative priority which is
+ * used to order the messages from different sources.
+ */
+ private enum OverallState {
+ // TODO: Add separate string for call details and call log pages for the states that needs
+ // to be shown in both.
+ /** Both notification and data channel are not working. */
+ NO_CONNECTION(
+ 0,
+ Action.CALL_VOICEMAIL,
+ R.string.voicemail_status_voicemail_not_available,
+ R.string.voicemail_status_audio_not_available),
+ /** Notifications working, but data channel is not working. Audio cannot be downloaded. */
+ NO_DATA(
+ 1,
+ Action.CALL_VOICEMAIL,
+ R.string.voicemail_status_voicemail_not_available,
+ R.string.voicemail_status_audio_not_available),
+ /** Messages are known to be waiting but data channel is not working. */
+ MESSAGE_WAITING(
+ 2,
+ Action.CALL_VOICEMAIL,
+ R.string.voicemail_status_messages_waiting,
+ R.string.voicemail_status_audio_not_available),
+ /** Notification channel not working, but data channel is. */
+ NO_NOTIFICATIONS(3, Action.CALL_VOICEMAIL, R.string.voicemail_status_voicemail_not_available),
+ /** Invite user to set up voicemail. */
+ INVITE_FOR_CONFIGURATION(
+ 4, Action.CONFIGURE_VOICEMAIL, R.string.voicemail_status_configure_voicemail),
+ /**
+ * No detailed notifications, but data channel is working. This is normal mode of operation for
+ * certain sources. No action needed.
+ */
+ NO_DETAILED_NOTIFICATION(5, Action.NONE, -1),
+ /** Visual voicemail not yet set up. No local action needed. */
+ NOT_CONFIGURED(6, Action.NONE, -1),
+ /** Everything is OK. */
+ OK(7, Action.NONE, -1),
+ /** If one or more state value set by the source is not valid. */
+ INVALID(8, Action.NONE, -1);
+
+ private final int mPriority;
+ private final Action mAction;
+ private final int mCallLogMessageId;
+ private final int mCallDetailsMessageId;
+
+ OverallState(int priority, Action action, int callLogMessageId) {
+ this(priority, action, callLogMessageId, -1);
+ }
+
+ OverallState(int priority, Action action, int callLogMessageId, int callDetailsMessageId) {
+ mPriority = priority;
+ mAction = action;
+ mCallLogMessageId = callLogMessageId;
+ mCallDetailsMessageId = callDetailsMessageId;
+ }
+
+ public Action getAction() {
+ return mAction;
+ }
+
+ public int getPriority() {
+ return mPriority;
+ }
+
+ public int getCallLogMessageId() {
+ return mCallLogMessageId;
+ }
+
+ public int getCallDetailsMessageId() {
+ return mCallDetailsMessageId;
+ }
+ }
+
+ /** A wrapper on {@link StatusMessage} which additionally stores the priority of the message. */
+ private static class MessageStatusWithPriority {
+
+ private final StatusMessage mMessage;
+ private final int mPriority;
+
+ public MessageStatusWithPriority(StatusMessage message, int priority) {
+ mMessage = message;
+ mPriority = priority;
+ }
+ }
+}
diff --git a/java/com/android/dialer/voicemailstatus/res/values/strings.xml b/java/com/android/dialer/voicemailstatus/res/values/strings.xml
new file mode 100644
index 0000000..495ddf2
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/res/values/strings.xml
@@ -0,0 +1,41 @@
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <!-- Voicemail status message shown at the top of call log to notify the user that no new
+ voicemails are currently available. This can happen when both notification as well as data
+ connection to the voicemail server is lost. [CHAR LIMIT=64] -->
+ <string name="voicemail_status_voicemail_not_available">Voicemail updates not available</string>
+ <!-- Voicemail status message shown at the top of call log to notify the user that there is no
+ data connection to the voicemail server, but there are new voicemails waiting on the server.
+ [CHAR LIMIT=64] -->
+ <string name="voicemail_status_messages_waiting">New voicemail waiting. Can\'t load right now.</string>
+ <!-- Voicemail status message shown at the top of call log to invite the user to configure
+ visual voicemail. [CHAR LIMIT=64] -->
+ <string name="voicemail_status_configure_voicemail">Set up your voicemail</string>
+ <!-- Voicemail status message shown at the top of call details screen to notify the user that
+ the audio of this voicemail is not available. [CHAR LIMIT=64] -->
+ <string name="voicemail_status_audio_not_available">Audio not available</string>
+
+ <!-- User action prompt shown next to a voicemail status message to let the user configure
+ visual voicemail. [CHAR LIMIT=20] -->
+ <string name="voicemail_status_action_configure">Set up</string>
+ <!-- User action prompt shown next to a voicemail status message to let the user call voicemail
+ server directly to listen to the voicemails. [CHAR LIMIT=20] -->
+ <string name="voicemail_status_action_call_server">Call voicemail</string>
+
+</resources>
diff --git a/java/com/android/dialer/widget/AndroidManifest.xml b/java/com/android/dialer/widget/AndroidManifest.xml
new file mode 100644
index 0000000..f104cc1
--- /dev/null
+++ b/java/com/android/dialer/widget/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.widget">
+</manifest>
diff --git a/java/com/android/dialer/widget/ResizingTextEditText.java b/java/com/android/dialer/widget/ResizingTextEditText.java
new file mode 100644
index 0000000..fb894bd
--- /dev/null
+++ b/java/com/android/dialer/widget/ResizingTextEditText.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.EditText;
+import com.android.dialer.util.ViewUtil;
+
+/** EditText which resizes dynamically with respect to text length. */
+public class ResizingTextEditText extends EditText {
+
+ private final int mOriginalTextSize;
+ private final int mMinTextSize;
+
+ public ResizingTextEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mOriginalTextSize = (int) getTextSize();
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResizingText);
+ mMinTextSize =
+ (int) a.getDimension(R.styleable.ResizingText_resizing_text_min_size, mOriginalTextSize);
+ a.recycle();
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+}
diff --git a/java/com/android/dialer/widget/ResizingTextTextView.java b/java/com/android/dialer/widget/ResizingTextTextView.java
new file mode 100644
index 0000000..9b62441
--- /dev/null
+++ b/java/com/android/dialer/widget/ResizingTextTextView.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.TextView;
+import com.android.dialer.util.ViewUtil;
+
+/** TextView which resizes dynamically with respect to text length. */
+public class ResizingTextTextView extends TextView {
+
+ private final int mOriginalTextSize;
+ private final int mMinTextSize;
+
+ public ResizingTextTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mOriginalTextSize = (int) getTextSize();
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResizingText);
+ mMinTextSize =
+ (int) a.getDimension(R.styleable.ResizingText_resizing_text_min_size, mOriginalTextSize);
+ a.recycle();
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+}
diff --git a/java/com/android/dialer/widget/res/values/attrs.xml b/java/com/android/dialer/widget/res/values/attrs.xml
new file mode 100644
index 0000000..bd5c3a4
--- /dev/null
+++ b/java/com/android/dialer/widget/res/values/attrs.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ Copyright (C) 2012 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>
+
+ <declare-styleable name="ResizingText">
+ <attr format="dimension" name="resizing_text_min_size"/>
+ </declare-styleable>
+
+</resources>