// 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 getTargetableDisplayIds(@Nullable Activity activity) { List 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 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) { 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 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); } }