blob: 68e551f212cfedf64e3a2db524e5bd08e52329dc [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.TransitionDrawable;
import android.graphics.drawable.VectorDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.Process;
import android.os.StatFs;
import android.os.StrictMode;
import android.os.UserManager;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.ImageViewCompat;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.textclassifier.TextClassifier;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import java.io.File;
import java.io.UnsupportedEncodingException;
/**
* Utility class to use new APIs that were added after KitKat (API level 19).
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class ApiCompatibilityUtils {
private ApiCompatibilityUtils() {
}
/**
* Compares two long values numerically. The value returned is identical to what would be
* returned by {@link Long#compare(long, long)} which is available since API level 19.
*/
public static int compareLong(long lhs, long rhs) {
return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1);
}
/**
* Compares two boolean values. The value returned is identical to what would be returned by
* {@link Boolean#compare(boolean, boolean)} which is available since API level 19.
*/
public static int compareBoolean(boolean lhs, boolean rhs) {
return lhs == rhs ? 0 : lhs ? 1 : -1;
}
/**
* Checks that the object reference is not null and throws NullPointerException if it is.
* See {@link Objects#requireNonNull} which is available since API level 19.
* @param obj The object to check
*/
@NonNull
public static <T> T requireNonNull(T obj) {
if (obj == null) throw new NullPointerException();
return obj;
}
/**
* Checks that the object reference is not null and throws NullPointerException if it is.
* See {@link Objects#requireNonNull} which is available since API level 19.
* @param obj The object to check
* @param message The message to put into NullPointerException
*/
@NonNull
public static <T> T requireNonNull(T obj, String message) {
if (obj == null) throw new NullPointerException(message);
return obj;
}
/**
* {@link String#getBytes()} but specifying UTF-8 as the encoding and capturing the resulting
* UnsupportedEncodingException.
*/
public static byte[] getBytesUtf8(String str) {
try {
return str.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 encoding not available.", e);
}
}
/**
* @return True if the running version of the Android supports printing.
*/
public static boolean isPrintingSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
}
/**
* @return True if the running version of the Android supports elevation. Elevation of a view
* determines the visual appearance of its shadow.
*/
public static boolean isElevationSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
/**
* @see android.text.Html#toHtml(Spanned, int)
* @param option is ignored on below N
*/
@SuppressWarnings("deprecation")
public static String toHtml(Spanned spanned, int option) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.toHtml(spanned, option);
} else {
return Html.toHtml(spanned);
}
}
// These methods have a new name, and the old name is deprecated.
/**
* @see android.app.Activity#finishAndRemoveTask()
*/
public static void finishAndRemoveTask(Activity activity) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
activity.finishAndRemoveTask();
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
// crbug.com/395772 : Fallback for Activity.finishAndRemoveTask() failing.
new FinishAndRemoveTaskWithRetry(activity).run();
} else {
activity.finish();
}
}
/**
* Set elevation if supported.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean setElevation(View view, float elevationValue) {
if (!isElevationSupported()) return false;
view.setElevation(elevationValue);
return true;
}
/**
* Set elevation if supported.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean setElevation(PopupWindow window, float elevationValue) {
if (!isElevationSupported()) return false;
window.setElevation(elevationValue);
return true;
}
/**
* Gets an intent to start the Android system notification settings activity for an app.
*
* @param context Context of the app whose settings intent should be returned.
*/
public static Intent getNotificationSettingsIntent(Context context) {
Intent intent = new Intent();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
} else {
intent.setAction("android.settings.ACTION_APP_NOTIFICATION_SETTINGS");
intent.putExtra("app_package", context.getPackageName());
intent.putExtra("app_uid", context.getApplicationInfo().uid);
}
return intent;
}
private static class FinishAndRemoveTaskWithRetry implements Runnable {
private static final long RETRY_DELAY_MS = 500;
private static final long MAX_TRY_COUNT = 3;
private final Activity mActivity;
private int mTryCount;
FinishAndRemoveTaskWithRetry(Activity activity) {
mActivity = activity;
}
@Override
public void run() {
mActivity.finishAndRemoveTask();
mTryCount++;
if (!mActivity.isFinishing()) {
if (mTryCount < MAX_TRY_COUNT) {
ThreadUtils.postOnUiThreadDelayed(this, RETRY_DELAY_MS);
} else {
mActivity.finish();
}
}
}
}
/**
* @return Whether the screen of the device is interactive.
*/
@SuppressWarnings("deprecation")
public static boolean isInteractive(Context context) {
PowerManager manager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
return manager.isInteractive();
} else {
return manager.isScreenOn();
}
}
@SuppressWarnings("deprecation")
public static int getActivityNewDocumentFlag() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
} else {
return Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
}
}
/**
* @see android.provider.Settings.Secure#SKIP_FIRST_USE_HINTS
*/
public static boolean shouldSkipFirstUseHints(ContentResolver contentResolver) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return Settings.Secure.getInt(
contentResolver, Settings.Secure.SKIP_FIRST_USE_HINTS, 0) != 0;
} else {
return false;
}
}
/**
* @param activity Activity that should get the task description update.
* @param title Title of the activity.
* @param icon Icon of the activity.
* @param color Color of the activity. It must be a fully opaque color.
*/
public static void setTaskDescription(Activity activity, String title, Bitmap icon, int color) {
// TaskDescription requires an opaque color.
assert Color.alpha(color) == 255;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ActivityManager.TaskDescription description =
new ActivityManager.TaskDescription(title, icon, color);
activity.setTaskDescription(description);
}
}
/**
* @see android.view.Window#setStatusBarColor(int color).
*/
public static void setStatusBarColor(Window window, int statusBarColor) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
// If both system bars are black, we can remove these from our layout,
// removing or shrinking the SurfaceFlinger overlay required for our views.
// This benefits battery usage on L and M. However, this no longer provides a battery
// benefit as of N and starts to cause flicker bugs on O, so don't bother on O and up.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && statusBarColor == Color.BLACK
&& window.getNavigationBarColor() == Color.BLACK) {
window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
}
window.setStatusBarColor(statusBarColor);
}
/**
* Sets the status bar icons to dark or light. Note that this is only valid for
* Android M+.
*
* @param rootView The root view used to request updates to the system UI theming.
* @param useDarkIcons Whether the status bar icons should be dark.
*/
public static void setStatusBarIconColor(View rootView, boolean useDarkIcons) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
int systemUiVisibility = rootView.getSystemUiVisibility();
if (useDarkIcons) {
systemUiVisibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
} else {
systemUiVisibility &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
}
rootView.setSystemUiVisibility(systemUiVisibility);
}
/**
* @see android.content.res.Resources#getDrawable(int id).
* TODO(ltian): use {@link AppCompatResources} to parse drawable to prevent fail on
* {@link VectorDrawable}. (http://crbug.com/792129)
*/
@SuppressWarnings("deprecation")
public static Drawable getDrawable(Resources res, int id) throws NotFoundException {
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return res.getDrawable(id, null);
} else {
return res.getDrawable(id);
}
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
public static void setImageTintList(
@NonNull ImageView view, @Nullable ColorStateList tintList) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
// Work around broken workaround in ImageViewCompat, see https://crbug.com/891609#c3.
if (tintList != null && view.getImageTintMode() == null) {
view.setImageTintMode(PorterDuff.Mode.SRC_IN);
}
}
ImageViewCompat.setImageTintList(view, tintList);
}
/**
* @see android.content.res.Resources#getDrawableForDensity(int id, int density).
*/
@SuppressWarnings("deprecation")
public static Drawable getDrawableForDensity(Resources res, int id, int density) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return res.getDrawableForDensity(id, density, null);
} else {
return res.getDrawableForDensity(id, density);
}
}
/**
* @see android.app.Activity#finishAfterTransition().
*/
public static void finishAfterTransition(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.finishAfterTransition();
} else {
activity.finish();
}
}
/**
* @see android.content.pm.PackageManager#getUserBadgedIcon(Drawable, android.os.UserHandle).
*/
public static Drawable getUserBadgedIcon(Context context, int id) {
Drawable drawable = getDrawable(context.getResources(), id);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
PackageManager packageManager = context.getPackageManager();
drawable = packageManager.getUserBadgedIcon(drawable, Process.myUserHandle());
}
return drawable;
}
/**
* @see android.content.pm.PackageManager#getUserBadgedDrawableForDensity(Drawable drawable,
* UserHandle user, Rect badgeLocation, int badgeDensity).
*/
public static Drawable getUserBadgedDrawableForDensity(
Context context, Drawable drawable, Rect badgeLocation, int density) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
PackageManager packageManager = context.getPackageManager();
return packageManager.getUserBadgedDrawableForDensity(
drawable, Process.myUserHandle(), badgeLocation, density);
}
return drawable;
}
/**
* @see android.content.res.Resources#getColor(int id).
*/
@SuppressWarnings("deprecation")
public static int getColor(Resources res, int id) throws NotFoundException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return res.getColor(id, null);
} else {
return res.getColor(id);
}
}
/**
* @see android.graphics.drawable.Drawable#getColorFilter().
*/
@SuppressWarnings("NewApi")
public static ColorFilter getColorFilter(Drawable drawable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return drawable.getColorFilter();
} else {
return null;
}
}
/**
* @see android.widget.TextView#setTextAppearance(int id).
*/
@SuppressWarnings("deprecation")
public static void setTextAppearance(TextView view, int id) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.setTextAppearance(id);
} else {
view.setTextAppearance(view.getContext(), id);
}
}
/**
* See {@link android.os.StatFs#getAvailableBlocksLong}.
*/
@SuppressWarnings("deprecation")
public static long getAvailableBlocks(StatFs statFs) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return statFs.getAvailableBlocksLong();
} else {
return statFs.getAvailableBlocks();
}
}
/**
* See {@link android.os.StatFs#getBlockCount}.
*/
@SuppressWarnings("deprecation")
public static long getBlockCount(StatFs statFs) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return statFs.getBlockCountLong();
} else {
return statFs.getBlockCount();
}
}
/**
* See {@link android.os.StatFs#getBlockSize}.
*/
@SuppressWarnings("deprecation")
public static long getBlockSize(StatFs statFs) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return statFs.getBlockSizeLong();
} else {
return statFs.getBlockSize();
}
}
/**
* @param context The Android context, used to retrieve the UserManager system service.
* @return Whether the device is running in demo mode.
*/
@SuppressWarnings("NewApi")
public static boolean isDemoUser(Context context) {
// UserManager#isDemoUser() is only available in Android NMR1+.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return false;
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
return userManager.isDemoUser();
}
/**
* @see Context#checkPermission(String, int, int)
*/
public static int checkPermission(Context context, String permission, int pid, int uid) {
try {
return context.checkPermission(permission, pid, uid);
} catch (RuntimeException e) {
// Some older versions of Android throw odd errors when checking for permissions, so
// just swallow the exception and treat it as the permission is denied.
// crbug.com/639099
return PackageManager.PERMISSION_DENIED;
}
}
/**
* @see android.view.inputmethod.InputMethodSubType#getLocate()
*/
@SuppressWarnings("deprecation")
public static String getLocale(InputMethodSubtype inputMethodSubType) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return inputMethodSubType.getLanguageTag();
} else {
return inputMethodSubType.getLocale();
}
}
/**
* Get the URI for a downloaded file.
*
* @param file A downloaded file.
* @return URI for |file|.
*/
public static Uri getUriForDownloadedFile(File file) {
return Build.VERSION.SDK_INT > Build.VERSION_CODES.M
? FileUtils.getUriForFile(file)
: Uri.fromFile(file);
}
/**
* @see android.view.Window#FEATURE_INDETERMINATE_PROGRESS
*/
public static void setWindowIndeterminateProgress(Window window) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
@SuppressWarnings("deprecation")
int featureNumber = Window.FEATURE_INDETERMINATE_PROGRESS;
@SuppressWarnings("deprecation")
int featureValue = Window.PROGRESS_VISIBILITY_OFF;
window.setFeatureInt(featureNumber, featureValue);
}
}
/**
* @param activity The {@link Activity} to check.
* @return Whether or not {@code activity} is currently in Android N+ multi-window mode.
*/
public static boolean isInMultiWindowMode(Activity activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return false;
}
return activity.isInMultiWindowMode();
}
/**
* Disables the Smart Select {@link TextClassifier} for the given {@link TextView} instance.
* @param textView The {@link TextView} that should have its classifier disabled.
*/
@TargetApi(Build.VERSION_CODES.O)
public static void disableSmartSelectionTextClassifier(TextView textView) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
textView.setTextClassifier(TextClassifier.NO_OP);
}
/**
* Creates an ActivityOptions Bundle with basic options and the LaunchDisplayId set.
* @param displayId The id of the display to launch into.
* @return The created bundle, or null if unsupported.
*/
public static Bundle createLaunchDisplayIdActivityOptions(int displayId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(displayId);
return options.toBundle();
}
/**
* @see View#setAccessibilityTraversalBefore(int)
*/
public static void setAccessibilityTraversalBefore(View view, int viewFocusedAfter) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
view.setAccessibilityTraversalBefore(viewFocusedAfter);
}
}
/**
* Creates regular LayerDrawable on Android L+. On older versions creates a helper class that
* fixes issues around {@link LayerDrawable#mutate()}. See https://crbug.com/890317 for details.
* See also {@link #createTransitionDrawable}.
* @param layers A list of drawables to use as layers in this new drawable.
*/
public static LayerDrawable createLayerDrawable(@NonNull Drawable[] layers) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
return new LayerDrawableCompat(layers);
}
return new LayerDrawable(layers);
}
/**
* Creates regular TransitionDrawable on Android L+. On older versions creates a helper class
* that fixes issues around {@link TransitionDrawable#mutate()}. See https://crbug.com/892061
* for details. See also {@link #createLayerDrawable}.
* @param layers A list of drawables to use as layers in this new drawable.
*/
public static TransitionDrawable createTransitionDrawable(@NonNull Drawable[] layers) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
return new TransitionDrawableCompat(layers);
}
return new TransitionDrawable(layers);
}
/**
* Adds a content description to the provided EditText password field on versions of Android
* where the hint text is not used for accessibility. Does nothing if the EditText field does
* not have a password input type or the hint text is empty. See https://crbug.com/911762.
*
* @param view The EditText password field.
*/
public static void setPasswordEditTextContentDescription(EditText view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) return;
if (isPasswordInputType(view.getInputType()) && !TextUtils.isEmpty(view.getHint())) {
view.setContentDescription(view.getHint());
}
}
private static boolean isPasswordInputType(int inputType) {
final int variation =
inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
return variation == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
|| variation
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
|| variation
== (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
}
private static class LayerDrawableCompat extends LayerDrawable {
private boolean mMutated;
LayerDrawableCompat(@NonNull Drawable[] layers) {
super(layers);
}
@NonNull
@Override
public Drawable mutate() {
// LayerDrawable in Android K loses bounds of layers, so this method works around that.
if (mMutated) {
// This object has already been mutated and shouldn't have any shared state.
return this;
}
Rect[] oldBounds = getLayersBounds(this);
Drawable superResult = super.mutate();
// LayerDrawable.mutate() always returns this, bail out if this isn't the case.
if (superResult != this) return superResult;
restoreLayersBounds(this, oldBounds);
mMutated = true;
return this;
}
}
private static class TransitionDrawableCompat extends TransitionDrawable {
private boolean mMutated;
TransitionDrawableCompat(@NonNull Drawable[] layers) {
super(layers);
}
@NonNull
@Override
public Drawable mutate() {
// LayerDrawable in Android K loses bounds of layers, so this method works around that.
if (mMutated) {
// This object has already been mutated and shouldn't have any shared state.
return this;
}
Rect[] oldBounds = getLayersBounds(this);
Drawable superResult = super.mutate();
// TransitionDrawable.mutate() always returns this, bail out if this isn't the case.
if (superResult != this) return superResult;
restoreLayersBounds(this, oldBounds);
mMutated = true;
return this;
}
}
/**
* Helper for {@link LayerDrawableCompat#mutate} and {@link TransitionDrawableCompat#mutate}.
* Obtains the bounds of layers so they can be restored after a mutation.
*/
private static Rect[] getLayersBounds(LayerDrawable layerDrawable) {
Rect[] result = new Rect[layerDrawable.getNumberOfLayers()];
for (int i = 0; i < layerDrawable.getNumberOfLayers(); i++) {
result[i] = layerDrawable.getDrawable(i).getBounds();
}
return result;
}
/**
* Helper for {@link LayerDrawableCompat#mutate} and {@link TransitionDrawableCompat#mutate}.
* Restores the bounds of layers after a mutation.
*/
private static void restoreLayersBounds(LayerDrawable layerDrawable, Rect[] oldBounds) {
assert layerDrawable.getNumberOfLayers() == oldBounds.length;
for (int i = 0; i < layerDrawable.getNumberOfLayers(); i++) {
layerDrawable.getDrawable(i).setBounds(oldBounds[i]);
}
}
}