blob: e9dd36d38a4f268cbf8cc61dd34731fc41f49e23 [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.app.PendingIntent;
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.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.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
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.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodSubtype;
import android.view.textclassifier.TextClassifier;
import android.widget.ImageView;
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> 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);
}
}
/**
* 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);
}
}
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 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();
}
/**
* @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.
* @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);
}
private static class LayerDrawableCompat extends LayerDrawable {
private boolean mMutated;
LayerDrawableCompat(@NonNull Drawable[] layers) {
super(layers);
}
@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;
}
// Save bounds before mutation.
Rect[] oldBounds = new Rect[getNumberOfLayers()];
for (int i = 0; i < getNumberOfLayers(); i++) {
oldBounds[i] = getDrawable(i).getBounds();
}
Drawable superResult = super.mutate();
if (superResult != this) {
// Unexpected, LayerDrawable.mutate() always returns this.
return superResult;
}
// Restore the saved bounds.
for (int i = 0; i < getNumberOfLayers(); i++) {
getDrawable(i).setBounds(oldBounds[i]);
}
mMutated = true;
return this;
}
}
}