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

Commit ba1e6973 authored by Cintia Martins's avatar Cintia Martins Committed by Android (Google) Code Review
Browse files

Merge "Persist supervision user data" into main

parents 8ff6f196 9fc13703
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -88,3 +88,12 @@ flag {
  description: "Enables usage of lock task feature to enable Quick Settings on LockTask mode"
  bug: "401576820"
}


flag {
  name: "persistent_supervision_settings"
  is_exported: false
  namespace: "supervision"
  description: "Saves supervision user data and recovery info into a binary xml file"
  bug: "406449915"
}
+22 −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 com.android.server.supervision;

/** Constants used in supervision logs. */
public class SupervisionLog {
    public static final String TAG = "SupervisionService";
}
+44 −16
Original line number Diff line number Diff line
@@ -74,8 +74,6 @@ import java.util.List;

/** Service for handling system supervision. */
public class SupervisionService extends ISupervisionManager.Stub {
    private static final String LOG_TAG = "SupervisionService";

    /**
     * Activity action: Requests user confirmation of supervision credentials.
     *
@@ -98,8 +96,11 @@ public class SupervisionService extends ISupervisionManager.Stub {
    private final Injector mInjector;
    final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl();

    @GuardedBy("getLockObject()")
    final SupervisionSettings mSupervisionSettings = SupervisionSettings.getInstance();

    public SupervisionService(Context context) {
        mContext = context.createAttributionContext(LOG_TAG);
        mContext = context.createAttributionContext(SupervisionLog.TAG);
        mInjector = new Injector(context);
        mInjector.getUserManagerInternal().addUserLifecycleListener(new UserLifecycleListener());
    }
@@ -181,12 +182,19 @@ public class SupervisionService extends ISupervisionManager.Stub {
    /** Set the Supervision Recovery Info. */
    @Override
    public void setSupervisionRecoveryInfo(SupervisionRecoveryInfo recoveryInfo) {
        if (Flags.persistentSupervisionSettings()) {
            mSupervisionSettings.saveRecoveryInfo(recoveryInfo);
        } else {
            SupervisionRecoveryInfoStorage.getInstance(mContext).saveRecoveryInfo(recoveryInfo);
        }
    }

    /** Returns the Supervision Recovery Info or null if recovery is not set. */
    @Override
    public SupervisionRecoveryInfo getSupervisionRecoveryInfo() {
        if (Flags.persistentSupervisionSettings()) {
            return mSupervisionSettings.getRecoveryInfo();
        }
        return SupervisionRecoveryInfoStorage.getInstance(mContext).loadRecoveryInfo();
    }

@@ -199,12 +207,18 @@ public class SupervisionService extends ISupervisionManager.Stub {
        }

        synchronized (getLockObject()) {
            if (Flags.persistentSupervisionSettings()) {
                if (mSupervisionSettings.anySupervisedUser()) {
                    return false;
                }
            } else {
                for (int i = 0; i < mUserData.size(); i++) {
                    if (mUserData.valueAt(i).supervisionEnabled) {
                        return false;
                    }
                }
            }
        }

        return true;
    }
