/*
 * Copyright (C) 2010 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.admin;

import static android.app.admin.flags.Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.admin.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Printer;
import android.util.SparseArray;
import android.util.Xml;

import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * This class is used to specify meta information of a device administrator
 * component.
 */
public final class DeviceAdminInfo implements Parcelable {
    static final String TAG = "DeviceAdminInfo";

    /**
     * A type of policy that this device admin can use: limit the passwords
     * that the user can select, via {@link DevicePolicyManager#setPasswordQuality}
     * and {@link DevicePolicyManager#setPasswordMinimumLength}.
     *
     * <p>To control this policy, the device admin must be a device owner or profile owner,
     * and must have a "limit-password" tag in the "uses-policies" section of its meta-data.
     * If used by a device owner, the policy only affects the primary user and its profiles,
     * but not any secondary users on the device.
     */
    public static final int USES_POLICY_LIMIT_PASSWORD = 0;

    /**
     * A type of policy that this device admin can use: able to watch login
     * attempts from the user, via {@link DeviceAdminReceiver#ACTION_PASSWORD_FAILED},
     * {@link DeviceAdminReceiver#ACTION_PASSWORD_SUCCEEDED}, and
     * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts}.
     *
     * <p>To control this policy, the device admin must have a "watch-login"
     * tag in the "uses-policies" section of its meta-data.
     */
    public static final int USES_POLICY_WATCH_LOGIN = 1;

    /**
     * A type of policy that this device admin can use: able to reset the
     * user's password via
     * {@link DevicePolicyManager#resetPassword}.
     *
     * <p>To control this policy, the device admin must have a "reset-password"
     * tag in the "uses-policies" section of its meta-data.
     */
    public static final int USES_POLICY_RESET_PASSWORD = 2;

    /**
     * A type of policy that this device admin can use: able to force the device
     * to lock via{@link DevicePolicyManager#lockNow} or limit the
     * maximum lock timeout for the device via
     * {@link DevicePolicyManager#setMaximumTimeToLock}.
     *
     * <p>To control this policy, the device admin must have a "force-lock"
     * tag in the "uses-policies" section of its meta-data.
     */
    public static final int USES_POLICY_FORCE_LOCK = 3;

    /**
     * A type of policy that this device admin can use: able to factory
     * reset the device, erasing all of the user's data, via
     * {@link DevicePolicyManager#wipeData}.
     *
     * <p>To control this policy, the device admin must have a "wipe-data"
     * tag in the "uses-policies" section of its meta-data.
     */
    public static final int USES_POLICY_WIPE_DATA = 4;

    /**
     * A type of policy that this device admin can use: able to specify the
     * device Global Proxy, via {@link DevicePolicyManager#setGlobalProxy}.
     *
     * <p>To control this policy, the device admin must have a "set-global-proxy"
     * tag in the "uses-policies" section of its meta-data.
     * @hide
     */
    public static final int USES_POLICY_SETS_GLOBAL_PROXY = 5;

    /**
     * A type of policy that this device admin can use: force the user to
     * change their password after an administrator-defined time limit.
     *
     * <p>To control this policy, the device admin must be a device owner or profile owner,
     * and must have an "expire-password" tag in the "uses-policies" section of its meta-data.
     * If used by a device owner, the policy only affects the primary user and its profiles,
     * but not any secondary users on the device.
     */
    public static final int USES_POLICY_EXPIRE_PASSWORD = 6;

    /**
     * A type of policy that this device admin can use: require encryption of stored data.
     *
     * <p>To control this policy, the device admin must have a "encrypted-storage"
     * tag in the "uses-policies" section of its meta-data.
     */
    public static final int USES_ENCRYPTED_STORAGE = 7;

    /**
     * A type of policy that this device admin can use: disables use of all device cameras.
     *
     * <p>To control this policy, the device admin must be a device owner or profile owner,
     * and must have a "disable-camera" tag in the "uses-policies" section of its meta-data.
     * If used by a device owner, the policy affects all users on the device.
     */
    public static final int USES_POLICY_DISABLE_CAMERA = 8;

