// 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.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; 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.Rect; import android.graphics.drawable.Drawable; 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.text.Html; import android.text.Spanned; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.InputMethodSubtype; import android.view.textclassifier.TextClassifier; import android.widget.TextView; import java.io.File; import java.io.UnsupportedEncodingException; /** * Utility class to use new APIs that were added after ICS (API level 14). */ @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 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 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); } } /** * Returns true if view's layout direction is right-to-left. * * @param view the View whose layout is being considered */ public static boolean isLayoutRtl(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } else { // All layouts are LTR before JB MR1. return false; } } /** * @see Configuration#getLayoutDirection() */ public static int getLayoutDirection(Configuration configuration) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return configuration.getLayoutDirection(); } else { // All layouts are LTR before JB MR1. return View.LAYOUT_DIRECTION_LTR; } } /** * @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.view.View#setLayoutDirection(int) */ public static void setLayoutDirection(View view, int layoutDirection) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { view.setLayoutDirection(layoutDirection); } else { // Do nothing. RTL layouts aren't supported before JB MR1. } } /** * @see android.view.View#setTextAlignment(int) */ public static void setTextAlignment(View view, int textAlignment) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { view.setTextAlignment(textAlignment); } else { // Do nothing. RTL text isn't supported before JB MR1. } } /** * @see android.view.View#setTextDirection(int) */ public static void setTextDirection(View view, int textDirection) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { view.setTextDirection(textDirection); } else { // Do nothing. RTL text isn't supported before JB MR1. } } /** * See {@link android.view.View#setLabelFor(int)}. */ public static void setLabelFor(View labelView, int id) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { labelView.setLabelFor(id); } else { // Do nothing. #setLabelFor() isn't supported before JB MR1. } } /** * @see android.widget.TextView#getCompoundDrawablesRelative() */ public static Drawable[] getCompoundDrawablesRelative(TextView textView) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return textView.getCompoundDrawablesRelative(); } else { return textView.getCompoundDrawables(); } } /** * @see android.widget.TextView#setCompoundDrawablesRelative(Drawable, Drawable, Drawable, * Drawable) */ public static void setCompoundDrawablesRelative(TextView textView, Drawable start, Drawable top, Drawable end, Drawable bottom) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { // On JB MR1, due to a platform bug, setCompoundDrawablesRelative() is a no-op if the // view has ever been measured. As a workaround, use setCompoundDrawables() directly. // See: http://crbug.com/368196 and http://crbug.com/361709 boolean isRtl = isLayoutRtl(textView); textView.setCompoundDrawables(isRtl ? end : start, top, isRtl ? start : end, bottom); } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { textView.setCompoundDrawablesRelative(start, top, end, bottom); } else { textView.setCompoundDrawables(start, top, end, bottom); } } /** * @see android.widget.TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable, * Drawable, Drawable, Drawable) */ public static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView, Drawable start, Drawable top, Drawable end, Drawable bottom) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { // Work around the platform bug described in setCompoundDrawablesRelative() above. boolean isRtl = isLayoutRtl(textView); textView.setCompoundDrawablesWithIntrinsicBounds(isRtl ? end : start, top, isRtl ? start : end, bottom); } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); } else { textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); } } /** * @see android.widget.TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(int, int, int, * int) */ public static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView, int start, int top, int end, int bottom) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { // Work around the platform bug described in setCompoundDrawablesRelative() above. boolean isRtl = isLayoutRtl(textView); textView.setCompoundDrawablesWithIntrinsicBounds(isRtl ? end : start, top, isRtl ? start : end, bottom); } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); } else { textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); } } /** * @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.PendingIntent#getCreatorPackage() */ @SuppressWarnings("deprecation") public static String getCreatorPackage(PendingIntent intent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return intent.getCreatorPackage(); } else { return intent.getTargetPackage(); } } /** * @see android.provider.Settings.Global#DEVICE_PROVISIONED */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static boolean isDeviceProvisioned(Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) return true; if (context == null) return true; if (context.getContentResolver() == null) return true; return Settings.Global.getInt( context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0; } /** * @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; } /** * 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); } } /** * @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 a URI for |file| which has the image capture. This function assumes that path of |file| * is based on the result of UiUtils.getDirectoryForImageCapture(). * * @param file image capture file. * @return URI for |file|. */ public static Uri getUriForImageCaptureFile(File file) { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 ? ContentUriUtils.getContentUriFromFile(file) : Uri.fromFile(file); } /** * 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(); } }