summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/util
diff options
context:
space:
mode:
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>