    /**
     * A type of policy that this device admin can use: disables use of keyguard features.
     *
     * <p>To control this policy, the device admin must be a device owner or profile owner,
     * and must have a "disable-keyguard-features" tag in the "uses-policies" section of its
     * meta-data.  If used by a device owner, the policy only affects the primary user and
     * its profiles, but not any secondary users on the device.
     */
    public static final int USES_POLICY_DISABLE_KEYGUARD_FEATURES = 9;


    /**
     * Value for {@link #getHeadlessDeviceOwnerMode} which indicates that this DPC should not
     * be provisioned into Device Owner mode on a Headless System User Mode device.
     */
    public static final int HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED = 0;

    /**
     * Value for {@link #getHeadlessDeviceOwnerMode} which indicates that this DPC should be
     * provisioned into "affiliated" mode when on a Headless System User Mode device.
     *
     * <p>This mode adds a Profile Owner to all users other than the user the Device Owner is on.
     *
     * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
     * DPCs should set the value of attribute "headless-device-owner-mode" inside the
     * "headless-system-user" tag as "affiliated".
     */
    public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1;

    /**
     * Value for {@link #getHeadlessDeviceOwnerMode} which indicates that this DPC should be
     * provisioned into the first secondary user when on a Headless System User Mode device.
     *
     * <p>This mode only allows a single secondary user on the device blocking the creation of
     * additional secondary users.
     *
     * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
     * DPCs should set the value of attribute "headless-device-owner-mode" inside the
     * "headless-system-user" tag as "single_user".
     */
    @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
    public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;

    /**
     * @hide
     */
    @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED,
            HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface HeadlessDeviceOwnerMode {}

    /** @hide */
    public static class PolicyInfo {
        public final int ident;
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
        public final String tag;
        public final int label;
        public final int description;
        public final int labelForSecondaryUsers;
        public final int descriptionForSecondaryUsers;

        public PolicyInfo(int ident, String tag, int label, int description) {
            this(ident, tag, label, description, label, description);
        }

        public PolicyInfo(int ident, String tag, int label, int description,
                int labelForSecondaryUsers, int descriptionForSecondaryUsers) {
            this.ident = ident;
            this.tag = tag;
            this.label = label;
            this.description = description;
            this.labelForSecondaryUsers = labelForSecondaryUsers;
            this.descriptionForSecondaryUsers = descriptionForSecondaryUsers;
        }
    }

    static ArrayList<PolicyInfo> sPoliciesDisplayOrder = new ArrayList<PolicyInfo>();
    static HashMap<String, Integer> sKnownPolicies = new HashMap<String, Integer>();
    static SparseArray<PolicyInfo> sRevKnownPolicies = new SparseArray<PolicyInfo>();

