/*
 * Copyright (C) 2023 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 android.app.ecm;

import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TargetApi;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
import android.permission.flags.Flags;
import android.util.ArraySet;

import java.lang.annotation.Retention;

/**
 * This class provides the core API for ECM (Enhanced Confirmation Mode). ECM is a feature that
 * restricts access to protected **settings** (i.e., sensitive resources) by restricted **apps**
 * (apps from from dangerous sources, such as sideloaded packages or packages downloaded from a web
 * browser), or restricts settings globally based on device state.
 *
 * <p>Specifically, this class provides the ability to:
 *
 * <ol>
 *   <li>Check whether a setting is restricted from an app ({@link #isRestricted})
 *   <li>Get an intent that will open the "Restricted setting" dialog ({@link
 *       #createRestrictedSettingDialogIntent}) (a dialog that informs the user that the operation
 *       they've attempted to perform is restricted)
 *   <li>Check whether an app is eligible to have its restriction status cleared ({@link
 *       #isClearRestrictionAllowed})
 *   <li>Clear an app's restriction status (i.e., un-restrict it). ({@link #clearRestriction})
 * </ol>
 *
 * <p>Methods of this class will generally accept an app (identified by a packageName and a user)
 * and a "setting" (a string representing the "sensitive resource") as arguments. ECM's exact
 * behavior will generally depend on what restriction state ECM considers each setting and app. For
 * example:
 *
 * <ol>
 *   <li>A setting may be considered by ECM to be either **protected** or **not protected**. In
 *       general, this should be considered hardcoded into ECM's implementation: nothing can
 *       "protect" or "unprotect" a setting.
 *   <li>An app may be considered as being **not restricted** or **restricted**. A restricted app
 *       will be restricted from accessing all protected settings. Whether ECM considers any
 *       particular app restricted is an implementation detail of ECM. However, the user is able to
 *       clear any restricted app's restriction status (i.e, un-restrict it), after which ECM will
 *       consider the app **not restricted**.
 *   <li>A setting may be globally restricted based on device state. In this case, any app may be
 *       automatically considered *restricted*, regardless of the app's restriction state. Users
 *       cannot un-restrict the app, in these cases.
 * </ol>
 *
 * Why is ECM needed? Consider the following (pre-ECM) scenario:
 *
 * <ol>
 *   <li>The user downloads and installs an apk file from a browser.
 *   <li>The user opens Settings -> Accessibility
 *   <li>The user tries to register the app as an accessibility service.
 *   <li>The user is shown a permission prompt "Allow _ to have full control of your device?"
 *   <li>The user clicks "Allow"
 *   <li>The downloaded app now has full control of the device.
 * </ol>
 *
 * The purpose of ECM is to add more friction to this scenario.
 *
 * <p>With ECM, this scenario becomes:
 *
 * <ol>
 *   <li>The user downloads and installs an apk file from a browser.
 *   <li>The user goes into Settings -> Accessibility.
 *   <li>The user tries to register the app as an accessibility service.
 *   <li>The user is presented with a "Restricted setting" dialog explaining that the attempted
 *       action has been restricted. (No "allow" button is shown, but a link is given to a screen
 *       with intentionally-obscure instructions on how to proceed.)
 *   <li>The user must now navigate to Settings -> Apps -> [app]
 *   <li>The user then must click on "..." (top-right corner hamburger menu), then click "Allow
 *       restricted settings"
 *   <li>The user goes (again) into Settings -> Accessibility and (again) tries to register the app
 *       as an accessibility service.
 *   <li>The user is shown a permission prompt "Allow _ to have full control of your device?"
 *   <li>The user clicks "Allow"
 *   <li>The downloaded app now has full control of the device.
 * </ol>
 *
 * And, expanding on the above scenario, the role that this class plays is as follows:
 *
 * <ol>
 *   <li>The user downloads and installs an apk file from a browser.
 *   <li>The user goes into Settings -> Accessibility.
 *       <p>**This screen then calls {@link #isRestricted}, which checks whether each app listed
 *       on-screen is restricted from the accessibility service setting. It uses this to visually
 *       "gray out" restricted apps.**
 *   <li>The user tries to register the app as an accessibility service.
 *       <p>**This screen then calls {@link #createRestrictedSettingDialogIntent} and starts the
 *       intent. This opens the "Restricted setting" dialog.**
 *   <li>The user is presented with a "Restricted setting" dialog explaining that the attempted
 *       action is restricted. (No "allow" button is shown, but a link is given to a screen with
 *       intentionally-obscure instructions on how to proceed.)
 *       <p>**Upon opening, this dialog marks the app as eligible to have its restriction status
 *       cleared.**
 *   <li>The user must now navigate to Settings -> Apps -> [app].
 *       <p>**This screen calls {@link #isClearRestrictionAllowed} to check whether the app is
 *       eligible to have its restriction status cleared. If this returns {@code true}, this screen
 *       should then show a "Allow restricted setting" button inside the top-right hamburger menu.**
 *   <li>The user then must click on "..." (top-right corner hamburger menu), then click "Allow
 *       restricted settings".
 *       <p>**In response, this screen should now call {@link #clearRestriction}.**
 *   <li>The user goes (again) into Settings -> Accessibility and (again) tries to register the app
 *       as an accessibility service.
 *   <li>The user is shown a permission prompt "Allow _ to have full control of your device?"
 *   <li>The user clicks "Allow"
 *   <li>The downloaded app now has full control of the device.
 * </ol>
 *
 * @hide
 */