@@ -262,7 +276,7 @@ public class SupervisionService extends ISupervisionManager.Stub {
    @Override
    protected void dump(
            @NonNull FileDescriptor fd, @NonNull PrintWriter printWriter, @Nullable String[] args) {
        if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, printWriter)) return;
        if (!DumpUtils.checkDumpPermission(mContext, SupervisionLog.TAG, printWriter)) return;

        try (var pw = new IndentingPrintWriter(printWriter, "  ")) {
            pw.println("SupervisionService state:");
@@ -285,6 +299,9 @@ public class SupervisionService extends ISupervisionManager.Stub {
    @NonNull
    @GuardedBy("getLockObject()")
    SupervisionUserData getUserDataLocked(@UserIdInt int userId) {
        if (Flags.persistentSupervisionSettings()) {
            return mSupervisionSettings.getUserData(userId);
        } else {
            SupervisionUserData data = mUserData.get(userId);
            if (data == null) {
                // TODO(b/362790738): Do not create user data for nonexistent users.
@@ -293,6 +310,7 @@ public class SupervisionService extends ISupervisionManager.Stub {
            }
            return data;
        }
    }

    /**
     * Sets supervision as enabled or disabled for the given user and, in case supervision is being
@@ -304,6 +322,9 @@ public class SupervisionService extends ISupervisionManager.Stub {
            SupervisionUserData data = getUserDataLocked(userId);
            data.supervisionEnabled = enabled;
            data.supervisionAppPackage = enabled ? supervisionAppPackage : null;
            if (Flags.persistentSupervisionSettings()) {
                mSupervisionSettings.saveUserData();
            }
        }
        final long token = Binder.clearCallingIdentity();
        try {
@@ -318,7 +339,7 @@ public class SupervisionService extends ISupervisionManager.Stub {
                            (ISupervisionAppService) conn.getServiceBinder();
                    if (binder == null) {
                        Slog.d(
                                LOG_TAG,
                                SupervisionLog.TAG,
                                TextUtils.formatSimple(
                                        "Unable to toggle supervision for package %s. Binder is"
                                                + " null.",
@@ -333,7 +354,7 @@ public class SupervisionService extends ISupervisionManager.Stub {
                        }
                    } catch (RemoteException e) {
                        Slog.d(
                                LOG_TAG,
                                SupervisionLog.TAG,
                                TextUtils.formatSimple(
                                        "Unable to toggle supervision for package %s. e = %s",
                                        targetPackage, e));
@@ -574,6 +595,9 @@ public class SupervisionService extends ISupervisionManager.Stub {
                SupervisionUserData data = getUserDataLocked(userId);
                data.supervisionLockScreenEnabled = enabled;
                data.supervisionLockScreenOptions = options;
                if (Flags.persistentSupervisionSettings()) {
                    mSupervisionSettings.saveUserData();
                }
            }
        }
    }
@@ -583,8 +607,12 @@ public class SupervisionService extends ISupervisionManager.Stub {
        @Override
        public void onUserRemoved(UserInfo user) {
            synchronized (getLockObject()) {
                if (Flags.persistentSupervisionSettings()) {
                    mSupervisionSettings.removeUserData(user.id);
                } else {
                    mUserData.remove(user.id);
                }
            }
        }
    }
}
+266 −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 com.android.server.supervision;

import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.supervision.SupervisionRecoveryInfo;
import android.os.Environment;
import android.os.PersistableBundle;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Provides storage and retrieval of device supervision recovery information and user data.
 *
 * <p>The storage is managed as a singleton, ensuring a single point of access for persistent user
 * data and recovery info.
 */
public class SupervisionSettings {

    private static SupervisionSettings sInstance;
    private static final Object sLock = new Object();

    private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();

    private static final String PREF_RECOVERY = "supervision_recovery_info";
    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 static final String PREF_DATA = "supervision_data";
    private static final String PREF_USER_DATA = "supervision_user_data";
    private static final String KEY_USER_ID = "user_id";
    private static final String KEY_ENABLED = "supervision_enabled";
    private static final String KEY_APP_PACKAGE = "supervision_app_package";
    private static final String KEY_LOCK_SCREEN_ENABLED = "supervision_lockscreen_enabled";
    private static final String KEY_LOCK_SCREEN_OPTIONS = "supervision_lockscreen_options";

    private AtomicFile recoveryInfoFile =
            new AtomicFile(
                    new File(Environment.getDataSystemDirectory(), "supervision_recovery_info.xml"),
                    "supervision");
    private AtomicFile userDataFile =
            new AtomicFile(
                    new File(Environment.getDataSystemDirectory(), "supervision_settings.xml"),
                    "supervision");

    private SupervisionSettings() {
        loadUserData();
    }

    public static SupervisionSettings getInstance() {
        synchronized (sLock) {
            if (sInstance == null) {
                sInstance = new SupervisionSettings();
            }
            return sInstance;
        }
    }

    @VisibleForTesting
    public void changeDirForTesting(File parent) {
        recoveryInfoFile =
                new AtomicFile(new File(parent, "supervision_recovery_info.xml"), "supervision");
        userDataFile = new AtomicFile(new File(parent, "supervision_settings.xml"), "supervision");
    }

    /** Gets data about a specific user. */
    @NonNull
    public SupervisionUserData getUserData(@UserIdInt int userId) {
        SupervisionUserData data = mUserData.get(userId);
        if (data == null) {
            // TODO(b/362790738): Do not create user data for nonexistent users.
            data = new SupervisionUserData(userId);
            mUserData.append(userId, data);
        }
        return data;
    }

    /** Removes data of a specific user. */
    public void removeUserData(int userId) {
        mUserData.remove(userId);
        saveUserData();
    }

    /** Checks if there is at least one supervised user in the device. */
    public boolean anySupervisedUser() {
        for (int i = 0; i < mUserData.size(); i++) {
            if (mUserData.valueAt(i).supervisionEnabled) {
                return true;
            }
        }
        return false;
    }

    /** Loads user data from persistent storage. */
    public void loadUserData() {
        Slog.d(SupervisionLog.TAG, "Restoring supervision state");
        mUserData.clear();
        if (!userDataFile.getBaseFile().exists()) {
            return;
        }
        try (FileInputStream stream = userDataFile.openRead()) {
            final TypedXmlPullParser parser = Xml.resolvePullParser(stream);
            XmlUtils.beginDocument(parser, PREF_DATA);
            final int outerDepth = parser.getDepth();
            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                if (parser.getName().equals(PREF_USER_DATA)) {
                    int userId = parser.getAttributeInt(null, KEY_USER_ID);
                    SupervisionUserData data = getUserData(userId);
                    data.supervisionEnabled = parser.getAttributeBoolean(null, KEY_ENABLED);
                    data.supervisionAppPackage = parser.getAttributeValue(null, KEY_APP_PACKAGE);
                    if (data.supervisionAppPackage.isEmpty()) {
                        data.supervisionAppPackage = null;
                    }
                    data.supervisionLockScreenEnabled =
                            parser.getAttributeBoolean(null, KEY_LOCK_SCREEN_ENABLED);
                    while (XmlUtils.nextElementWithin(parser, outerDepth + 1)) {
                        if (parser.getName().equals(KEY_LOCK_SCREEN_OPTIONS)) {
                            data.supervisionLockScreenOptions =
                                    PersistableBundle.restoreFromXml(parser);
                        }
                    }
                }
            }
        } catch (IOException | XmlPullParserException e) {
            Slog.e(SupervisionLog.TAG, "Failed to restore supervision state", e);
        }
    }

    /** Saves user data to persistent storage. */
    public void saveUserData() {
        FileOutputStream stream = null;
        Slog.d(SupervisionLog.TAG, "Writing supervision state");
        try {
            stream = userDataFile.startWrite();
            final TypedXmlSerializer xml = Xml.resolveSerializer(stream);
            xml.startDocument(null, true);
            xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            xml.startTag(null, PREF_DATA);
            for (int i = 0; i < mUserData.size(); i++) {
                SupervisionUserData data = mUserData.valueAt(i);
                xml.startTag(null, PREF_USER_DATA);
                xml.attributeInt(null, KEY_USER_ID, data.userId);
                xml.attributeBoolean(null, KEY_ENABLED, data.supervisionEnabled);
                xml.attribute(
                        null,
                        KEY_APP_PACKAGE,
                        data.supervisionAppPackage == null ? "" : data.supervisionAppPackage);
                xml.attributeBoolean(
                        null, KEY_LOCK_SCREEN_ENABLED, data.supervisionLockScreenEnabled);
                if (data.supervisionLockScreenOptions != null) {
                    xml.startTag(null, KEY_LOCK_SCREEN_OPTIONS);
                    data.supervisionLockScreenOptions.saveToXml(xml);
                    xml.endTag(null, KEY_LOCK_SCREEN_OPTIONS);
                }
                xml.endTag(null, PREF_USER_DATA);
            }
            xml.endTag(null, PREF_DATA);
            xml.endDocument();
            userDataFile.finishWrite(stream);
        } catch (IOException | XmlPullParserException e) {
            userDataFile.failWrite(stream);
            Slog.e(SupervisionLog.TAG, "Failed to save supervision state", e);
        }
    }

    /**
     * Gets the device supervision recovery information from persistent storage.
     *
     * @return The {@link SupervisionRecoveryInfo} if found, otherwise {@code null}.
     */
    public SupervisionRecoveryInfo getRecoveryInfo() {
        Slog.d(SupervisionLog.TAG, "Retrieving recovery info");
        if (!recoveryInfoFile.getBaseFile().exists()) {
            Slog.d(SupervisionLog.TAG, "Recovery info file does not exist");
            return null;
        }
        try (FileInputStream stream = recoveryInfoFile.openRead()) {
            final TypedXmlPullParser parser = Xml.resolvePullParser(stream);
            XmlUtils.beginDocument(parser, PREF_RECOVERY);
            int outerDepth = parser.getDepth();
            String accountType = parser.getAttributeValue(null, KEY_ACCOUNT_TYPE);
            String accountName = parser.getAttributeValue(null, KEY_ACCOUNT_NAME);
            int state =
                    parser.getAttributeInt(null, KEY_STATE, SupervisionRecoveryInfo.STATE_PENDING);
            PersistableBundle accountData = null;
            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                if (parser.getName().equals(KEY_ACCOUNT_DATA)) {
                    accountData = PersistableBundle.restoreFromXml(parser);
                }
            }
            if (!accountType.isEmpty() && !accountName.isEmpty()) {
                return new SupervisionRecoveryInfo(accountName, accountType, state, accountData);
            }
        } catch (IOException | XmlPullParserException e) {
            Slog.e(SupervisionLog.TAG, "Failed to get recovery info from xml file", e);
        }

        return null;
    }

    /**
     * Saves the device supervision recovery information to persistent storage.
     *
     * @param recoveryInfo The {@link SupervisionRecoveryInfo} to save or {@code null} to clear the
     *     stored information.
     */
    public void saveRecoveryInfo(SupervisionRecoveryInfo recoveryInfo) {
        Slog.d(SupervisionLog.TAG, "Saving recovery info");
        if (recoveryInfo == null) {
            recoveryInfoFile.delete();
            return;
        }

        FileOutputStream stream = null;
        try {
            stream = recoveryInfoFile.startWrite();
            final TypedXmlSerializer xml = Xml.resolveSerializer(stream);
            xml.startDocument(null, true);
            xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            xml.startTag(null, PREF_RECOVERY);
            xml.attribute(null, KEY_ACCOUNT_TYPE, recoveryInfo.getAccountType());
            xml.attribute(null, KEY_ACCOUNT_NAME, recoveryInfo.getAccountName());
            xml.attributeInt(null, KEY_STATE, recoveryInfo.getState());
            xml.startTag(null, KEY_ACCOUNT_DATA);
            recoveryInfo.getAccountData().saveToXml(xml);
            xml.endTag(null, KEY_ACCOUNT_DATA);
            xml.endTag(null, PREF_RECOVERY);
            xml.endDocument();
            recoveryInfoFile.finishWrite(stream);
        } catch (IOException | XmlPullParserException e) {
            userDataFile.failWrite(stream);
            Slog.e(SupervisionLog.TAG, "Failed to save recovery info", e);
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -400,6 +400,7 @@ class SupervisionServiceTest {
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_PERSISTENT_SUPERVISION_SETTINGS)
    fun setSupervisionRecoveryInfo() {
        assertThat(service.supervisionRecoveryInfo).isNull()

Loading