blob: 33bfc1d8961f228993fbaefd2f6476e453635f00 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// 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.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.Application;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.graphics.Bitmap;
import android.graphics.ImageDecoder;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.os.UserManager;
import android.provider.MediaStore;
import android.provider.Settings;
import android.view.Display;
import android.view.View;
import android.view.textclassifier.TextClassifier;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class to use APIs not in all supported Android versions.
*
* Do not inline because we use many new APIs, and if they are inlined, they could cause dex
* validation errors on low Android versions.
*/
public class ApiCompatibilityUtils {
private static final String TAG = "ApiCompatUtil";
private ApiCompatibilityUtils() {}
@RequiresApi(Build.VERSION_CODES.Q)
private static class ApisQ {
static boolean isRunningInUserTestHarness() {
return ActivityManager.isRunningInUserTestHarness();
}
static List<Integer> getTargetableDisplayIds(@Nullable Activity activity) {
List<Integer> displayList = new ArrayList<>();
if (activity == null) return displayList;
DisplayManager displayManager =
(DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE);
if (displayManager == null) return displayList;
Display[] displays = displayManager.getDisplays();
ActivityManager am =
(ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
for (Display display : displays) {
if (display.getState() == Display.STATE_ON
&& am.isActivityStartAllowedOnDisplay(
activity,
display.getDisplayId(),
new Intent(activity, activity.getClass()))) {
displayList.add(display.getDisplayId());
}
}
return displayList;
}
}
@RequiresApi(Build.VERSION_CODES.P)
private static class ApisP {
static String getProcessName() {
return Application.getProcessName();
}
static Bitmap getBitmapByUri(ContentResolver cr, Uri uri) throws IOException {
return ImageDecoder.decodeBitmap(ImageDecoder.createSource(cr, uri));
}
}
@RequiresApi(Build.VERSION_CODES.O)
private static class ApisO {
static void initNotificationSettingsIntent(Intent intent, String packageName) {
intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
}
static void disableSmartSelectionTextClassifier(TextView textView) {
textView.setTextClassifier(TextClassifier.NO_OP);
}
static Bundle createLaunchDisplayIdActivityOptions(int displayId) {
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(displayId);
return options.toBundle();
}
}
// This class is sufficiently small that it's fine if it doesn't verify for N devices.
@RequiresApi(Build.VERSION_CODES.N_MR1)
private static class ApisNMR1 {
static boolean isDemoUser() {
UserManager userManager =
(UserManager)
ContextUtils.getApplicationContext()
.getSystemService(Context.USER_SERVICE);
return userManager.isDemoUser();
}
}
/**
* 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) {
return str.getBytes(StandardCharsets.UTF_8);
}
/**
* Gets an intent to start the Android system notification settings activity for an app.
*
*/
public static Intent getNotificationSettingsIntent() {
Intent intent = new Intent();
String packageName = ContextUtils.getApplicationContext().getPackageName();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ApisO.initNotificationSettingsIntent(intent, packageName);
} else {
intent.setAction("android.settings.ACTION_APP_NOTIFICATION_SETTINGS");
intent.putExtra("app_package", packageName);
intent.putExtra(
"app_uid", ContextUtils.getApplicationContext().getApplicationInfo().uid);
}
return intent;
}
/**
* @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)
*/
public static Drawable getDrawable(Resources res, int id) throws NotFoundException {
return getDrawableForDensity(res, id, 0);
}
/**
* @see android.content.res.Resources#getDrawableForDensity(int id, int density).
*/
@SuppressWarnings("deprecation")
public static Drawable getDrawableForDensity(Resources res, int id, int density) {
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
// For Android Oreo+, Resources.getDrawable(id, null) delegates to
// Resources.getDrawableForDensity(id, 0, null), but before that the two functions are
// independent. This check can be removed after Oreo becomes the minimum supported API.
if (density == 0) {
return res.getDrawable(id, null);
}
return res.getDrawableForDensity(id, density, null);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
/**
* @see android.content.res.Resources#getColor(int id).
*/
@SuppressWarnings("deprecation")
public static int getColor(Resources res, int id) throws NotFoundException {
return res.getColor(id);
}
/**
* @see android.widget.TextView#setTextAppearance(int id).
*/
@SuppressWarnings("deprecation")
public static void setTextAppearance(TextView view, int id) {
// setTextAppearance(id) is the undeprecated version of this, but it just calls the
// deprecated one, so there is no benefit to using the non-deprecated one until we can
// drop support for it entirely (new one was added in M).
view.setTextAppearance(view.getContext(), id);
}
/**
* @return Whether the device is running in demo mode.
*/
public static boolean isDemoUser() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && ApisNMR1.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;
}
}
/**
* @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) {
return activity.isInMultiWindowMode();
}
/**
* Get a list of ids of targetable displays, including the default display for the
* current activity. A set of targetable displays can only be determined on Q+. An empty list
* is returned if called on prior Q.
* @param activity The {@link Activity} to check.
* @return A list of display ids. Empty if there is none or version is less than Q, or
* windowAndroid does not contain an activity.
*/
@NonNull
public static List<Integer> getTargetableDisplayIds(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return ApisQ.getTargetableDisplayIds(activity);
}
return new ArrayList<>();
}
/**
* Disables the Smart Select {@link TextClassifier} for the given {@link TextView} instance.
* @param textView The {@link TextView} that should have its classifier disabled.
*/
public static void disableSmartSelectionTextClassifier(TextView textView) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ApisO.disableSmartSelectionTextClassifier(textView);
}
}
/**
* 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 ApisO.createLaunchDisplayIdActivityOptions(displayId);
}
return null;
}
/**
* Sets the mode {@link ActivityOptions#MODE_BACKGROUND_ACTIVITY_START_ALLOWED} to the
* given {@link ActivityOptions}. The options can be used to send {@link PendingIntent}
* passed to Chrome from a backgrounded app.
* @param options {@ActivityOptions} to set the required mode to.
*/
public static void setActivityOptionsBackgroundActivityStartMode(
@NonNull ActivityOptions options) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return;
options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
}
/**
* Sets the bottom handwriting bounds offset of the given view to 0.
* See https://crbug.com/1427112
* @param view The view on which to set the handwriting bounds.
*/
public static void clearHandwritingBoundsOffsetBottom(View view) {
// TODO(crbug.com/1427112): Replace uses of this method with direct calls once the API is
// available.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return;
// Set the bottom handwriting bounds offset to 0 so that the view doesn't intercept
// stylus events meant for the web contents.
try {
// float offsetTop = this.getHandwritingBoundsOffsetTop();
float offsetTop =
(float) View.class.getMethod("getHandwritingBoundsOffsetTop").invoke(view);
// float offsetLeft = this.getHandwritingBoundsOffsetLeft();
float offsetLeft =
(float) View.class.getMethod("getHandwritingBoundsOffsetLeft").invoke(view);
// float offsetRight = this.getHandwritingBoundsOffsetRight();
float offsetRight =
(float) View.class.getMethod("getHandwritingBoundsOffsetRight").invoke(view);
// this.setHandwritingBoundsOffsets(offsetLeft, offsetTop, offsetRight, 0);
Method setHandwritingBoundsOffsets =
View.class.getMethod(
"setHandwritingBoundsOffsets",
float.class,
float.class,
float.class,
float.class);
setHandwritingBoundsOffsets.invoke(view, offsetLeft, offsetTop, offsetRight, 0);
} catch (IllegalAccessException
| InvocationTargetException
| NoSuchMethodException
| NullPointerException e) {
// Do nothing.
}
}
// Access this via ContextUtils.getProcessName().
@SuppressWarnings("PrivateApi")
static String getProcessName() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return ApisP.getProcessName();
}
try {
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
return (String) activityThreadClazz.getMethod("currentProcessName").invoke(null);
} catch (Exception e) {
// If fallback logic is ever needed, refer to:
// https://chromium-review.googlesource.com/c/chromium/src/+/905563/1
throw new RuntimeException(e);
}
}
public static boolean isRunningInUserTestHarness() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return ApisQ.isRunningInUserTestHarness();
}
return false;
}
/** Retrieves an image for the given uri as a Bitmap. */
public static Bitmap getBitmapByUri(ContentResolver cr, Uri uri) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return ApisP.getBitmapByUri(cr, uri);
}
return MediaStore.Images.Media.getBitmap(cr, uri);
}
}