chromium/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.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.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.StrictMode;
import android.os.UserManager;
import android.provider.MediaStore;
import android.view.Display;
import android.view.View;

import androidx.annotation.NonNull;

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() {}

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

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

    /**
     * @return Whether the device is running in demo mode.
     */
    public static boolean isDemoUser() {
        UserManager userManager =
                (UserManager)
                        ContextUtils.getApplicationContext().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;
        }
    }

    /**
     * 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) {
            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;
        }
        return new ArrayList<>();
    }

    /**
     * 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 mode {@link ActivityOptions#MODE_BACKGROUND_ACTIVITY_START_ALLOWED} to the given
     * {@link ActivityOptions}. The options can be used to create {@link PendingIntent}.
     *
     * @param options {@ActivityOptions} to set the required mode to.
     */
    public static void setCreatorActivityOptionsBackgroundActivityStartMode(
            @NonNull ActivityOptions options) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return;
        options.setPendingIntentCreatorBackgroundActivityStartMode(
                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/40261637): 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.
        }
    }

    public static boolean isRunningInUserTestHarness() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return ActivityManager.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 ImageDecoder.decodeBitmap(ImageDecoder.createSource(cr, uri));
        }
        return MediaStore.Images.Media.getBitmap(cr, uri);
    }
}