@SystemApi
@FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SystemService(Context.ECM_ENHANCED_CONFIRMATION_SERVICE)
public final class EnhancedConfirmationManager {
    /*
     * At the API level, we use the following terminology:
     *
     * - The capability of an app to access a setting may be considered (by ECM) to be *restricted*
     * or *not restricted*.
     * - A setting may be considered (by ECM) to be *protected* or *not protected*.
     * - The state of an app may be considered (by ECM) to be *restricted* or *not restricted*
     *
     * In this implementation, however, the state of an app is considered either **guarded** or
     * **not guarded**; these terms can generally be considered synonymous with **restricted** and
     * **not restricted**. (Keeping in mind that, the capability of any app to access any
     * non-protected setting will always be considered "not restricted", even if the state of the
     * app is considered "restricted".). An app can also be in a third state: **guarded and
     * acknowledged**, which corresponds with an app that is restricted and is eligible to have its
     * restriction status cleared.
     *
     * Currently, the ECM state of any given app is stored in the OP_ACCESS_RESTRICTED_SETTINGS
     * appop (though this may change in the future):
     *
     * - MODE_ALLOWED means the app is explicitly **not guarded**. (U- default)
     * - MODE_ERRORED means the app is explicitly **guarded**. (Only settable in U-.)
     * - MODE_IGNORED means the app is explicitly **guarded and acknowledged**. (An app enters this
     *   state as soon as the "Restricted setting" dialog has been shown to the user. If an app is
     *   in this state, Settings is now allowed to provide the user with the option to clear the
     *   restriction.)
     * - MODE_DEFAULT means the app's ECM state should be decided lazily. (V+ default) (That is,
     *   each time a caller checks whether or not an app is considered guarded by ECM, we'll run an
     *   heuristic to determine this.)
     *
     * Some notes on compatibility:
     *
     *   - On U-, MODE_ALLOWED is the default mode of OP_ACCESS_RESTRICTED_SETTINGS. On both U- and
     *   V+, this is also the mode after the app's restriction has been cleared.
     *   - In U-, the mode needed to be explicitly set (for example, by a browser that allows a
     *   dangerous app to be installed) to MODE_ERRORED to indicate that an app is guarded. In V+,
     *   we no longer allow an app to be placed into MODE_ERRORED, but for compatibility, we still
     *   recognize MODE_ERRORED to indicate that an app is explicitly guarded.
     * - In V+, the default mode is MODE_DEFAULT. Unlike U-, this potentially affects *all* apps,
     *   not just the ones which have been explicitly marked as **guarded**.
     *
     * Regarding ECM "setting"s: a setting may be any abstract resource identified by a string. ECM
     * may consider any particular setting **protected** or **not protected**. For now, the set of
     * protected settings is hardcoded, but this may evolve in the future.
     *
     * TODO(b/320512579): These methods currently enforce UPDATE_APP_OPS_STATS,
     * UPDATE_APP_OPS_STATS, and, for setter methods, MANAGE_APP_OPS_MODES. We should add
     * RequiresPermission annotations, but we can't, because some of these permissions are hidden
     * API. Either upgrade these to SystemApi or enforce a different permission, then add the
     * appropriate RequiresPermission annotation.
     */

    /**
     * Shows the "Restricted setting" dialog. Opened when a setting is blocked.
     */
    @SdkConstant(BROADCAST_INTENT_ACTION)
    public static final String ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG =
            "android.app.ecm.action.SHOW_ECM_RESTRICTED_SETTING_DIALOG";

    /**
     * The setting is restricted because of the phone state of the device
     * @hide
     */
    public static final String REASON_PHONE_STATE = "phone_state";

    /**
     * The setting is restricted because the restricted app op is set for the given package
     * @hide
     */
    public static final String REASON_PACKAGE_RESTRICTED = "package_restricted";


    /** A map of ECM states to their corresponding app op states */
    @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
    @IntDef(prefix = {"ECM_STATE_"}, value = {EcmState.ECM_STATE_NOT_GUARDED,
            EcmState.ECM_STATE_GUARDED, EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED,
            EcmState.ECM_STATE_IMPLICIT})
    private @interface EcmState {
        int ECM_STATE_NOT_GUARDED = AppOpsManager.MODE_ALLOWED;
        int ECM_STATE_GUARDED = AppOpsManager.MODE_ERRORED;
        int ECM_STATE_GUARDED_AND_ACKNOWLEDGED = AppOpsManager.MODE_IGNORED;
        int ECM_STATE_IMPLICIT = AppOpsManager.MODE_DEFAULT;
    }

