Loading core/java/android/app/supervision/SupervisionRecoveryInfo.aidl +2 −10 Original line number Diff line number Diff line Loading @@ -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; core/java/android/app/supervision/SupervisionRecoveryInfo.java 0 → 100644 +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; } } services/supervision/java/com/android/server/supervision/SupervisionRecoveryInfoStorage.java +66 −15 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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()) { Loading @@ -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; } } } services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +12 −6 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading Loading
core/java/android/app/supervision/SupervisionRecoveryInfo.aidl +2 −10 Original line number Diff line number Diff line Loading @@ -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;
core/java/android/app/supervision/SupervisionRecoveryInfo.java 0 → 100644 +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; } }
services/supervision/java/com/android/server/supervision/SupervisionRecoveryInfoStorage.java +66 −15 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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()) { Loading @@ -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; } } }
services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +12 −6 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading