Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eed90e49 authored by Sandy Pan's avatar Sandy Pan
Browse files

Update SupervisionRecoveryInfo

System API will be added in another CL

Bug: 406054267
Bug: 373403206
Flag: android.app.supervision.flags.supervision_manager_apis
Test: com.android.server.supervision.SupervisionServiceTest
Change-Id: I62fc54eebaf3b52385250346c63b7566ff849039
parent e7a0aef5
Loading
Loading
Loading
Loading
+2 −10
Original line number Diff line number Diff line
@@ -18,14 +18,6 @@ package android.app.supervision;

/**
 * A parcelable of the supervision recovery information. This stores information for recovery
 * purposes.
 *
 * <p>Email: The email for recovery. ID: The account id for recovery.
 *
 * @hide
 * purposes for device supervision pin.
 */
@JavaDerive(equals = true, toString = true)
parcelable SupervisionRecoveryInfo {
    @nullable String email;
    @nullable String id;
}
parcelable SupervisionRecoveryInfo;
+178 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.supervision;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;

import androidx.annotation.Keep;

import java.util.Objects;

/**
 * Contains the information needed for recovering the device supervision pin.
 *
 * <p>Returned by {@link SupervisionManager#getSupervisionRecoveryInfo}.
 *
 * @hide
 */
public final class SupervisionRecoveryInfo implements Parcelable {
    /**
     * Extra key used to pass supervision recovery information within an intent.
     *
     * <p>The associated value should be a {@link android.app.supervision.SupervisionRecoveryInfo}
     * object.
     *
     * <p>* This extra is intended for use when launching the PIN recovery activity via {@link
     * com.android.settingslib.supervision.SupervisionIntentProvider#getPinRecoveryIntent }
     */
    public static final String EXTRA_SUPERVISION_RECOVERY_INFO =
            "android.app.supervision.extra.SUPERVISION_RECOVERY_INFO";

    @NonNull
    public static final Creator<SupervisionRecoveryInfo> CREATOR =
            new Creator<SupervisionRecoveryInfo>() {
                @Override
                public SupervisionRecoveryInfo createFromParcel(@NonNull Parcel source) {
                    String accountName = source.readString();
                    String accountType = source.readString();
                    PersistableBundle accountData =
                            source.readPersistableBundle(getClass().getClassLoader());
                    int state = source.readInt();
                    if (accountName != null && accountType != null) {
                        return new SupervisionRecoveryInfo(
                                accountName, accountType, state, accountData);
                    }
                    return null;
                }

                @Override
                public SupervisionRecoveryInfo[] newArray(int size) {
                    return new SupervisionRecoveryInfo[size];
                }
            };

    /** An IntDef which describes the various states of the recovery information. */
    @Keep
    @IntDef({STATE_PENDING, STATE_VERIFIED})
    public @interface State {}

    /** Indicates that the recovery information is pending verification. */
    public static final int STATE_PENDING = 0;

    /** Indicates that the recovery information has been verified. */
    public static final int STATE_VERIFIED = 1;

    @NonNull private String mAccountName;
    @NonNull private String mAccountType;
    @Nullable private PersistableBundle mAccountData;
    @State private int mState;

    public SupervisionRecoveryInfo(
            @NonNull String accountName,
            @NonNull String accountType,
            @State int state,
            @Nullable PersistableBundle accountData) {
        this.mAccountName = accountName;
        this.mAccountType = accountType;
        this.mAccountData = accountData;
        this.mState = state;
    }

    /** Gets the recovery account name. */
    @NonNull
    public String getAccountName() {
        return mAccountName;
    }

    /** Gets the recovery account type. */
    @NonNull
    public String getAccountType() {
        return mAccountType;
    }

    /** Gets the recovery account data. */
    @NonNull
    public PersistableBundle getAccountData() {
        return mAccountData == null ? new PersistableBundle() : mAccountData;
    }

    /**
     * Gets the state of the recovery information.
     *
     * @return One of {@link #STATE_PENDING}, {@link #STATE_VERIFIED}.
     */
    @State
    public int getState() {
        return mState;
    }

