IntentCompat.java

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.content;

import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.core.util.Preconditions.checkNotNull;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;

import java.lang.annotation.Retention;
import java.util.List;

/**
 * Helper for accessing features in {@link android.content.Intent}.
 */
public final class IntentCompat {
    private IntentCompat() {
        /* Hide constructor */
    }

    /**
     * Activity Action: Creates a reminder.
     * <p>Input: {@link android.content.Intent#EXTRA_TITLE} The title of the
     * reminder that will be shown to the user.
     * {@link android.content.Intent#EXTRA_TEXT} The reminder text that will be
     * shown to the user. The intent should at least specify a title or a text.
     * {@link #EXTRA_TIME} The time when the reminder will
     * be shown to the user. The time is specified in milliseconds since the
     * Epoch (optional).
     * </p>
     * <p>Output: Nothing.</p>
     *
     * @see android.content.Intent#EXTRA_TITLE
     * @see android.content.Intent#EXTRA_TEXT
     * @see #EXTRA_TIME
     */
    @SuppressLint("ActionValue")
    public static final String ACTION_CREATE_REMINDER = "android.intent.action.CREATE_REMINDER";

    /**
     * Activity action: creates an intent to redirect the user to UI to turn on/off their
     * unused app restriction settings.
     */
    @SuppressLint("ActionValue")
    public static final String ACTION_UNUSED_APP_RESTRICTIONS =
            "android.intent.action.AUTO_REVOKE_PERMISSIONS";

    /**
     * A constant String that is associated with the Intent, used with
     * {@link android.content.Intent#ACTION_SEND} to supply an alternative to
     * {@link android.content.Intent#EXTRA_TEXT}
     * as HTML formatted text.  Note that you <em>must</em> also supply
     * {@link android.content.Intent#EXTRA_TEXT}.
     */
    public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT";

    /**
     * Used as a boolean extra field in {@link android.content.Intent#ACTION_VIEW} intents to
     * indicate that content should immediately be played without any intermediate screens that
     * require additional user input, e.g. a profile selection screen or a details page.
     */
    public static final String EXTRA_START_PLAYBACK = "android.intent.extra.START_PLAYBACK";

    /**
     * Optional extra specifying a time in milliseconds since the Epoch. The value must be
     * non-negative.
     * <p>
     * Type: long
     * </p>
     */
    @SuppressLint("ActionValue")
    public static final String EXTRA_TIME = "android.intent.extra.TIME";

    /**
     * Indicates an activity optimized for Leanback mode, and that should
     * be displayed in the Leanback launcher.
     */
    public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";

    /** The status of Unused App Restrictions features is unknown for this app. */
    public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0;

    /** There are no available Unused App Restrictions features for this app. */
    public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1;

    /**
     * Permission revocation is enabled for this app (i.e. permissions will be automatically
     * reset if the app is unused).
     *
     * Note: this also means that app hibernation is not available for this app.
     */
    public static final int PERMISSION_REVOCATION_ENABLED = 2;

    /**
     * Permission revocation is disabled for this app (i.e. this app is exempt from having
     * its permissions automatically removed).
     *
     * Note: this also means that app hibernation is not available for this app.
     */
    public static final int PERMISSION_REVOCATION_DISABLED = 3;

    /**
     * App hibernation is enabled for this app (i.e. this app will be hibernated and have its
     * permissions revoked if the app is unused).
     *
     * Note: this also means that permission revocation is enabled for this app.
     */
    public static final int APP_HIBERNATION_ENABLED = 4;

    /**
     * App hibernation is disabled for this app (i.e. this app is exempt from being hibernated).
     *
     * Note: this also means that permission revocation is disabled for this app.
     */
    public static final int APP_HIBERNATION_DISABLED = 5;

    /**
     * The status of Unused App Restrictions features for this app.
     * @hide
     */
    @IntDef({UNUSED_APP_RESTRICTION_STATUS_UNKNOWN, UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE,
            PERMISSION_REVOCATION_ENABLED, PERMISSION_REVOCATION_DISABLED,
            APP_HIBERNATION_ENABLED, APP_HIBERNATION_DISABLED})
    @Retention(SOURCE)
    @RestrictTo(LIBRARY)
    public @interface UnusedAppRestrictionsStatus {
    }

