| // 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); |
| } |
| } |