    @Override
    public void writeToParcel(@NonNull Parcel parcel, int flag) {
        parcel.writeString(mAccountName);
        parcel.writeString(mAccountType);
        parcel.writePersistableBundle(mAccountData);
        parcel.writeInt(mState);
    }

    /**
     * Reads the SupervisionRecoveryInfo object from the given {@link Parcel}.
     *
     * @param parcel The {@link Parcel} to read from.
     */
    public void readFromParcel(@NonNull Parcel parcel) {
        mAccountName = Objects.requireNonNull(parcel.readString());
        mAccountType = Objects.requireNonNull(parcel.readString());
        mAccountData = parcel.readPersistableBundle(getClass().getClassLoader());
        mState = parcel.readInt();
    }

    @Override
    public String toString() {
        java.util.StringJoiner joiner = new java.util.StringJoiner(", ", "{", "}");
        joiner.add("accountName: " + mAccountName);
        joiner.add("accountType: " + mAccountType);
        joiner.add("accountData: " + mAccountData);
        joiner.add("state: " + mState);
        return "SupervisionRecoveryInfo" + joiner;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof SupervisionRecoveryInfo)) return false;
        SupervisionRecoveryInfo that = (SupervisionRecoveryInfo) other;
        return Objects.equals(mAccountName, that.mAccountName)
                && Objects.equals(mAccountType, that.mAccountType)
                && Objects.equals(mAccountData, that.mAccountData)
                && mState == that.mState;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mAccountName, mAccountType, mAccountData, mState);
    }

    @Override
    public int describeContents() {
        return 0;
    }
}
+66 −15
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.app.supervision.SupervisionRecoveryInfo;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.os.PersistableBundle;
import android.util.Log;

import java.io.File;
@@ -38,13 +39,13 @@ import java.io.File;
public class SupervisionRecoveryInfoStorage {
    private static final String LOG_TAG = "RecoveryInfoStorage";
    private static final String PREF_NAME = "supervision_recovery_info";
    private static final String KEY_EMAIL = "email";
    private static final String KEY_ID = "id";
    private static final String KEY_ACCOUNT_TYPE = "account_type";
    private static final String KEY_ACCOUNT_NAME = "account_name";
    private static final String KEY_ACCOUNT_DATA = "account_data";
    private static final String KEY_STATE = "state";

    private final SharedPreferences mSharedPreferences;

    private static SupervisionRecoveryInfoStorage sInstance;

    private static final Object sLock = new Object();