    /**
     * Make an Intent for the main activity of an application, without
     * specifying a specific activity to run but giving a selector to find
     * the activity.  This results in a final Intent that is structured
     * the same as when the application is launched from
     * Home.  For anything else that wants to launch an application in the
     * same way, it is important that they use an Intent structured the same
     * way, and can use this function to ensure this is the case.
     *
     * <p>The returned Intent has {@link Intent#ACTION_MAIN} as its action, and includes the
     * category {@link Intent#CATEGORY_LAUNCHER}.  This does <em>not</em> have
     * {@link Intent#FLAG_ACTIVITY_NEW_TASK} set, though typically you will want
     * to do that through {@link Intent#addFlags(int)} on the returned Intent.
     *
     * @param selectorAction The action name of the Intent's selector.
     * @param selectorCategory The name of a category to add to the Intent's
     * selector.
     * @return Returns a newly created Intent that can be used to launch the
     * activity as a main application entry.
     */
    @NonNull
    public static Intent makeMainSelectorActivity(@NonNull String selectorAction,
            @NonNull String selectorCategory) {
        if (Build.VERSION.SDK_INT >= 15) {
            return Intent.makeMainSelectorActivity(selectorAction, selectorCategory);
        } else {
            // Before api 15 you couldn't set a selector intent.
            // Fall back and just return an intent with the requested action/category,
            // even though it won't be a proper "main" intent.
            Intent intent = new Intent(selectorAction);
            intent.addCategory(selectorCategory);
            return intent;
        }
    }