    static {
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WIPE_DATA, "wipe-data",
                com.android.internal.R.string.policylab_wipeData,
                com.android.internal.R.string.policydesc_wipeData,
                com.android.internal.R.string.policylab_wipeData_secondaryUser,
                com.android.internal.R.string.policydesc_wipeData_secondaryUser
                ));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_RESET_PASSWORD, "reset-password",
                com.android.internal.R.string.policylab_resetPassword,
                com.android.internal.R.string.policydesc_resetPassword));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_LIMIT_PASSWORD, "limit-password",
                com.android.internal.R.string.policylab_limitPassword,
                com.android.internal.R.string.policydesc_limitPassword));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WATCH_LOGIN, "watch-login",
                com.android.internal.R.string.policylab_watchLogin,
                com.android.internal.R.string.policydesc_watchLogin,
                com.android.internal.R.string.policylab_watchLogin,
                com.android.internal.R.string.policydesc_watchLogin_secondaryUser
        ));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_FORCE_LOCK, "force-lock",
                com.android.internal.R.string.policylab_forceLock,
                com.android.internal.R.string.policydesc_forceLock));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_SETS_GLOBAL_PROXY, "set-global-proxy",
                com.android.internal.R.string.policylab_setGlobalProxy,
                com.android.internal.R.string.policydesc_setGlobalProxy));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_EXPIRE_PASSWORD, "expire-password",
                com.android.internal.R.string.policylab_expirePassword,
                com.android.internal.R.string.policydesc_expirePassword));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_ENCRYPTED_STORAGE, "encrypted-storage",
                com.android.internal.R.string.policylab_encryptedStorage,
                com.android.internal.R.string.policydesc_encryptedStorage));
        sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_DISABLE_CAMERA, "disable-camera",
                com.android.internal.R.string.policylab_disableCamera,
                com.android.internal.R.string.policydesc_disableCamera));
        sPoliciesDisplayOrder.add(new PolicyInfo(
                USES_POLICY_DISABLE_KEYGUARD_FEATURES, "disable-keyguard-features",
                com.android.internal.R.string.policylab_disableKeyguardFeatures,
                com.android.internal.R.string.policydesc_disableKeyguardFeatures));

        for (int i=0; i<sPoliciesDisplayOrder.size(); i++) {
            PolicyInfo pi = sPoliciesDisplayOrder.get(i);
            sRevKnownPolicies.put(pi.ident, pi);
            sKnownPolicies.put(pi.tag, pi.ident);
        }
    }

    /**
     * The BroadcastReceiver that implements this device admin component.
     */
    final ActivityInfo mActivityInfo;

    /**
     * Whether this should be visible to the user.
     */
    boolean mVisible;

    /**
     * The policies this administrator needs access to.
     */
    int mUsesPolicies;

    /**
     * Whether this administrator can be a target in an ownership transfer.
     *
     * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
     */
    boolean mSupportsTransferOwnership;

    @HeadlessDeviceOwnerMode int mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;

    /**
     * Constructor.
     *
     * @param context The Context in which we are parsing the device admin.
     * @param resolveInfo The ResolveInfo returned from the package manager about
     * this device admin's component.
     */
    public DeviceAdminInfo(Context context, ResolveInfo resolveInfo)
            throws XmlPullParserException, IOException {
        this(context, resolveInfo.activityInfo);
    }
    /**
     * Constructor.
     *
     * @param context The Context in which we are parsing the device admin.
     * @param activityInfo The ActivityInfo returned from the package manager about
     * this device admin's component.
     *
     * @hide
     */
    public DeviceAdminInfo(Context context, ActivityInfo activityInfo)
            throws XmlPullParserException, IOException {
        mActivityInfo = activityInfo;

        PackageManager pm = context.getPackageManager();

        XmlResourceParser parser = null;
        try {
            parser = mActivityInfo.loadXmlMetaData(pm, DeviceAdminReceiver.DEVICE_ADMIN_META_DATA);
            if (parser == null) {
                throw new XmlPullParserException("No "
                        + DeviceAdminReceiver.DEVICE_ADMIN_META_DATA + " meta-data");
            }

            Resources res = pm.getResourcesForApplication(mActivityInfo.applicationInfo);

            AttributeSet attrs = Xml.asAttributeSet(parser);

            int type;
            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
                    && type != XmlPullParser.START_TAG) {
            }

            String nodeName = parser.getName();
            if (!"device-admin".equals(nodeName)) {
                throw new XmlPullParserException(
                        "Meta-data does not start with device-admin tag");
            }

            TypedArray sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.DeviceAdmin);

            mVisible = sa.getBoolean(
                    com.android.internal.R.styleable.DeviceAdmin_visible, true);

            sa.recycle();

            int outerDepth = parser.getDepth();
            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
                   && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                    continue;
                }
                String tagName = parser.getName();
                if (tagName.equals("uses-policies")) {
                    int innerDepth = parser.getDepth();
                    while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
                           && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
                        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                            continue;
                        }
                        String policyName = parser.getName();
                        Integer val = sKnownPolicies.get(policyName);
                        if (val != null) {
                            mUsesPolicies |= 1 << val.intValue();
                        } else {
                            Log.w(TAG, "Unknown tag under uses-policies of "
                                    + getComponent() + ": " + policyName);
                        }
                    }
                } else if (tagName.equals("support-transfer-ownership")) {
                    if (parser.next() != XmlPullParser.END_TAG) {
                        throw new XmlPullParserException(
                                "support-transfer-ownership tag must be empty.");
                    }
                    mSupportsTransferOwnership = true;
                } else if (tagName.equals("headless-system-user")) {
                    String deviceOwnerModeStringValue = null;
                    if (Flags.headlessSingleUserCompatibilityFix()) {
                        deviceOwnerModeStringValue = parser.getAttributeValue(
                                 null, "headless-device-owner-mode");
                    }
                    if (deviceOwnerModeStringValue == null) {
                        deviceOwnerModeStringValue =
                                parser.getAttributeValue(null, "device-owner-mode");
                    }

                    if ("unsupported".equalsIgnoreCase(deviceOwnerModeStringValue)) {
                        mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
                    } else if ("affiliated".equalsIgnoreCase(deviceOwnerModeStringValue)) {
                        mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
                    } else if ("single_user".equalsIgnoreCase(deviceOwnerModeStringValue)) {
                        mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
                    } else {
                        if (Flags.headlessSingleUserCompatibilityFix()) {
                            Log.e(TAG, "Unknown headless-system-user mode: "
                                    + deviceOwnerModeStringValue);
                        } else {
                            throw new XmlPullParserException(
                                    "headless-system-user mode must be valid");
                        }
                    }
                }
            }
        } catch (NameNotFoundException e) {
            throw new XmlPullParserException(
                    "Unable to create context for: " + mActivityInfo.packageName);
        } finally {
            if (parser != null) parser.close();
        }
    }

    DeviceAdminInfo(Parcel source) {
        mActivityInfo = ActivityInfo.CREATOR.createFromParcel(source);
        mUsesPolicies = source.readInt();
        mSupportsTransferOwnership = source.readBoolean();
        mHeadlessDeviceOwnerMode = source.readInt();
    }

    /**
     * Return the .apk package that implements this device admin.
     */
    public String getPackageName() {
        return mActivityInfo.packageName;
    }

    /**
     * Return the class name of the receiver component that implements
     * this device admin.
     */
    public String getReceiverName() {
        return mActivityInfo.name;
    }

    /**
     * Return the raw information about the receiver implementing this
     * device admin.  Do not modify the returned object.
     */
    public ActivityInfo getActivityInfo() {
        return mActivityInfo;
    }

    /**
     * Return the component of the receiver that implements this device admin.
     */
    @NonNull
    public ComponentName getComponent() {
        return new ComponentName(mActivityInfo.packageName,
                mActivityInfo.name);
    }

    /**
     * Load the user-displayed label for this device admin.
     *
     * @param pm Supply a PackageManager used to load the device admin's
     * resources.
     */
    public CharSequence loadLabel(PackageManager pm) {
        return mActivityInfo.loadLabel(pm);
    }

    /**
     * Load user-visible description associated with this device admin.
     *
     * @param pm Supply a PackageManager used to load the device admin's
     * resources.
     */
    public CharSequence loadDescription(PackageManager pm) throws NotFoundException {
        if (mActivityInfo.descriptionRes != 0) {
            return pm.getText(mActivityInfo.packageName,
                    mActivityInfo.descriptionRes, mActivityInfo.applicationInfo);
        }
        throw new NotFoundException();
    }

    /**
     * Load the user-displayed icon for this device admin.
     *
     * @param pm Supply a PackageManager used to load the device admin's
     * resources.
     */
    public Drawable loadIcon(PackageManager pm) {
        return mActivityInfo.loadIcon(pm);
    }

    /**
     * Returns whether this device admin would like to be visible to the
     * user, even when it is not enabled.
     */
    public boolean isVisible() {
        return mVisible;
    }

    /**
     * Return true if the device admin has requested that it be able to use
     * the given policy control.  The possible policy identifier inputs are:
     * {@link #USES_POLICY_LIMIT_PASSWORD}, {@link #USES_POLICY_WATCH_LOGIN},
     * {@link #USES_POLICY_RESET_PASSWORD}, {@link #USES_POLICY_FORCE_LOCK},
     * {@link #USES_POLICY_WIPE_DATA},
     * {@link #USES_POLICY_EXPIRE_PASSWORD}, {@link #USES_ENCRYPTED_STORAGE},
     * {@link #USES_POLICY_DISABLE_CAMERA}.
     */
    public boolean usesPolicy(int policyIdent) {
        return (mUsesPolicies & (1<<policyIdent)) != 0;
    }

    /**
     * Return the XML tag name for the given policy identifier.  Valid identifiers
     * are as per {@link #usesPolicy(int)}.  If the given identifier is not
     * known, null is returned.
     */
    public String getTagForPolicy(int policyIdent) {
        return sRevKnownPolicies.get(policyIdent).tag;
    }

    /**
     * Return true if this administrator can be a target in an ownership transfer.
     */
    public boolean supportsTransferOwnership() {
        return mSupportsTransferOwnership;
    }

    /**
     * Returns the mode this DeviceAdmin wishes to use if provisioned as a Device Owner on a
     * headless system user mode device.
     */
    public @HeadlessDeviceOwnerMode int getHeadlessDeviceOwnerMode() {
        return mHeadlessDeviceOwnerMode;
    }

    /** @hide */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public ArrayList<PolicyInfo> getUsedPolicies() {
        ArrayList<PolicyInfo> res = new ArrayList<PolicyInfo>();
        for (int i=0; i<sPoliciesDisplayOrder.size(); i++) {
            PolicyInfo pi = sPoliciesDisplayOrder.get(i);
            if (usesPolicy(pi.ident)) {
                res.add(pi);
            }
        }
        return res;
    }

    /** @hide */
    public void writePoliciesToXml(TypedXmlSerializer out)
            throws IllegalArgumentException, IllegalStateException, IOException {
        out.attributeInt(null, "flags", mUsesPolicies);
    }

    /** @hide */
    public void readPoliciesFromXml(TypedXmlPullParser parser)
            throws XmlPullParserException, IOException {
        mUsesPolicies = parser.getAttributeInt(null, "flags");
    }

    public void dump(Printer pw, String prefix) {
        pw.println(prefix + "Receiver:");
        mActivityInfo.dump(pw, prefix + "  ");
    }

    @Override
    public String toString() {
        return "DeviceAdminInfo{" + mActivityInfo.name + "}";
    }

    /**
     * Used to package this object into a {@link Parcel}.
     *
     * @param dest The {@link Parcel} to be written.
     * @param flags The flags used for parceling.
     */
    public void writeToParcel(Parcel dest, int flags) {
        mActivityInfo.writeToParcel(dest, flags);
        dest.writeInt(mUsesPolicies);
        dest.writeBoolean(mSupportsTransferOwnership);
        dest.writeInt(mHeadlessDeviceOwnerMode);
    }

    /**
     * Used to make this class parcelable.
     */
    public static final @android.annotation.NonNull Parcelable.Creator<DeviceAdminInfo> CREATOR =
            new Parcelable.Creator<DeviceAdminInfo>() {
        public DeviceAdminInfo createFromParcel(Parcel source) {
            return new DeviceAdminInfo(source);
        }

        public DeviceAdminInfo[] newArray(int size) {
            return new DeviceAdminInfo[size];
        }
    };

    public int describeContents() {
        return 0;
    }
}
