summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/util
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/dialer/util
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
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
Diffstat (limited to 'java/com/android/dialer/util')
-rw-r--r--java/com/android/dialer/util/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/util/CallUtil.java135
-rw-r--r--java/com/android/dialer/util/DialerUtils.java246
-rw-r--r--java/com/android/dialer/util/DrawableConverter.java97
-rw-r--r--java/com/android/dialer/util/ExpirableCache.java269
-rw-r--r--java/com/android/dialer/util/IntentUtil.java78
-rw-r--r--java/com/android/dialer/util/MoreStrings.java64
-rw-r--r--java/com/android/dialer/util/OrientationUtil.java30
-rw-r--r--java/com/android/dialer/util/PermissionsUtil.java121
-rw-r--r--java/com/android/dialer/util/SettingsUtil.java95
-rw-r--r--java/com/android/dialer/util/TouchPointManager.java60
-rw-r--r--java/com/android/dialer/util/TransactionSafeActivity.java64
-rw-r--r--java/com/android/dialer/util/ViewUtil.java129
-rw-r--r--java/com/android/dialer/util/res/values/strings.xml42
14 files changed, 1433 insertions, 0 deletions
diff --git a/java/com/android/dialer/util/AndroidManifest.xml b/java/com/android/dialer/util/AndroidManifest.xml
new file mode 100644
index 000000000..499df9b4e
--- /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 000000000..81a4bb21e
--- /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 000000000..63f870e73
--- /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 000000000..5670315c9
--- /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 000000000..2778a572c
--- /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() {
+ * &#64;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>
+ * &#64;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 000000000..2f265b5a7
--- /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 000000000..5a43b1d10
--- /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 000000000..5a8d1ae0f
--- /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 000000000..70b96dfe1
--- /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 000000000..c61c09b6c
--- /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 000000000..74f87c477
--- /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 000000000..9b5e92ba8
--- /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 000000000..de08e41a7
--- /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 000000000..43ea6e31a
--- /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>