    private SupervisionRecoveryInfoStorage(Context context) {
@@ -75,14 +76,22 @@ public class SupervisionRecoveryInfoStorage {
     */
    public SupervisionRecoveryInfo loadRecoveryInfo() {
        synchronized (sLock) {
            String email = mSharedPreferences.getString(KEY_EMAIL, null);
            String id = mSharedPreferences.getString(KEY_ID, null);
            String accountType = mSharedPreferences.getString(KEY_ACCOUNT_TYPE, null);
            String accountName = mSharedPreferences.getString(KEY_ACCOUNT_NAME, null);
            String accountDataString = mSharedPreferences.getString(KEY_ACCOUNT_DATA, null);
            int state = mSharedPreferences.getInt(KEY_STATE, SupervisionRecoveryInfo.STATE_PENDING);

            if (email != null || id != null) {
                SupervisionRecoveryInfo info = new SupervisionRecoveryInfo();
                info.email = email;
                info.id = id;
                return info;
            if (accountType != null && accountName != null) {
                PersistableBundle accountData = null;
                if (accountDataString != null) {
                    try {
                        accountData = PersistableBundleUtils.fromString(accountDataString);
                    } catch (Exception e) {
                        Log.e(LOG_TAG, "Failed to load account data from SharedPreferences", e);
                        // If failed to load accountData, just return other info.
                    }
                }
                return new SupervisionRecoveryInfo(accountName, accountType, state, accountData);
            }
        }
        return null;
@@ -99,11 +108,18 @@ public class SupervisionRecoveryInfoStorage {
            SharedPreferences.Editor editor = mSharedPreferences.edit();

            if (recoveryInfo == null) {
                editor.remove(KEY_EMAIL);
                editor.remove(KEY_ID);
                editor.remove(KEY_ACCOUNT_TYPE);
                editor.remove(KEY_ACCOUNT_NAME);
                editor.remove(KEY_ACCOUNT_DATA);
                editor.remove(KEY_STATE);
            } else {
                editor.putString(KEY_EMAIL, recoveryInfo.email);
                editor.putString(KEY_ID, recoveryInfo.id);
                editor.putString(KEY_ACCOUNT_TYPE, recoveryInfo.getAccountType());
                editor.putString(KEY_ACCOUNT_NAME, recoveryInfo.getAccountName());
                PersistableBundle accountData = recoveryInfo.getAccountData();
                String accountDataString =
                        accountData != null ? PersistableBundleUtils.toString(accountData) : null;
                editor.putString(KEY_ACCOUNT_DATA, accountDataString);
                editor.putInt(KEY_STATE, recoveryInfo.getState());
            }
            editor.apply();
            if (!editor.commit()) {
@@ -111,4 +127,39 @@ public class SupervisionRecoveryInfoStorage {
            }
        }
    }

    private static class PersistableBundleUtils {
        private static final String SEPARATOR = ";";
        private static final String KEY_VALUE_SEPARATOR = ":";

        public static String toString(PersistableBundle bundle) {
            if (bundle == null) {
                return null;
            }
            StringBuilder sb = new StringBuilder();
            for (String key : bundle.keySet()) {
                String value = String.valueOf(bundle.get(key));
                sb.append(key).append(KEY_VALUE_SEPARATOR).append(value).append(SEPARATOR);
            }
            return sb.toString();
        }

        public static PersistableBundle fromString(String str) {
            if (str == null || str.isEmpty()) {
                return null;
            }
            PersistableBundle bundle = new PersistableBundle();
            String[] pairs = str.split(SEPARATOR);
            for (String pair : pairs) {
                if (pair.isEmpty()) {
                    continue;
                }
                String[] keyValue = pair.split(KEY_VALUE_SEPARATOR);
                if (keyValue.length == 2) {
                    bundle.putString(keyValue[0], keyValue[1]);
                }
            }
            return bundle;
        }
    }
}
+12 −6
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.app.KeyguardManager
import android.app.admin.DevicePolicyManager
import android.app.admin.DevicePolicyManagerInternal
import android.app.supervision.SupervisionRecoveryInfo
import android.app.supervision.SupervisionRecoveryInfo.STATE_PENDING
import android.app.supervision.flags.Flags
import android.content.BroadcastReceiver
import android.content.ComponentName
@@ -403,15 +404,20 @@ class SupervisionServiceTest {
        assertThat(service.supervisionRecoveryInfo).isNull()

        val recoveryInfo =
            SupervisionRecoveryInfo().apply {
                email = "test_email"
                id = "test_id"
            }
            SupervisionRecoveryInfo(
                "email",
                "default",
                STATE_PENDING,
                PersistableBundle().apply { putString("id", "id") },
            )
        service.setSupervisionRecoveryInfo(recoveryInfo)

        assertThat(service.supervisionRecoveryInfo).isNotNull()
        assertThat(service.supervisionRecoveryInfo.email).isEqualTo(recoveryInfo.email)
        assertThat(service.supervisionRecoveryInfo.id).isEqualTo(recoveryInfo.id)
        assertThat(service.supervisionRecoveryInfo.accountType).isEqualTo(recoveryInfo.accountType)
        assertThat(service.supervisionRecoveryInfo.accountName).isEqualTo(recoveryInfo.accountName)
        assertThat(service.supervisionRecoveryInfo.accountData.getString("id"))
            .isEqualTo(recoveryInfo.accountData.getString("id"))
        assertThat(service.supervisionRecoveryInfo.state).isEqualTo(recoveryInfo.state)
    }

    private val systemSupervisionPackage: String