    private static final String LOG_TAG = EnhancedConfirmationManager.class.getSimpleName();

    private static final ArraySet<String> PROTECTED_SETTINGS = new ArraySet<>();

    static {
        PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
        // TODO(b/310654015): Add other explicitly protected settings
    }

    private final @NonNull Context mContext;
    private final PackageManager mPackageManager;

    private final @NonNull IEnhancedConfirmationManager mService;

    /**
     * @hide
     */
    public EnhancedConfirmationManager(@NonNull Context context,
            @NonNull IEnhancedConfirmationManager service) {
        mContext = context;
        mPackageManager = context.getPackageManager();
        mService = service;
    }

    /**
     * Check whether a setting is restricted from an app.
     *
     * <p>This is {@code true} when the setting is a protected setting (i.e., a sensitive resource),
     * and the app is restricted (i.e., considered dangerous), and the user has not yet cleared the
     * app's restriction status (i.e., by clicking "Allow restricted settings" for this app).
     *
     * @param packageName package name of the application to check for
     * @param settingIdentifier identifier of the resource to check to check for
     * @return {@code true} if the setting is restricted from the app
     * @throws NameNotFoundException if the provided package was not found
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
    public boolean isRestricted(@NonNull String packageName, @NonNull String settingIdentifier)
            throws NameNotFoundException {
        try {
            return mService.isRestricted(packageName, settingIdentifier,
                    mContext.getUser().getIdentifier());
        } catch (IllegalArgumentException e) {
            throw new NameNotFoundException(packageName);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Clear an app's restriction status (i.e., un-restrict it).
     *
     * <p>After this is called, the app will no longer be restricted from accessing any protected
     * setting by ECM. This method should be called when the user clicks "Allow restricted settings"
     * for the app.
     *
     * @param packageName package name of the application to remove protection from
     * @throws NameNotFoundException if the provided package was not found
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
    public void clearRestriction(@NonNull String packageName) throws NameNotFoundException {
        try {
            mService.clearRestriction(packageName, mContext.getUser().getIdentifier());
        } catch (IllegalArgumentException e) {
            throw new NameNotFoundException(packageName);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Check whether the provided app is eligible to have its restriction status cleared (i.e., the
     * app is restricted, and the "Restricted setting" dialog has been presented to the user).
     *
     * <p>The Settings UI should use method this to check whether to present the user with the
     * "Allow restricted settings" button.
     *
     * @param packageName package name of the application to check for
     * @return {@code true} if the settings UI should present the user with the ability to clear
     * restrictions from the provided app
     * @throws NameNotFoundException if the provided package was not found
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
    public boolean isClearRestrictionAllowed(@NonNull String packageName)
            throws NameNotFoundException {
        try {
            return mService.isClearRestrictionAllowed(packageName,
                    mContext.getUser().getIdentifier());
        } catch (IllegalArgumentException e) {
            throw new NameNotFoundException(packageName);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Mark the app as eligible to have its restriction status cleared.
     *
     * <p>This should be called from the "Restricted setting" dialog (which {@link
     * #createRestrictedSettingDialogIntent} directs to) upon being presented to the user.
     *
     * <p>This restriction clearing does not apply to any settings that are restricted based on
     * global device state
     *
     * @param packageName package name of the application which should be considered acknowledged
     * @throws NameNotFoundException if the provided package was not found
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
    public void setClearRestrictionAllowed(@NonNull String packageName)
            throws NameNotFoundException {
        try {
            mService.setClearRestrictionAllowed(packageName, mContext.getUser().getIdentifier());
        } catch (IllegalArgumentException e) {
            throw new NameNotFoundException(packageName);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Gets an intent that will open the "Restricted setting" dialog for the specified package
     * and setting.
     *
     * <p>The "Restricted setting" dialog is a dialog that informs the user that the operation
     * they've attempted to perform is restricted, and provides them with a link explaining how to
     * proceed.
     *
     * @param packageName package name of the restricted application
     * @param settingIdentifier identifier of the restricted setting
     * @throws NameNotFoundException if the provided package was not found
     */
    public @NonNull Intent createRestrictedSettingDialogIntent(@NonNull String packageName,
            @NonNull String settingIdentifier) throws NameNotFoundException {
        Intent intent = new Intent(ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG);
        intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
        int uid = getPackageUid(packageName);
        intent.putExtra(Intent.EXTRA_UID, uid);
        intent.putExtra(Intent.EXTRA_SUBJECT, settingIdentifier);
        try {
            String restrictionReason = mService.getRestrictionReason(packageName,
                    settingIdentifier, UserHandle.getUserHandleForUid(uid).getIdentifier());
            intent.putExtra(Intent.EXTRA_REASON, restrictionReason);
        } catch (SecurityException | RemoteException e) {
            // The caller of this method does not have permission to read the ECM state, so we
            // won't include it in the return
        }
        return intent;
    }

    private int getPackageUid(String packageName) throws NameNotFoundException {
        return mPackageManager.getApplicationInfoAsUser(packageName, /* flags */ 0,
                mContext.getUser()).uid;
    }
}