    /**
     * Make an Intent to redirect the user to UI to manage their unused app restriction settings
     * for a particular app (e.g. permission revocation, app hibernation).
     *
     * Note: developers must first call {@link #getUnusedAppRestrictionsStatus(Context)} to make
     * sure that unused app restriction features are available on the device before attempting to
     * create an intent using this method. Any return value of this method besides
     * {@link #UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE} indicates that at least one
     * unused app restriction feature is available on the device. If the return value _is_
     * {@link #UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE}, this method will throw an
     * {@link UnsupportedOperationException}.
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 31 and above, this method generates an intent with action {@code Intent
     * .ACTION_APPLICATION_DETAILS_SETTINGS} and {@code packageName} as data.
     * <li>SDK 30, this method generates an intent with action {@code Intent
     * .ACTION_AUTO_REVOKE_PERMISSIONS} and {@code packageName} as data.
     * <li>SDK 23 through 29, this method will generate an intent with action
     * {@link Intent#ACTION_AUTO_REVOKE_PERMISSIONS} and the package as the app with the Verifier
     * role that can resolve the intent.
     * <li>SDK 22 and below, this method will throw an {@link UnsupportedOperationException}
     * </ul>
     *
     * @param context The {@link Context} of the calling application.
     * @param packageName The package name of the calling application.
     *
     * @return Returns a newly created Intent that can be used to launch an activity where users
     * can manage unused app restrictions for a specific app.
     */
    @NonNull
    public static Intent createManageUnusedAppRestrictionsIntent(@NonNull Context context,
            @NonNull String packageName) {
        if (getUnusedAppRestrictionsStatus(context)
                == UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE) {
            throw new UnsupportedOperationException(
                    "Unused App Restriction features are not available on this device");
        }

        // If the OS version is S+, generate the intent using the Application Details Settings
        // intent action to support compatibility with the App Hibernation feature
        // TODO: replace with VERSION_CODES.S once it's defined
        if (Build.VERSION.SDK_INT >= 31) {
            return new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
                    .setData(Uri.fromParts("package", packageName, /* fragment= */ null));
        }

        Intent unusedAppRestrictionsIntent =
                new Intent(ACTION_UNUSED_APP_RESTRICTIONS)
                        .setData(Uri.fromParts(
                                "package", packageName, /* fragment= */ null));

        // If the OS version is R, then no need to add any other data or flags, since we're
        // relying on the Android R system feature.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            return unusedAppRestrictionsIntent;
        } else {
            // Only allow apps with the Verifier role to resolve the intent.
            String verifierPackageName = getVerifierRolePackageName(context.getPackageManager());
            // The Verifier package name shouldn't be null since we've already checked that there
            // exists a Verifier on the device, but nonetheless we double-check here.
            return unusedAppRestrictionsIntent
                    .setPackage(checkNotNull(verifierPackageName));
        }
    }

    /**
     * Returns the package name of the one and only Verifier on the device. If none exist, this
     * will return {@code null}. Likewise, if multiple Verifiers exist, this method will return
     * the first Verifier's package name.
     */
    @Nullable
    private static String getVerifierRolePackageName(PackageManager packageManager) {
        Intent unusedAppRestrictionsIntent =
                new Intent(ACTION_UNUSED_APP_RESTRICTIONS)
                        .setData(Uri.fromParts(
                                "package", "com.example", /* fragment= */ null));
        List<ResolveInfo> intentResolvers =
                packageManager.queryIntentActivities(unusedAppRestrictionsIntent, /* flags= */ 0);

        String verifierPackageName = null;

        for (ResolveInfo intentResolver: intentResolvers) {
            String packageName = intentResolver.activityInfo.packageName;
            if (packageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
                    packageName) != PackageManager.PERMISSION_GRANTED) {
                continue;
            }

            if (verifierPackageName != null) {
                // This shouldn't happen, but we fail gracefully nonetheless and avoid throwing an
                // exception, instead returning the first package name with the Verifier role
                // that we found.
                return verifierPackageName;
            }
            verifierPackageName = packageName;
        }

        return verifierPackageName;
    }

    /**
     * Returns the status of Unused App Restriction features for the current application, i.e.
     * whether the features are available and if so, enabled for the application.
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 31 and above, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this
     * will return {@link #APP_HIBERNATION_ENABLED}. Else, it will return
     * {@link #APP_HIBERNATION_DISABLED}.</li>
     * <li>SDK 30, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this will return
     * {@link #PERMISSION_REVOCATION_ENABLED}. Else, it will return
     * {@link #PERMISSION_REVOCATION_DISABLED}.</li>
     * <li>SDK 23 through 29, if there exists an app with the Verifier role that can resolve the
     * {@code Intent.ACTION_AUTO_REVOKE_PERMISSIONS} action.
     * <li>SDK 22 and below, this method always returns
     * {@link #UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE} as runtime permissions did not exist
     * yet.
     * </ul>
     */
    public static @UnusedAppRestrictionsStatus int getUnusedAppRestrictionsStatus(
            @NonNull Context context) {
        // Return false if the Android OS version is before M, because Android M introduced runtime
        // permissions
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE;
        }

        // TODO: replace with VERSION_CODES.S once it's defined
        if (Build.VERSION.SDK_INT >= 31) {
            return Api30Impl.areUnusedAppRestrictionsEnabled(context)
                    ? APP_HIBERNATION_ENABLED
                    : APP_HIBERNATION_DISABLED;
        }

        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
            return Api30Impl.areUnusedAppRestrictionsEnabled(context)
                    ? PERMISSION_REVOCATION_ENABLED
                    : PERMISSION_REVOCATION_DISABLED;
        }

        // Else, check for an app with the verifier role that can resolve the intent
        String verifierPackageName = getVerifierRolePackageName(context.getPackageManager());
        // Check that we were able to get the one Verifier's package name. If no Verifier or
        // more than one Verifier exists on the device, unused app restrictions are not available
        // on the device.
        return (verifierPackageName == null)
                ? UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE
                // TODO(b/177234481): Implement the backport behavior of this API
                : UNUSED_APP_RESTRICTION_STATUS_UNKNOWN;
    }

    /**
     * We create this static class to avoid Class Verification Failures from referencing a method
     * only added in Android R.
     *
     * <p>Gating references on SDK checks does not address class verification failures, hence the
     * need for this inner class.
     */
    @RequiresApi(Build.VERSION_CODES.R)
    private static class Api30Impl {
        private Api30Impl() {}
        static boolean areUnusedAppRestrictionsEnabled(@NonNull Context context) {
            // If the app is allowlisted, that means that it is exempt from unused app restriction
            // features, and thus the features are _disabled_.
            return !context.getPackageManager().isAutoRevokeWhitelisted();
        }
    }
}