369 lines
14 KiB
Java
369 lines
14 KiB
Java
![]() |
// 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);
|
||
|
}
|
||
|
}
|