Merge "Refactor SensorPrivacyService to separate state from service"
diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java
index 7074a2c..79153d7 100644
--- a/core/java/android/hardware/SensorPrivacyManager.java
+++ b/core/java/android/hardware/SensorPrivacyManager.java
@@ -31,6 +31,7 @@
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.service.SensorPrivacyIndividualEnabledSensorProto;
+import android.service.SensorPrivacySensorProto;
 import android.service.SensorPrivacyToggleSourceProto;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -75,7 +76,7 @@
     private final SparseArray<Boolean> mToggleSupportCache = new SparseArray<>();
 
     /**
-     * Individual sensors not listed in {@link Sensors}
+     * Sensor constants which are used in {@link SensorPrivacyManager}
      */
     public static class Sensors {
 
@@ -84,12 +85,12 @@
         /**
          * Constant for the microphone
          */
-        public static final int MICROPHONE = SensorPrivacyIndividualEnabledSensorProto.MICROPHONE;
+        public static final int MICROPHONE = SensorPrivacySensorProto.MICROPHONE;
 
         /**
          * Constant for the camera
          */
-        public static final int CAMERA = SensorPrivacyIndividualEnabledSensorProto.CAMERA;
+        public static final int CAMERA = SensorPrivacySensorProto.CAMERA;
 
         /**
          * Individual sensors not listed in {@link Sensors}
@@ -161,6 +162,68 @@
     }
 
     /**
+     * Types of toggles which can exist for sensor privacy
+     * @hide
+     */
+    public static class ToggleTypes {
+        private ToggleTypes() {}
+
+        /**
+         * Constant for software toggle.
+         */
+        public static final int SOFTWARE = SensorPrivacyIndividualEnabledSensorProto.SOFTWARE;
+
+        /**
+         * Constant for hardware toggle.
+         */
+        public static final int HARDWARE = SensorPrivacyIndividualEnabledSensorProto.HARDWARE;
+
+        /**
+         * Types of toggles which can exist for sensor privacy
+         *
+         * @hide
+         */
+        @IntDef(value = {
+                SOFTWARE,
+                HARDWARE
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface ToggleType {}
+
+    }
+
+    /**
+     * Types of state which can exist for the sensor privacy toggle
+     * @hide
+     */
+    public static class StateTypes {
+        private StateTypes() {}
+
+        /**
+         * Constant indicating privacy is enabled.
+         */
+        public static final int ENABLED = SensorPrivacyIndividualEnabledSensorProto.ENABLED;
+
+        /**
+         * Constant indicating privacy is disabled.
+         */
+        public static final int DISABLED = SensorPrivacyIndividualEnabledSensorProto.DISABLED;
+
+        /**
+         * Types of state which can exist for a sensor privacy toggle
+         *
+         * @hide
+         */
+        @IntDef(value = {
+                ENABLED,
+                DISABLED
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface StateType {}
+
+    }
+
+    /**
      * A class implementing this interface can register with the {@link
      * android.hardware.SensorPrivacyManager} to receive notification when the sensor privacy
      * state changes.
@@ -507,7 +570,6 @@
     /**
      * Don't show dialogs to turn off sensor privacy for this package.
      *
-     * @param packageName Package name not to show dialogs for
      * @param suppress Whether to suppress or re-enable.
      *
      * @hide
@@ -521,7 +583,6 @@
     /**
      * Don't show dialogs to turn off sensor privacy for this package.
      *
-     * @param packageName Package name not to show dialogs for
      * @param suppress Whether to suppress or re-enable.
      * @param userId the user's id
      *
diff --git a/core/proto/android/hardware/sensorprivacy.proto b/core/proto/android/hardware/sensorprivacy.proto
index 81d849e..97870a1 100644
--- a/core/proto/android/hardware/sensorprivacy.proto
+++ b/core/proto/android/hardware/sensorprivacy.proto
@@ -22,6 +22,13 @@
 
 import "frameworks/base/core/proto/android/privacy.proto";
 
+message AllSensorPrivacyServiceDumpProto {
+    option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    // Is global sensor privacy enabled
+    optional bool is_enabled = 1;
+}
+
 message SensorPrivacyServiceDumpProto {
     option (android.msg_privacy).dest = DEST_AUTOMATIC;
 
@@ -35,6 +42,9 @@
 
     // Per user settings for sensor privacy
     repeated SensorPrivacyUserProto user = 3;
+
+    // Implementation
+    optional string storage_implementation = 4;
 }
 
 message SensorPrivacyUserProto {
@@ -43,16 +53,47 @@
     // User id
     optional int32 user_id = 1;
 
+    // DEPRECATED
     // Is global sensor privacy enabled
     optional bool is_enabled = 2;
 
     // Per sensor privacy enabled
+    // DEPRECATED
     repeated SensorPrivacyIndividualEnabledSensorProto individual_enabled_sensor = 3;
+
+    // Per toggle type sensor privacy
+    repeated SensorPrivacySensorProto sensors = 4;
+}
+
+message SensorPrivacySensorProto {
+    option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    enum Sensor {
+        UNKNOWN = 0;
+
+        MICROPHONE = 1;
+        CAMERA = 2;
+    }
+
+    optional int32 sensor = 1;
+
+    repeated SensorPrivacyIndividualEnabledSensorProto toggles = 2;
 }
 
 message SensorPrivacyIndividualEnabledSensorProto {
     option (android.msg_privacy).dest = DEST_AUTOMATIC;
 
+    enum ToggleType {
+        SOFTWARE = 1;
+        HARDWARE = 2;
+    }
+
+    enum StateType {
+        ENABLED = 1;
+        DISABLED = 2;
+    }
+
+    // DEPRECATED
     enum Sensor {
         UNKNOWN = 0;
 
@@ -63,8 +104,17 @@
     // Sensor for which privacy might be enabled
     optional Sensor sensor = 1;
 
-    // If sensor privacy is enabled for this sensor
+    // DEPRECATED
     optional bool is_enabled = 2;
+
+    // Timestamp of the last time the sensor was changed
+    optional int64 last_change = 3;
+
+    // The toggle type for this state
+    optional ToggleType toggle_type = 4;
+
+    // If sensor privacy state for this sensor
+    optional StateType state_type = 5;
 }
 
 message SensorPrivacyToggleSourceProto {
diff --git a/services/core/java/com/android/server/OWNERS b/services/core/java/com/android/server/OWNERS
index efdd7ab..4129feb 100644
--- a/services/core/java/com/android/server/OWNERS
+++ b/services/core/java/com/android/server/OWNERS
@@ -10,9 +10,6 @@
 # Userspace reboot
 per-file UserspaceRebootLogger.java = ioffe@google.com, dvander@google.com
 
-# Sensor Privacy
-per-file SensorPrivacyService.java = file:platform/frameworks/native:/libs/sensorprivacy/OWNERS
-
 # ServiceWatcher
 per-file ServiceWatcher.java = sooniln@google.com
 
diff --git a/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java b/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
new file mode 100644
index 0000000..f797f09
--- /dev/null
+++ b/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.sensorprivacy;
+
+import android.annotation.NonNull;
+import android.os.Environment;
+import android.os.Handler;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+import android.util.Xml;
+
+import com.android.internal.util.XmlUtils;
+import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.IoThread;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+
+class AllSensorStateController {
+
+    private static final String LOG_TAG = AllSensorStateController.class.getSimpleName();
+
+    private static final String SENSOR_PRIVACY_XML_FILE = "sensor_privacy.xml";
+    private static final String XML_TAG_SENSOR_PRIVACY = "all-sensor-privacy";
+    private static final String XML_TAG_SENSOR_PRIVACY_LEGACY = "sensor-privacy";
+    private static final String XML_ATTRIBUTE_ENABLED = "enabled";
+
+    private static AllSensorStateController sInstance;
+
+    private final AtomicFile mAtomicFile =
+            new AtomicFile(new File(Environment.getDataSystemDirectory(), SENSOR_PRIVACY_XML_FILE));
+
+    private boolean mEnabled;
+    private SensorPrivacyStateController.AllSensorPrivacyListener mListener;
+    private Handler mListenerHandler;
+
+    static AllSensorStateController getInstance() {
+        if (sInstance == null) {
+            sInstance = new AllSensorStateController();
+        }
+        return sInstance;
+    }
+
+    private AllSensorStateController() {
+        if (!mAtomicFile.exists()) {
+            return;
+        }
+        try (FileInputStream inputStream = mAtomicFile.openRead()) {
+            TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
+
+            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+                String tagName = parser.getName();
+                if (XML_TAG_SENSOR_PRIVACY.equals(tagName)) {
+                    mEnabled |= XmlUtils
+                            .readBooleanAttribute(parser, XML_ATTRIBUTE_ENABLED, false);
+                    break;
+                }
+                if (XML_TAG_SENSOR_PRIVACY_LEGACY.equals(tagName)) {
+                    mEnabled |= XmlUtils
+                            .readBooleanAttribute(parser, XML_ATTRIBUTE_ENABLED, false);
+                }
+                if ("user".equals(tagName)) { // Migrate from mic/cam toggles format
+                    int user = XmlUtils.readIntAttribute(parser, "id", -1);
+                    if (user == 0) {
+                        mEnabled |=
+                                XmlUtils.readBooleanAttribute(parser, XML_ATTRIBUTE_ENABLED);
+                    }
+                }
+                XmlUtils.nextElement(parser);
+            }
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(LOG_TAG, "Caught an exception reading the state from storage: ", e);
+            mEnabled = false;
+        }
+    }
+
+    public boolean getAllSensorStateLocked() {
+        return mEnabled;
+    }
+
+    public void setAllSensorStateLocked(boolean enabled) {
+        if (mEnabled != enabled) {
+            mEnabled = enabled;
+            if (mListener != null && mListenerHandler != null) {
+                mListenerHandler.sendMessage(
+                        PooledLambda.obtainMessage(mListener::onAllSensorPrivacyChanged, enabled));
+            }
+        }
+    }
+
+    void setAllSensorPrivacyListenerLocked(Handler handler,
+            SensorPrivacyStateController.AllSensorPrivacyListener listener) {
+        Objects.requireNonNull(handler);
+        Objects.requireNonNull(listener);
+        if (mListener != null) {
+            throw new IllegalStateException("Listener is already set");
+        }
+        mListener = listener;
+        mListenerHandler = handler;
+    }
+
+    public void schedulePersistLocked() {
+        IoThread.getHandler().sendMessage(PooledLambda.obtainMessage(this::persist, mEnabled));
+    }
+
+    private void persist(boolean enabled) {
+        FileOutputStream outputStream = null;
+        try {
+            outputStream = mAtomicFile.startWrite();
+            TypedXmlSerializer serializer = Xml.resolveSerializer(outputStream);
+            serializer.startDocument(null, true);
+            serializer.startTag(null, XML_TAG_SENSOR_PRIVACY);
+            serializer.attributeBoolean(null, XML_ATTRIBUTE_ENABLED, enabled);
+            serializer.endTag(null, XML_TAG_SENSOR_PRIVACY);
+            serializer.endDocument();
+            mAtomicFile.finishWrite(outputStream);
+        } catch (IOException e) {
+            Log.e(LOG_TAG, "Caught an exception persisting the sensor privacy state: ", e);
+            mAtomicFile.failWrite(outputStream);
+        }
+    }
+
+    void resetForTesting() {
+        mListener = null;
+        mListenerHandler = null;
+        mEnabled = false;
+    }
+
+    void dumpLocked(@NonNull DualDumpOutputStream dumpStream) {
+        // TODO stub
+    }
+}
diff --git a/services/core/java/com/android/server/sensorprivacy/OWNERS b/services/core/java/com/android/server/sensorprivacy/OWNERS
new file mode 100644
index 0000000..15e3f7a
--- /dev/null
+++ b/services/core/java/com/android/server/sensorprivacy/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/native:/libs/sensorprivacy/OWNERS
\ No newline at end of file
diff --git a/services/core/java/com/android/server/sensorprivacy/PersistedState.java b/services/core/java/com/android/server/sensorprivacy/PersistedState.java
new file mode 100644
index 0000000..ce9fff5
--- /dev/null
+++ b/services/core/java/com/android/server/sensorprivacy/PersistedState.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2021 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.sensorprivacy;
+
+import android.hardware.SensorPrivacyManager;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.service.SensorPrivacyIndividualEnabledSensorProto;
+import android.service.SensorPrivacySensorProto;
+import android.service.SensorPrivacyServiceDumpProto;
+import android.service.SensorPrivacyUserProto;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+import android.util.Xml;
+
+import com.android.internal.util.XmlUtils;
+import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.internal.util.function.QuadConsumer;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.IoThread;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Class for managing persisted state. Synchronization must be handled by the caller.
+ */
+class PersistedState {
+
+    private static final String LOG_TAG = PersistedState.class.getSimpleName();
+
+    /** Version number indicating compatibility parsing the persisted file */
+    private static final int CURRENT_PERSISTENCE_VERSION = 2;
+    /** Version number indicating the persisted data needs upgraded to match new internal data
+     *  structures and features */
+    private static final int CURRENT_VERSION = 2;
+
+    private static final String XML_TAG_SENSOR_PRIVACY = "sensor-privacy";
+    private static final String XML_TAG_SENSOR_STATE = "sensor-state";
+    private static final String XML_ATTRIBUTE_PERSISTENCE_VERSION = "persistence-version";
+    private static final String XML_ATTRIBUTE_VERSION = "version";
+    private static final String XML_ATTRIBUTE_TOGGLE_TYPE = "toggle-type";
+    private static final String XML_ATTRIBUTE_USER_ID = "user-id";
+    private static final String XML_ATTRIBUTE_SENSOR = "sensor";
+    private static final String XML_ATTRIBUTE_STATE_TYPE = "state-type";
+    private static final String XML_ATTRIBUTE_LAST_CHANGE = "last-change";
+
+    private final AtomicFile mAtomicFile;
+
+    private ArrayMap<TypeUserSensor, SensorState> mStates = new ArrayMap<>();
+
+    static PersistedState fromFile(String fileName) {
+        return new PersistedState(fileName);
+    }
+
+    private PersistedState(String fileName) {
+        mAtomicFile = new AtomicFile(new File(Environment.getDataSystemDirectory(), fileName));
+        readState();
+    }
+
+    private void readState() {
+        AtomicFile file = mAtomicFile;
+        if (!file.exists()) {
+            AtomicFile fileToMigrateFrom =
+                    new AtomicFile(new File(Environment.getDataSystemDirectory(),
+                            "sensor_privacy.xml"));
+
+            if (fileToMigrateFrom.exists()) {
+                // Sample the start tag to determine if migration is needed
+                try (FileInputStream inputStream = fileToMigrateFrom.openRead()) {
+                    TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
+                    XmlUtils.beginDocument(parser, XML_TAG_SENSOR_PRIVACY);
+                    file = fileToMigrateFrom;
+                } catch (IOException e) {
+                    Log.e(LOG_TAG, "Caught an exception reading the state from storage: ", e);
+                    // Delete the file to prevent the same error on subsequent calls and assume
+                    // sensor privacy is not enabled.
+                    fileToMigrateFrom.delete();
+                } catch (XmlPullParserException e) {
+                    // No migration needed
+                }
+            }
+        }
+
+        Object nonupgradedState = null;
+        if (file.exists()) {
+            try (FileInputStream inputStream = file.openRead()) {
+                TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
+                XmlUtils.beginDocument(parser, XML_TAG_SENSOR_PRIVACY);
+                final int persistenceVersion = parser.getAttributeInt(null,
+                        XML_ATTRIBUTE_PERSISTENCE_VERSION, 0);
+
+                // Use inline string literals for xml tags/attrs when parsing old versions since
+                // these should never be changed even with refactorings.
+                if (persistenceVersion == 0) {
+                    int version = 0;
+                    PVersion0 version0 = new PVersion0(version);
+                    nonupgradedState = version0;
+                    readPVersion0(parser, version0);
+                } else if (persistenceVersion == 1) {
+                    int version = parser.getAttributeInt(null,
+                            "version", 1);
+                    PVersion1 version1 = new PVersion1(version);
+                    nonupgradedState = version1;
+
+                    readPVersion1(parser, version1);
+                } else if (persistenceVersion == CURRENT_PERSISTENCE_VERSION) {
+                    int version = parser.getAttributeInt(null,
+                            XML_ATTRIBUTE_VERSION, 2);
+                    PVersion2 version2 = new PVersion2(version);
+                    nonupgradedState = version2;
+
+                    readPVersion2(parser, version2);
+                } else {
+                    Log.e(LOG_TAG, "Unknown persistence version: " + persistenceVersion
+                                    + ". Deleting.",
+                            new RuntimeException());
+                    file.delete();
+                    nonupgradedState = null;
+                }
+
+            } catch (IOException | XmlPullParserException | RuntimeException e) {
+                Log.e(LOG_TAG, "Caught an exception reading the state from storage: ", e);
+                // Delete the file to prevent the same error on subsequent calls and assume
+                // sensor privacy is not enabled.
+                file.delete();
+                nonupgradedState = null;
+            }
+        }
+
+        if (nonupgradedState == null) {
+            // New file, default state for current version goes here.
+            nonupgradedState = new PVersion2(2);
+        }
+
+        if (nonupgradedState instanceof PVersion0) {
+            nonupgradedState = PVersion1.fromPVersion0((PVersion0) nonupgradedState);
+        }
+        if (nonupgradedState instanceof PVersion1) {
+            nonupgradedState = PVersion2.fromPVersion1((PVersion1) nonupgradedState);
+        }
+        if (nonupgradedState instanceof PVersion2) {
+            PVersion2 upgradedState = (PVersion2) nonupgradedState;
+            mStates = upgradedState.mStates;
+        } else {
+            Log.e(LOG_TAG, "State not successfully upgraded.");
+            mStates = new ArrayMap<>();
+        }
+    }
+
+    private static void readPVersion0(TypedXmlPullParser parser, PVersion0 version0)
+            throws XmlPullParserException, IOException {
+
+        XmlUtils.nextElement(parser);
+        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+            if ("individual-sensor-privacy".equals(parser.getName())) {
+                int sensor = XmlUtils.readIntAttribute(parser, "sensor");
+                boolean indEnabled = XmlUtils.readBooleanAttribute(parser,
+                        "enabled");
+                version0.addState(sensor, indEnabled);
+                XmlUtils.skipCurrentTag(parser);
+            } else {
+                XmlUtils.nextElement(parser);
+            }
+        }
+    }
+
+    private static void readPVersion1(TypedXmlPullParser parser, PVersion1 version1)
+            throws XmlPullParserException, IOException {
+        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+            XmlUtils.nextElement(parser);
+
+            if ("user".equals(parser.getName())) {
+                int currentUserId = parser.getAttributeInt(null, "id");
+                int depth = parser.getDepth();
+                while (XmlUtils.nextElementWithin(parser, depth)) {
+                    if ("individual-sensor-privacy".equals(parser.getName())) {
+                        int sensor = parser.getAttributeInt(null, "sensor");
+                        boolean isEnabled = parser.getAttributeBoolean(null,
+                                "enabled");
+                        version1.addState(currentUserId, sensor, isEnabled);
+                    }
+                }
+            }
+        }
+    }
+
+    private static void readPVersion2(TypedXmlPullParser parser, PVersion2 version2)
+            throws XmlPullParserException, IOException {
+
+        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+            XmlUtils.nextElement(parser);
+
+            if (XML_TAG_SENSOR_STATE.equals(parser.getName())) {
+                int toggleType = parser.getAttributeInt(null, XML_ATTRIBUTE_TOGGLE_TYPE);
+                int userId = parser.getAttributeInt(null, XML_ATTRIBUTE_USER_ID);
+                int sensor = parser.getAttributeInt(null, XML_ATTRIBUTE_SENSOR);
+                int state = parser.getAttributeInt(null, XML_ATTRIBUTE_STATE_TYPE);
+                long lastChange = parser.getAttributeLong(null, XML_ATTRIBUTE_LAST_CHANGE);
+
+                version2.addState(toggleType, userId, sensor, state, lastChange);
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+    }
+
+    public SensorState getState(int toggleType, int userId, int sensor) {
+        return mStates.get(new TypeUserSensor(toggleType, userId, sensor));
+    }
+
+    public SensorState setState(int toggleType, int userId, int sensor, SensorState sensorState) {
+        return mStates.put(new TypeUserSensor(toggleType, userId, sensor), sensorState);
+    }
+
+    private static class TypeUserSensor {
+
+        int mType;
+        int mUserId;
+        int mSensor;
+
+        TypeUserSensor(int type, int userId, int sensor) {
+            mType = type;
+            mUserId = userId;
+            mSensor = sensor;
+        }
+
+        TypeUserSensor(TypeUserSensor typeUserSensor) {
+            this(typeUserSensor.mType, typeUserSensor.mUserId, typeUserSensor.mSensor);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof TypeUserSensor)) return false;
+            TypeUserSensor that = (TypeUserSensor) o;
+            return mType == that.mType && mUserId == that.mUserId && mSensor == that.mSensor;
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * (31 * mType + mUserId) + mSensor;
+        }
+    }
+
+    void schedulePersist() {
+        int numStates = mStates.size();
+
+        ArrayMap<TypeUserSensor, SensorState> statesCopy = new ArrayMap<>();
+        for (int i = 0; i < numStates; i++) {
+            statesCopy.put(new TypeUserSensor(mStates.keyAt(i)),
+                    new SensorState(mStates.valueAt(i)));
+        }
+        IoThread.getHandler().sendMessage(
+                PooledLambda.obtainMessage(PersistedState::persist, this, statesCopy));
+    }
+
+    private void persist(ArrayMap<TypeUserSensor, SensorState> states) {
+        FileOutputStream outputStream = null;
+        try {
+            outputStream = mAtomicFile.startWrite();
+            TypedXmlSerializer serializer = Xml.resolveSerializer(outputStream);
+            serializer.startDocument(null, true);
+            serializer.startTag(null, XML_TAG_SENSOR_PRIVACY);
+            serializer.attributeInt(null, XML_ATTRIBUTE_PERSISTENCE_VERSION,
+                    CURRENT_PERSISTENCE_VERSION);
+            serializer.attributeInt(null, XML_ATTRIBUTE_VERSION, CURRENT_VERSION);
+            for (int i = 0; i < states.size(); i++) {
+                TypeUserSensor userSensor = states.keyAt(i);
+                SensorState sensorState = states.valueAt(i);
+
+                serializer.startTag(null, XML_TAG_SENSOR_STATE);
+                serializer.attributeInt(null, XML_ATTRIBUTE_TOGGLE_TYPE,
+                        userSensor.mType);
+                serializer.attributeInt(null, XML_ATTRIBUTE_USER_ID,
+                        userSensor.mUserId);
+                serializer.attributeInt(null, XML_ATTRIBUTE_SENSOR,
+                        userSensor.mSensor);
+                serializer.attributeInt(null, XML_ATTRIBUTE_STATE_TYPE,
+                        sensorState.getState());
+                serializer.attributeLong(null, XML_ATTRIBUTE_LAST_CHANGE,
+                        sensorState.getLastChange());
+                serializer.endTag(null, XML_TAG_SENSOR_STATE);
+            }
+
+            serializer.endTag(null, XML_TAG_SENSOR_PRIVACY);
+            serializer.endDocument();
+            mAtomicFile.finishWrite(outputStream);
+        } catch (IOException e) {
+            Log.e(LOG_TAG, "Caught an exception persisting the sensor privacy state: ", e);
+            mAtomicFile.failWrite(outputStream);
+        }
+    }
+
+    void dump(DualDumpOutputStream dumpStream) {
+        // Collect per user, then per sensor. <toggle type, state>
+        SparseArray<SparseArray<Pair<Integer, SensorState>>> statesMatrix = new SparseArray<>();
+        int numStates = mStates.size();
+        for (int i = 0; i < numStates; i++) {
+            int toggleType = mStates.keyAt(i).mType;
+            int userId = mStates.keyAt(i).mUserId;
+            int sensor = mStates.keyAt(i).mSensor;
+
+            SparseArray<Pair<Integer, SensorState>> userStates = statesMatrix.get(userId);
+            if (userStates == null) {
+                userStates = new SparseArray<>();
+                statesMatrix.put(userId, userStates);
+            }
+            userStates.put(sensor, new Pair<>(toggleType, mStates.valueAt(i)));
+        }
+
+        dumpStream.write("storage_implementation",
+                SensorPrivacyServiceDumpProto.STORAGE_IMPLEMENTATION,
+                SensorPrivacyStateControllerImpl.class.getName());
+
+        int numUsers = statesMatrix.size();
+        for (int i = 0; i < numUsers; i++) {
+            int userId = statesMatrix.keyAt(i);
+            long userToken = dumpStream.start("users", SensorPrivacyServiceDumpProto.USER);
+            dumpStream.write("user_id", SensorPrivacyUserProto.USER_ID, userId);
+            SparseArray<Pair<Integer, SensorState>> userStates = statesMatrix.valueAt(i);
+            int numSensors = userStates.size();
+            for (int j = 0; j < numSensors; j++) {
+                int sensor = userStates.keyAt(j);
+                int toggleType = userStates.valueAt(j).first;
+                SensorState sensorState = userStates.valueAt(j).second;
+                long sensorToken = dumpStream.start("sensors", SensorPrivacyUserProto.SENSORS);
+                dumpStream.write("sensor", SensorPrivacySensorProto.SENSOR, sensor);
+                long toggleToken = dumpStream.start("toggles", SensorPrivacySensorProto.TOGGLES);
+                dumpStream.write("toggle_type",
+                        SensorPrivacyIndividualEnabledSensorProto.TOGGLE_TYPE,
+                        toggleType);
+                dumpStream.write("state_type",
+                        SensorPrivacyIndividualEnabledSensorProto.STATE_TYPE,
+                        sensorState.getState());
+                dumpStream.write("last_change",
+                        SensorPrivacyIndividualEnabledSensorProto.LAST_CHANGE,
+                        sensorState.getLastChange());
+                dumpStream.end(toggleToken);
+                dumpStream.end(sensorToken);
+            }
+            dumpStream.end(userToken);
+        }
+    }
+
+    void forEachKnownState(QuadConsumer<Integer, Integer, Integer, SensorState> consumer) {
+        int numStates = mStates.size();
+        for (int i = 0; i < numStates; i++) {
+            TypeUserSensor tus = mStates.keyAt(i);
+            SensorState sensorState = mStates.valueAt(i);
+            consumer.accept(tus.mType, tus.mUserId, tus.mSensor, sensorState);
+        }
+    }
+
+    // Structure for persistence version 0
+    private static class PVersion0 {
+        private SparseArray<SensorState> mIndividualEnabled = new SparseArray<>();
+
+        private PVersion0(int version) {
+            if (version != 0) {
+                throw new RuntimeException("Only version 0 supported");
+            }
+        }
+
+        private void addState(int sensor, boolean enabled) {
+            mIndividualEnabled.put(sensor, new SensorState(enabled));
+        }
+
+        private void upgrade() {
+            // No op, only version 0 is supported
+        }
+    }
+
+    // Structure for persistence version 1
+    private static class PVersion1 {
+        private SparseArray<SparseArray<SensorState>> mIndividualEnabled = new SparseArray<>();
+
+        private PVersion1(int version) {
+            if (version != 1) {
+                throw new RuntimeException("Only version 1 supported");
+            }
+        }
+
+        private static PVersion1 fromPVersion0(PVersion0 version0) {
+            version0.upgrade();
+
+            PVersion1 result = new PVersion1(1);
+
+            int[] users = {UserHandle.USER_SYSTEM};
+            try {
+                users = LocalServices.getService(UserManagerInternal.class).getUserIds();
+            } catch (Exception e) {
+                Log.e(LOG_TAG, "Unable to get users.", e);
+            }
+
+            // Copy global state to each user
+            for (int i = 0; i < users.length; i++) {
+                int userId = users[i];
+
+                for (int j = 0; j < version0.mIndividualEnabled.size(); j++) {
+                    final int sensor = version0.mIndividualEnabled.keyAt(j);
+                    final SensorState sensorState = version0.mIndividualEnabled.valueAt(j);
+
+                    result.addState(userId, sensor, sensorState.isEnabled());
+                }
+            }
+
+            return result;
+        }
+
+        private void addState(int userId, int sensor, boolean enabled) {
+            SparseArray<SensorState> userIndividualSensorEnabled =
+                    mIndividualEnabled.get(userId, new SparseArray<>());
+            mIndividualEnabled.put(userId, userIndividualSensorEnabled);
+
+            userIndividualSensorEnabled
+                    .put(sensor, new SensorState(enabled));
+        }
+
+        private void upgrade() {
+            // No op, only version 1 is supported
+        }
+    }
+
+    // Structure for persistence version 2
+    private static class PVersion2 {
+        private ArrayMap<TypeUserSensor, SensorState> mStates = new ArrayMap<>();
+
+        private PVersion2(int version) {
+            if (version != 2) {
+                throw new RuntimeException("Only version 2 supported");
+            }
+        }
+
+        private static PVersion2 fromPVersion1(PVersion1 version1) {
+            version1.upgrade();
+
+            PVersion2 result = new PVersion2(2);
+
+            SparseArray<SparseArray<SensorState>> individualEnabled =
+                    version1.mIndividualEnabled;
+            int numUsers = individualEnabled.size();
+            for (int i = 0; i < numUsers; i++) {
+                int userId = individualEnabled.keyAt(i);
+                SparseArray<SensorState> userIndividualEnabled = individualEnabled.valueAt(i);
+                int numSensors = userIndividualEnabled.size();
+                for (int j = 0; j < numSensors; j++) {
+                    int sensor = userIndividualEnabled.keyAt(j);
+                    SensorState sensorState = userIndividualEnabled.valueAt(j);
+                    result.addState(SensorPrivacyManager.ToggleTypes.SOFTWARE,
+                            userId, sensor, sensorState.getState(), sensorState.getLastChange());
+                }
+            }
+
+            return result;
+        }
+
+        private void addState(int toggleType, int userId, int sensor, int state,
+                long lastChange) {
+            mStates.put(new TypeUserSensor(toggleType, userId, sensor),
+                    new SensorState(state, lastChange));
+        }
+    }
+
+    public void resetForTesting() {
+        mStates = new ArrayMap<>();
+    }
+}
diff --git a/services/core/java/com/android/server/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
similarity index 71%
rename from services/core/java/com/android/server/SensorPrivacyService.java
rename to services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 7a4d11c..e9b5f11 100644
--- a/services/core/java/com/android/server/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server;
+package com.android.server.sensorprivacy;
 
 import static android.Manifest.permission.MANAGE_SENSOR_PRIVACY;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
@@ -40,10 +40,10 @@
 import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;
 import static android.hardware.SensorPrivacyManager.Sources.SHELL;
 import static android.os.UserHandle.USER_NULL;
-import static android.os.UserHandle.USER_SYSTEM;
 import static android.service.SensorPrivacyIndividualEnabledSensorProto.UNKNOWN;
 
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION;
+import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__CAMERA;
@@ -83,7 +83,6 @@
 import android.hardware.SensorPrivacyManagerInternal;
 import android.os.Binder;
 import android.os.Bundle;
-import android.os.Environment;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -97,26 +96,19 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.service.SensorPrivacyIndividualEnabledSensorProto;
-import android.service.SensorPrivacyServiceDumpProto;
-import android.service.SensorPrivacyUserProto;
 import android.service.voice.VoiceInteractionManagerInternal;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
 import android.telephony.emergency.EmergencyNumber;
 import android.text.Html;
+import android.text.Spanned;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
-import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
-import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
@@ -125,19 +117,14 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FunctionalUtils;
-import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.FgThread;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -151,32 +138,10 @@
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_LOGGING = false;
 
-    /** Version number indicating compatibility parsing the persisted file */
-    private static final int CURRENT_PERSISTENCE_VERSION = 1;
-    /** Version number indicating the persisted data needs upgraded to match new internal data
-     *  structures and features */
-    private static final int CURRENT_VERSION = 1;
-
-    private static final String SENSOR_PRIVACY_XML_FILE = "sensor_privacy.xml";
-    private static final String XML_TAG_SENSOR_PRIVACY = "sensor-privacy";
-    private static final String XML_TAG_USER = "user";
-    private static final String XML_TAG_INDIVIDUAL_SENSOR_PRIVACY = "individual-sensor-privacy";
-    private static final String XML_ATTRIBUTE_ID = "id";
-    private static final String XML_ATTRIBUTE_PERSISTENCE_VERSION = "persistence-version";
-    private static final String XML_ATTRIBUTE_VERSION = "version";
-    private static final String XML_ATTRIBUTE_ENABLED = "enabled";
-    private static final String XML_ATTRIBUTE_LAST_CHANGE = "last-change";
-    private static final String XML_ATTRIBUTE_SENSOR = "sensor";
-
     private static final String SENSOR_PRIVACY_CHANNEL_ID = Context.SENSOR_PRIVACY_SERVICE;
     private static final String ACTION_DISABLE_INDIVIDUAL_SENSOR_PRIVACY =
             SensorPrivacyService.class.getName() + ".action.disable_sensor_privacy";
 
-    // These are associated with fields that existed for older persisted versions of files
-    private static final int VER0_ENABLED = 0;
-    private static final int VER0_INDIVIDUAL_ENABLED = 1;
-    private static final int VER1_ENABLED = 0;
-    private static final int VER1_INDIVIDUAL_ENABLED = 1;
     public static final int REMINDER_DIALOG_DELAY_MILLIS = 500;
 
     private final Context mContext;
@@ -200,6 +165,7 @@
 
     public SensorPrivacyService(Context context) {
         super(context);
+
         mContext = context;
         mAppOpsManager = context.getSystemService(AppOpsManager.class);
         mAppOpsManagerInternal = getLocalService(AppOpsManagerInternal.class);
@@ -247,12 +213,8 @@
 
         private final SensorPrivacyHandler mHandler;
         private final Object mLock = new Object();
-        @GuardedBy("mLock")
-        private final AtomicFile mAtomicFile;
-        @GuardedBy("mLock")
-        private SparseBooleanArray mEnabled = new SparseBooleanArray();
-        @GuardedBy("mLock")
-        private SparseArray<SparseArray<SensorState>> mIndividualEnabled = new SparseArray<>();
+
+        private SensorPrivacyStateController mSensorPrivacyStateController;
 
         /**
          * Packages for which not to show sensor use reminders.
@@ -266,34 +228,6 @@
         private final ArrayMap<SensorUseReminderDialogInfo, ArraySet<Integer>>
                 mQueuedSensorUseReminderDialogs = new ArrayMap<>();
 
-        private class SensorState {
-            private boolean mEnabled;
-            private long mLastChange;
-
-            SensorState(boolean enabled) {
-                mEnabled = enabled;
-                mLastChange = getCurrentTimeMillis();
-            }
-
-            SensorState(boolean enabled, long lastChange) {
-                mEnabled = enabled;
-                if (lastChange < 0) {
-                    mLastChange = getCurrentTimeMillis();
-                } else {
-                    mLastChange = lastChange;
-                }
-            }
-
-            boolean setEnabled(boolean enabled) {
-                if (mEnabled != enabled) {
-                    mEnabled = enabled;
-                    mLastChange = getCurrentTimeMillis();
-                    return true;
-                }
-                return false;
-            }
-        }
-
         private class SensorUseReminderDialogInfo {
             private int mTaskId;
             private UserHandle mUser;
@@ -323,14 +257,7 @@
 
         SensorPrivacyServiceImpl() {
             mHandler = new SensorPrivacyHandler(FgThread.get().getLooper(), mContext);
-            File sensorPrivacyFile = new File(Environment.getDataSystemDirectory(),
-                    SENSOR_PRIVACY_XML_FILE);
-            mAtomicFile = new AtomicFile(sensorPrivacyFile);
-            synchronized (mLock) {
-                if (readPersistedSensorPrivacyStateLocked()) {
-                    persistSensorPrivacyStateLocked();
-                }
-            }
+            mSensorPrivacyStateController = SensorPrivacyStateController.getInstance();
 
             int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE,
                     OP_CAMERA, OP_PHONE_CALL_CAMERA};
@@ -349,7 +276,26 @@
                 }
             }, new IntentFilter(ACTION_DISABLE_INDIVIDUAL_SENSOR_PRIVACY),
                     MANAGE_SENSOR_PRIVACY, null, Context.RECEIVER_EXPORTED);
+
+            mContext.registerReceiver(new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    mSensorPrivacyStateController.forEachState(
+                            (toggleType, userId, sensor, state) ->
+                                    logSensorPrivacyToggle(OTHER, sensor, state.isEnabled(),
+                                    state.getLastChange(), true)
+                    );
+                }
+            }, new IntentFilter(Intent.ACTION_SHUTDOWN));
+
             mUserManagerInternal.addUserRestrictionsListener(this);
+
+            mSensorPrivacyStateController.setAllSensorPrivacyListener(
+                    mHandler, mHandler::handleSensorPrivacyChanged);
+            mSensorPrivacyStateController.setSensorPrivacyListener(
+                    mHandler,
+                    (toggleType, userId, sensor, state) -> mHandler.handleSensorPrivacyChanged(
+                            userId, sensor, state.isEnabled()));
         }
 
         @Override
@@ -490,8 +436,9 @@
                 }
             }
 
-            String inputMethodComponent = Settings.Secure.getString(mContext.getContentResolver(),
-                    Settings.Secure.DEFAULT_INPUT_METHOD);
+            String inputMethodComponent = Settings.Secure.getStringForUser(
+                    mContext.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD,
+                    mCurrentUser);
             String inputMethodPackageName = null;
             if (inputMethodComponent != null) {
                 inputMethodPackageName = ComponentName.unflattenFromString(
@@ -546,7 +493,8 @@
         private void enqueueSensorUseReminderDialogAsync(int taskId, @NonNull UserHandle user,
                 @NonNull String packageName, int sensor) {
             mHandler.sendMessage(PooledLambda.obtainMessage(
-                    this:: enqueueSensorUseReminderDialog, taskId, user, packageName, sensor));
+                    SensorPrivacyServiceImpl::enqueueSensorUseReminderDialog, this, taskId, user,
+                    packageName, sensor));
         }
 
         private void enqueueSensorUseReminderDialog(int taskId, @NonNull UserHandle user,
@@ -564,8 +512,8 @@
                     sensors.add(sensor);
                 }
                 mQueuedSensorUseReminderDialogs.put(info, sensors);
-                mHandler.sendMessageDelayed(
-                        PooledLambda.obtainMessage(this::showSensorUserReminderDialog, info),
+                mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
+                        SensorPrivacyServiceImpl::showSensorUserReminderDialog, this, info),
                         REMINDER_DIALOG_DELAY_MILLIS);
                 return;
             }
@@ -655,28 +603,32 @@
             notificationManager.createNotificationChannel(channel);
 
             Icon icon = Icon.createWithResource(getUiContext().getResources(), iconRes);
+
+            String contentTitle = getUiContext().getString(messageRes);
+            Spanned contentText = Html.fromHtml(getUiContext().getString(
+                    R.string.sensor_privacy_start_use_notification_content_text, packageLabel), 0);
+            PendingIntent contentIntent = PendingIntent.getActivity(mContext, sensor,
+                    new Intent(Settings.ACTION_PRIVACY_SETTINGS),
+                    PendingIntent.FLAG_IMMUTABLE
+                            | PendingIntent.FLAG_UPDATE_CURRENT);
+
+            String actionTitle = getUiContext().getString(
+                    R.string.sensor_privacy_start_use_dialog_turn_on_button);
+            PendingIntent actionIntent = PendingIntent.getBroadcast(mContext, sensor,
+                    new Intent(ACTION_DISABLE_INDIVIDUAL_SENSOR_PRIVACY)
+                            .setPackage(mContext.getPackageName())
+                            .putExtra(EXTRA_SENSOR, sensor)
+                            .putExtra(Intent.EXTRA_USER, user),
+                    PendingIntent.FLAG_IMMUTABLE
+                            | PendingIntent.FLAG_UPDATE_CURRENT);
             notificationManager.notify(notificationId,
                     new Notification.Builder(mContext, SENSOR_PRIVACY_CHANNEL_ID)
-                            .setContentTitle(getUiContext().getString(messageRes))
-                            .setContentText(Html.fromHtml(getUiContext().getString(
-                                    R.string.sensor_privacy_start_use_notification_content_text,
-                                    packageLabel),0))
+                            .setContentTitle(contentTitle)
+                            .setContentText(contentText)
                             .setSmallIcon(icon)
                             .addAction(new Notification.Action.Builder(icon,
-                                    getUiContext().getString(
-                                            R.string.sensor_privacy_start_use_dialog_turn_on_button),
-                                    PendingIntent.getBroadcast(mContext, sensor,
-                                            new Intent(ACTION_DISABLE_INDIVIDUAL_SENSOR_PRIVACY)
-                                                    .setPackage(mContext.getPackageName())
-                                                    .putExtra(EXTRA_SENSOR, sensor)
-                                                    .putExtra(Intent.EXTRA_USER, user),
-                                            PendingIntent.FLAG_IMMUTABLE
-                                                    | PendingIntent.FLAG_UPDATE_CURRENT))
-                                    .build())
-                            .setContentIntent(PendingIntent.getActivity(mContext, sensor,
-                                    new Intent(Settings.ACTION_PRIVACY_SETTINGS),
-                                    PendingIntent.FLAG_IMMUTABLE
-                                            | PendingIntent.FLAG_UPDATE_CURRENT))
+                                    actionTitle, actionIntent).build())
+                            .setContentIntent(contentIntent)
                             .extend(new Notification.TvExtender())
                             .setTimeoutAfter(isTelevision(mContext)
                                     ? /* dismiss immediately */ 1
@@ -697,16 +649,7 @@
         @Override
         public void setSensorPrivacy(boolean enable) {
             enforceManageSensorPrivacyPermission();
-            // Keep the state consistent between all users to make it a single global state
-            forAllUsers(userId -> setSensorPrivacy(userId, enable));
-        }
-
-        private void setSensorPrivacy(@UserIdInt int userId, boolean enable) {
-            synchronized (mLock) {
-                mEnabled.put(userId, enable);
-                persistSensorPrivacyStateLocked();
-            }
-            mHandler.onSensorPrivacyChanged(enable);
+            mSensorPrivacyStateController.setAllSensorState(enable);
         }
 
         @Override
@@ -735,43 +678,23 @@
 
         private void setIndividualSensorPrivacyUnchecked(int userId, int source, int sensor,
                 boolean enable) {
-            synchronized (mLock) {
-                SparseArray<SensorState> userIndividualEnabled = mIndividualEnabled.get(userId,
-                        new SparseArray<>());
-                SensorState sensorState = userIndividualEnabled.get(sensor);
-                long lastChange;
-                if (sensorState != null) {
-                    lastChange = sensorState.mLastChange;
-                    if (!sensorState.setEnabled(enable)) {
-                        // State not changing
-                        return;
-                    }
-                } else {
-                    sensorState = new SensorState(enable);
-                    lastChange = sensorState.mLastChange;
-                    userIndividualEnabled.put(sensor, sensorState);
-                }
-                mIndividualEnabled.put(userId, userIndividualEnabled);
-
-                if (userId == mUserManagerInternal.getProfileParentId(userId)) {
-                    logSensorPrivacyToggle(source, sensor, sensorState.mEnabled, lastChange);
-                }
-
-                if (!enable) {
-                    final long token = Binder.clearCallingIdentity();
-                    try {
-                        // Remove any notifications prompting the user to disable sensory privacy
-                        NotificationManager notificationManager =
-                                mContext.getSystemService(NotificationManager.class);
-
-                        notificationManager.cancel(sensor);
-                    } finally {
-                        Binder.restoreCallingIdentity(token);
-                    }
-                }
-                persistSensorPrivacyState();
-            }
-            mHandler.onSensorPrivacyChanged(userId, sensor, enable);
+            final long[] lastChange = new long[1];
+            mSensorPrivacyStateController.atomic(() -> {
+                SensorState sensorState = mSensorPrivacyStateController
+                        .getState(SensorPrivacyManager.ToggleTypes.SOFTWARE, userId, sensor);
+                lastChange[0] = sensorState.getLastChange();
+                mSensorPrivacyStateController.setState(
+                        SensorPrivacyManager.ToggleTypes.SOFTWARE, userId, sensor, enable, mHandler,
+                        changeSuccessful -> {
+                            if (changeSuccessful) {
+                                if (userId == mUserManagerInternal.getProfileParentId(userId)) {
+                                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                                            SensorPrivacyServiceImpl::logSensorPrivacyToggle, this,
+                                            source, sensor, enable, lastChange[0], false));
+                                }
+                            }
+                        });
+            });
         }
 
         private boolean canChangeIndividualSensorPrivacy(@UserIdInt int userId, int sensor) {
@@ -801,14 +724,23 @@
         }
 
         private void logSensorPrivacyToggle(int source, int sensor, boolean enabled,
-                long lastChange) {
+                long lastChange, boolean onShutDown) {
             long logMins = Math.max(0, (getCurrentTimeMillis() - lastChange) / (1000 * 60));
 
             int logAction = -1;
-            if (enabled) {
-                logAction = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
+            if (onShutDown) {
+                // TODO ACTION_POWER_OFF_WHILE_(ON/OFF)
+                if (enabled) {
+                    logAction = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
+                } else {
+                    logAction = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
+                }
             } else {
-                logAction = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
+                if (enabled) {
+                    logAction = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
+                } else {
+                    logAction = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
+                }
             }
 
             int logSensor = -1;
@@ -895,13 +827,7 @@
         @Override
         public boolean isSensorPrivacyEnabled() {
             enforceObserveSensorPrivacyPermission();
-            return isSensorPrivacyEnabled(USER_SYSTEM);
-        }
-
-        private boolean isSensorPrivacyEnabled(@UserIdInt int userId) {
-            synchronized (mLock) {
-                return mEnabled.get(userId, false);
-            }
+            return mSensorPrivacyStateController.getAllSensorState();
         }
 
         @Override
@@ -918,239 +844,8 @@
             if (userId == UserHandle.USER_CURRENT) {
                 userId = mCurrentUser;
             }
-            synchronized (mLock) {
-                return isIndividualSensorPrivacyEnabledLocked(userId, sensor);
-            }
-        }
-
-        private boolean isIndividualSensorPrivacyEnabledLocked(int userId, int sensor) {
-            SparseArray<SensorState> states = mIndividualEnabled.get(userId);
-            if (states == null) {
-                return false;
-            }
-            SensorState state = states.get(sensor);
-            if (state == null) {
-                return false;
-            }
-            return state.mEnabled;
-        }
-
-        /**
-         * Returns the state of sensor privacy from persistent storage.
-         */
-        private boolean readPersistedSensorPrivacyStateLocked() {
-            // if the file does not exist then sensor privacy has not yet been enabled on
-            // the device.
-
-            SparseArray<Object> map = new SparseArray<>();
-            int version = -1;
-
-            if (mAtomicFile.exists()) {
-                try (FileInputStream inputStream = mAtomicFile.openRead()) {
-                    TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
-                    XmlUtils.beginDocument(parser, XML_TAG_SENSOR_PRIVACY);
-                    final int persistenceVersion = parser.getAttributeInt(null,
-                            XML_ATTRIBUTE_PERSISTENCE_VERSION, 0);
-
-                    // Use inline string literals for xml tags/attrs when parsing old versions since
-                    // these should never be changed even with refactorings.
-                    if (persistenceVersion == 0) {
-                        boolean enabled = parser.getAttributeBoolean(null, "enabled", false);
-                        SparseArray<SensorState> individualEnabled = new SparseArray<>();
-                        version = 0;
-
-                        XmlUtils.nextElement(parser);
-                        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
-                            String tagName = parser.getName();
-                            if ("individual-sensor-privacy".equals(tagName)) {
-                                int sensor = XmlUtils.readIntAttribute(parser, "sensor");
-                                boolean indEnabled = XmlUtils.readBooleanAttribute(parser,
-                                        "enabled");
-                                individualEnabled.put(sensor, new SensorState(indEnabled));
-                                XmlUtils.skipCurrentTag(parser);
-                            } else {
-                                XmlUtils.nextElement(parser);
-                            }
-                        }
-                        map.put(VER0_ENABLED, enabled);
-                        map.put(VER0_INDIVIDUAL_ENABLED, individualEnabled);
-                    } else if (persistenceVersion == CURRENT_PERSISTENCE_VERSION) {
-                        SparseBooleanArray enabled = new SparseBooleanArray();
-                        SparseArray<SparseArray<SensorState>> individualEnabled =
-                                new SparseArray<>();
-                        version = parser.getAttributeInt(null,
-                                XML_ATTRIBUTE_VERSION, 1);
-
-                        int currentUserId = -1;
-                        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
-                            XmlUtils.nextElement(parser);
-                            String tagName = parser.getName();
-                            if (XML_TAG_USER.equals(tagName)) {
-                                currentUserId = parser.getAttributeInt(null, XML_ATTRIBUTE_ID);
-                                boolean isEnabled = parser.getAttributeBoolean(null,
-                                        XML_ATTRIBUTE_ENABLED);
-                                if (enabled.indexOfKey(currentUserId) >= 0) {
-                                    Log.e(TAG, "User listed multiple times in file.",
-                                            new RuntimeException());
-                                    mAtomicFile.delete();
-                                    version = -1;
-                                    break;
-                                }
-
-                                if (mUserManagerInternal.getUserInfo(currentUserId) == null) {
-                                    // User may no longer exist, skip this user
-                                    currentUserId = -1;
-                                    continue;
-                                }
-
-                                enabled.put(currentUserId, isEnabled);
-                            }
-                            if (XML_TAG_INDIVIDUAL_SENSOR_PRIVACY.equals(tagName)) {
-                                if (mUserManagerInternal.getUserInfo(currentUserId) == null) {
-                                    // User may no longer exist or isn't set
-                                    continue;
-                                }
-                                int sensor = parser.getAttributeInt(null, XML_ATTRIBUTE_SENSOR);
-                                boolean isEnabled = parser.getAttributeBoolean(null,
-                                        XML_ATTRIBUTE_ENABLED);
-                                long lastChange = parser
-                                        .getAttributeLong(null, XML_ATTRIBUTE_LAST_CHANGE, -1);
-                                SparseArray<SensorState> userIndividualEnabled =
-                                        individualEnabled.get(currentUserId, new SparseArray<>());
-
-                                userIndividualEnabled
-                                        .put(sensor, new SensorState(isEnabled, lastChange));
-                                individualEnabled.put(currentUserId, userIndividualEnabled);
-                            }
-                        }
-
-                        map.put(VER1_ENABLED, enabled);
-                        map.put(VER1_INDIVIDUAL_ENABLED, individualEnabled);
-                    } else {
-                        Log.e(TAG, "Unknown persistence version: " + persistenceVersion
-                                        + ". Deleting.",
-                                new RuntimeException());
-                        mAtomicFile.delete();
-                        version = -1;
-                    }
-
-                } catch (IOException | XmlPullParserException e) {
-                    Log.e(TAG, "Caught an exception reading the state from storage: ", e);
-                    // Delete the file to prevent the same error on subsequent calls and assume
-                    // sensor privacy is not enabled.
-                    mAtomicFile.delete();
-                    version = -1;
-                }
-            }
-
-            try {
-                return upgradeAndInit(version, map);
-            } catch (Exception e) {
-                Log.wtf(TAG, "Failed to upgrade and set sensor privacy state,"
-                        + " resetting to default.", e);
-                mEnabled = new SparseBooleanArray();
-                mIndividualEnabled = new SparseArray<>();
-                return true;
-            }
-        }
-
-        private boolean upgradeAndInit(int version, SparseArray map) {
-            if (version == -1) {
-                // New file, default state for current version goes here.
-                mEnabled = new SparseBooleanArray();
-                mIndividualEnabled = new SparseArray<>();
-                forAllUsers(userId -> mEnabled.put(userId, false));
-                forAllUsers(userId -> mIndividualEnabled.put(userId, new SparseArray<>()));
-                return true;
-            }
-            boolean upgraded = false;
-            final int[] users = getLocalService(UserManagerInternal.class).getUserIds();
-            if (version == 0) {
-                final boolean enabled = (boolean) map.get(VER0_ENABLED);
-                final SparseArray<SensorState> individualEnabled =
-                        (SparseArray<SensorState>) map.get(VER0_INDIVIDUAL_ENABLED);
-
-                final SparseBooleanArray perUserEnabled = new SparseBooleanArray();
-                final SparseArray<SparseArray<SensorState>> perUserIndividualEnabled =
-                        new SparseArray<>();
-
-                // Copy global state to each user
-                for (int i = 0; i < users.length; i++) {
-                    int user = users[i];
-                    perUserEnabled.put(user, enabled);
-                    SparseArray<SensorState> userIndividualSensorEnabled = new SparseArray<>();
-                    perUserIndividualEnabled.put(user, userIndividualSensorEnabled);
-                    for (int j = 0; j < individualEnabled.size(); j++) {
-                        final int sensor = individualEnabled.keyAt(j);
-                        final SensorState isSensorEnabled = individualEnabled.valueAt(j);
-                        userIndividualSensorEnabled.put(sensor, isSensorEnabled);
-                    }
-                }
-
-                map.clear();
-                map.put(VER1_ENABLED, perUserEnabled);
-                map.put(VER1_INDIVIDUAL_ENABLED, perUserIndividualEnabled);
-
-                version = 1;
-                upgraded = true;
-            }
-            if (version == CURRENT_VERSION) {
-                mEnabled = (SparseBooleanArray) map.get(VER1_ENABLED);
-                mIndividualEnabled =
-                        (SparseArray<SparseArray<SensorState>>) map.get(VER1_INDIVIDUAL_ENABLED);
-            }
-            return upgraded;
-        }
-
-        /**
-         * Persists the state of sensor privacy.
-         */
-        private void persistSensorPrivacyState() {
-            synchronized (mLock) {
-                persistSensorPrivacyStateLocked();
-            }
-        }
-
-        private void persistSensorPrivacyStateLocked() {
-            FileOutputStream outputStream = null;
-            try {
-                outputStream = mAtomicFile.startWrite();
-                TypedXmlSerializer serializer = Xml.resolveSerializer(outputStream);
-                serializer.startDocument(null, true);
-                serializer.startTag(null, XML_TAG_SENSOR_PRIVACY);
-                serializer.attributeInt(
-                        null, XML_ATTRIBUTE_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION);
-                serializer.attributeInt(null, XML_ATTRIBUTE_VERSION, CURRENT_VERSION);
-                forAllUsers(userId -> {
-                    serializer.startTag(null, XML_TAG_USER);
-                    serializer.attributeInt(null, XML_ATTRIBUTE_ID, userId);
-                    serializer.attributeBoolean(
-                            null, XML_ATTRIBUTE_ENABLED, isSensorPrivacyEnabled(userId));
-
-                    SparseArray<SensorState> individualEnabled =
-                            mIndividualEnabled.get(userId, new SparseArray<>());
-                    int numIndividual = individualEnabled.size();
-                    for (int i = 0; i < numIndividual; i++) {
-                        serializer.startTag(null, XML_TAG_INDIVIDUAL_SENSOR_PRIVACY);
-                        int sensor = individualEnabled.keyAt(i);
-                        SensorState sensorState = individualEnabled.valueAt(i);
-                        boolean enabled = sensorState.mEnabled;
-                        long lastChange = sensorState.mLastChange;
-                        serializer.attributeInt(null, XML_ATTRIBUTE_SENSOR, sensor);
-                        serializer.attributeBoolean(null, XML_ATTRIBUTE_ENABLED, enabled);
-                        serializer.attributeLong(null, XML_ATTRIBUTE_LAST_CHANGE, lastChange);
-                        serializer.endTag(null, XML_TAG_INDIVIDUAL_SENSOR_PRIVACY);
-                    }
-                    serializer.endTag(null, XML_TAG_USER);
-
-                });
-                serializer.endTag(null, XML_TAG_SENSOR_PRIVACY);
-                serializer.endDocument();
-                mAtomicFile.finishWrite(outputStream);
-            } catch (IOException e) {
-                Log.e(TAG, "Caught an exception persisting the sensor privacy state: ", e);
-                mAtomicFile.failWrite(outputStream);
-            }
+            return mSensorPrivacyStateController.getState(SensorPrivacyManager.ToggleTypes.SOFTWARE,
+                    userId, sensor).isEnabled();
         }
 
         @Override
@@ -1285,23 +980,23 @@
         }
 
         private void userSwitching(int from, int to) {
-            boolean micState;
-            boolean camState;
-            boolean prevMicState;
-            boolean prevCamState;
-            synchronized (mLock) {
-                prevMicState = isIndividualSensorPrivacyEnabledLocked(from, MICROPHONE);
-                prevCamState = isIndividualSensorPrivacyEnabledLocked(from, CAMERA);
-                micState = isIndividualSensorPrivacyEnabledLocked(to, MICROPHONE);
-                camState = isIndividualSensorPrivacyEnabledLocked(to, CAMERA);
+            final boolean[] micState = new boolean[1];
+            final boolean[] camState = new boolean[1];
+            final boolean[] prevMicState = new boolean[1];
+            final boolean[] prevCamState = new boolean[1];
+            mSensorPrivacyStateController.atomic(() -> {
+                prevMicState[0] = isIndividualSensorPrivacyEnabled(from, MICROPHONE);
+                prevCamState[0] = isIndividualSensorPrivacyEnabled(from, CAMERA);
+                micState[0] = isIndividualSensorPrivacyEnabled(to, MICROPHONE);
+                camState[0] = isIndividualSensorPrivacyEnabled(to, CAMERA);
+            });
+            if (from == USER_NULL || prevMicState[0] != micState[0]) {
+                mHandler.onUserGlobalSensorPrivacyChanged(MICROPHONE, micState[0]);
+                setGlobalRestriction(MICROPHONE, micState[0]);
             }
-            if (from == USER_NULL || prevMicState != micState) {
-                mHandler.onUserGlobalSensorPrivacyChanged(MICROPHONE, micState);
-                setGlobalRestriction(MICROPHONE, micState);
-            }
-            if (from == USER_NULL || prevCamState != camState) {
-                mHandler.onUserGlobalSensorPrivacyChanged(CAMERA, camState);
-                setGlobalRestriction(CAMERA, camState);
+            if (from == USER_NULL || prevCamState[0] != camState[0]) {
+                mHandler.onUserGlobalSensorPrivacyChanged(CAMERA, camState[0]);
+                setGlobalRestriction(CAMERA, camState[0]);
             }
         }
 
@@ -1395,12 +1090,14 @@
             final long identity = Binder.clearCallingIdentity();
             try {
                 if (dumpAsProto) {
-                    dump(new DualDumpOutputStream(new ProtoOutputStream(fd)));
+                    mSensorPrivacyStateController.dump(
+                            new DualDumpOutputStream(new ProtoOutputStream(fd)));
                 } else {
                     pw.println("SENSOR PRIVACY MANAGER STATE (dumpsys "
                             + Context.SENSOR_PRIVACY_SERVICE + ")");
 
-                    dump(new DualDumpOutputStream(new IndentingPrintWriter(pw, "  ")));
+                    mSensorPrivacyStateController.dump(
+                            new DualDumpOutputStream(new IndentingPrintWriter(pw, "  ")));
                 }
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -1408,45 +1105,6 @@
         }
 
         /**
-         * Dump state to {@link DualDumpOutputStream}.
-         *
-         * @param dumpStream The destination to dump to
-         */
-        private void dump(@NonNull DualDumpOutputStream dumpStream) {
-            synchronized (mLock) {
-
-                forAllUsers(userId -> {
-                    long userToken = dumpStream.start("users", SensorPrivacyServiceDumpProto.USER);
-                    dumpStream.write("user_id", SensorPrivacyUserProto.USER_ID, userId);
-                    dumpStream.write("is_enabled", SensorPrivacyUserProto.IS_ENABLED,
-                            mEnabled.get(userId, false));
-
-                    SparseArray<SensorState> individualEnabled = mIndividualEnabled.get(userId);
-                    if (individualEnabled != null) {
-                        int numIndividualEnabled = individualEnabled.size();
-                        for (int i = 0; i < numIndividualEnabled; i++) {
-                            long individualToken = dumpStream.start("individual_enabled_sensor",
-                                    SensorPrivacyUserProto.INDIVIDUAL_ENABLED_SENSOR);
-
-                            dumpStream.write("sensor",
-                                    SensorPrivacyIndividualEnabledSensorProto.SENSOR,
-                                    individualEnabled.keyAt(i));
-                            dumpStream.write("is_enabled",
-                                    SensorPrivacyIndividualEnabledSensorProto.IS_ENABLED,
-                                    individualEnabled.valueAt(i).mEnabled);
-                            // TODO dump last change
-
-                            dumpStream.end(individualToken);
-                        }
-                    }
-                    dumpStream.end(userToken);
-                });
-            }
-
-            dumpStream.flush();
-        }
-
-        /**
          * Convert a string into a {@link SensorPrivacyManager.Sensors.Sensor id}.
          *
          * @param sensor The name to convert
@@ -1504,25 +1162,6 @@
                             setIndividualSensorPrivacy(userId, SHELL, sensor, false);
                         }
                         break;
-                        case "reset": {
-                            int sensor = sensorStrToId(getNextArgRequired());
-                            if (sensor == UNKNOWN) {
-                                pw.println("Invalid sensor");
-                                return -1;
-                            }
-
-                            enforceManageSensorPrivacyPermission();
-
-                            synchronized (mLock) {
-                                SparseArray<SensorState> individualEnabled =
-                                        mIndividualEnabled.get(userId);
-                                if (individualEnabled != null) {
-                                    individualEnabled.delete(sensor);
-                                }
-                                persistSensorPrivacyState();
-                            }
-                        }
-                        break;
                         default:
                             return handleDefaultCommands(cmd);
                     }
@@ -1545,9 +1184,6 @@
                     pw.println("  disable USER_ID SENSOR");
                     pw.println("    Disable privacy for a certain sensor.");
                     pw.println("");
-                    pw.println("  reset USER_ID SENSOR");
-                    pw.println("    Reset privacy state for a certain sensor.");
-                    pw.println("");
                 }
             }).exec(this, in, out, err, args, callback, resultReceiver);
         }
@@ -1581,22 +1217,6 @@
             mContext = context;
         }
 
-        public void onSensorPrivacyChanged(boolean enabled) {
-            sendMessage(PooledLambda.obtainMessage(SensorPrivacyHandler::handleSensorPrivacyChanged,
-                    this, enabled));
-            sendMessage(
-                    PooledLambda.obtainMessage(SensorPrivacyServiceImpl::persistSensorPrivacyState,
-                            mSensorPrivacyServiceImpl));
-        }
-
-        public void onSensorPrivacyChanged(int userId, int sensor, boolean enabled) {
-            sendMessage(PooledLambda.obtainMessage(SensorPrivacyHandler::handleSensorPrivacyChanged,
-                    this, userId, sensor, enabled));
-            sendMessage(
-                    PooledLambda.obtainMessage(SensorPrivacyServiceImpl::persistSensorPrivacyState,
-                            mSensorPrivacyServiceImpl));
-        }
-
         public void onUserGlobalSensorPrivacyChanged(int sensor, boolean enabled) {
             sendMessage(PooledLambda.obtainMessage(
                     SensorPrivacyHandler::handleUserGlobalSensorPrivacyChanged,
@@ -1692,6 +1312,7 @@
         }
 
         public void handleSensorPrivacyChanged(int userId, int sensor, boolean enabled) {
+            // TODO handle hardware
             mSensorPrivacyManagerInternal.dispatch(userId, sensor, enabled);
             SparseArray<RemoteCallbackList<ISensorPrivacyListener>> listenersForUser =
                     mIndividualSensorListeners.get(userId);
@@ -1972,11 +1593,7 @@
         }
     }
 
-    private static long getCurrentTimeMillis() {
-        try {
-            return SystemClock.currentNetworkTimeMillis();
-        } catch (Exception e) {
-            return System.currentTimeMillis();
-        }
+    static long getCurrentTimeMillis() {
+        return SystemClock.elapsedRealtime();
     }
 }
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
new file mode 100644
index 0000000..9694958
--- /dev/null
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2021 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.sensorprivacy;
+
+import android.os.Handler;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+abstract class SensorPrivacyStateController {
+
+    private static SensorPrivacyStateController sInstance;
+
+    AllSensorStateController mAllSensorStateController = AllSensorStateController.getInstance();
+
+    private final Object mLock = new Object();
+
+    static SensorPrivacyStateController getInstance() {
+        if (sInstance == null) {
+            sInstance = SensorPrivacyStateControllerImpl.getInstance();
+        }
+
+        return sInstance;
+    }
+
+    SensorState getState(int toggleType, int userId, int sensor) {
+        synchronized (mLock) {
+            return getStateLocked(toggleType, userId, sensor);
+        }
+    }
+
+    void setState(int toggleType, int userId, int sensor, boolean enabled, Handler callbackHandler,
+            SetStateResultCallback callback) {
+        synchronized (mLock) {
+            setStateLocked(toggleType, userId, sensor, enabled, callbackHandler, callback);
+        }
+    }
+
+    void setSensorPrivacyListener(Handler handler,
+            SensorPrivacyListener listener) {
+        synchronized (mLock) {
+            setSensorPrivacyListenerLocked(handler, listener);
+        }
+    }
+
+    // Following calls are for the developer settings sensor mute feature
+    boolean getAllSensorState() {
+        synchronized (mLock) {
+            return mAllSensorStateController.getAllSensorStateLocked();
+        }
+    }
+
+    void setAllSensorState(boolean enable) {
+        synchronized (mLock) {
+            mAllSensorStateController.setAllSensorStateLocked(enable);
+        }
+    }
+
+    void setAllSensorPrivacyListener(Handler handler, AllSensorPrivacyListener listener) {
+        synchronized (mLock) {
+            mAllSensorStateController.setAllSensorPrivacyListenerLocked(handler, listener);
+        }
+    }
+
+    void persistAll() {
+        synchronized (mLock) {
+            mAllSensorStateController.schedulePersistLocked();
+            schedulePersistLocked();
+        }
+    }
+
+    void forEachState(SensorPrivacyStateConsumer consumer) {
+        synchronized (mLock) {
+            forEachStateLocked(consumer);
+        }
+    }
+
+    void dump(DualDumpOutputStream dumpStream) {
+        synchronized (mLock) {
+            mAllSensorStateController.dumpLocked(dumpStream);
+            dumpLocked(dumpStream);
+        }
+        dumpStream.flush();
+    }
+
+    public void atomic(Runnable r) {
+        synchronized (mLock) {
+            r.run();
+        }
+    }
+
+    interface SensorPrivacyListener {
+        void onSensorPrivacyChanged(int toggleType, int userId, int sensor, SensorState state);
+    }
+
+    interface SensorPrivacyStateConsumer {
+        void accept(int toggleType, int userId, int sensor, SensorState state);
+    }
+
+    interface SetStateResultCallback {
+        void callback(boolean changed);
+    }
+
+    interface AllSensorPrivacyListener {
+        void onAllSensorPrivacyChanged(boolean enabled);
+    }
+
+    @GuardedBy("mLock")
+    abstract SensorState getStateLocked(int toggleType, int userId, int sensor);
+
+    @GuardedBy("mLock")
+    abstract void setStateLocked(int toggleType, int userId, int sensor, boolean enabled,
+            Handler callbackHandler, SetStateResultCallback callback);
+
+    @GuardedBy("mLock")
+    abstract void setSensorPrivacyListenerLocked(Handler handler,
+            SensorPrivacyListener listener);
+
+    @GuardedBy("mLock")
+    abstract void schedulePersistLocked();
+
+    @GuardedBy("mLock")
+    abstract void forEachStateLocked(SensorPrivacyStateConsumer consumer);
+
+    @GuardedBy("mLock")
+    abstract void dumpLocked(DualDumpOutputStream dumpStream);
+
+    static void sendSetStateCallback(Handler callbackHandler,
+            SetStateResultCallback callback, boolean success) {
+        callbackHandler.sendMessage(PooledLambda.obtainMessage(SetStateResultCallback::callback,
+                callback, success));
+    }
+
+    /**
+     * Used for unit testing
+     */
+    void resetForTesting() {
+        mAllSensorStateController.resetForTesting();
+        resetForTestingImpl();
+        sInstance = null;
+    }
+    abstract void resetForTestingImpl();
+}
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
new file mode 100644
index 0000000..d1ea8e9
--- /dev/null
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2021 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.sensorprivacy;
+
+import android.hardware.SensorPrivacyManager;
+import android.os.Handler;
+
+import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import java.util.Objects;
+
+class SensorPrivacyStateControllerImpl extends SensorPrivacyStateController {
+
+    private static final String SENSOR_PRIVACY_XML_FILE = "sensor_privacy_impl.xml";
+
+    private static SensorPrivacyStateControllerImpl sInstance;
+
+    private PersistedState mPersistedState;
+
+    private SensorPrivacyListener mListener;
+    private Handler mListenerHandler;
+
+    static SensorPrivacyStateController getInstance() {
+        if (sInstance == null) {
+            sInstance = new SensorPrivacyStateControllerImpl();
+        }
+        return sInstance;
+    }
+
+    private SensorPrivacyStateControllerImpl() {
+        mPersistedState = PersistedState.fromFile(SENSOR_PRIVACY_XML_FILE);
+        persistAll();
+    }
+
+    @Override
+    SensorState getStateLocked(int toggleType, int userId, int sensor) {
+        if (toggleType == SensorPrivacyManager.ToggleTypes.HARDWARE) {
+            // Device doesn't support hardware state
+            return getDefaultSensorState();
+        }
+        SensorState sensorState = mPersistedState.getState(toggleType, userId, sensor);
+        if (sensorState != null) {
+            return new SensorState(sensorState);
+        }
+        return getDefaultSensorState();
+    }
+
+    private static SensorState getDefaultSensorState() {
+        return new SensorState(false);
+    }
+
+    @Override
+    void setStateLocked(int toggleType, int userId, int sensor, boolean enabled,
+            Handler callbackHandler, SetStateResultCallback callback) {
+        if (toggleType != SensorPrivacyManager.ToggleTypes.SOFTWARE) {
+            // Implementation only supports software switch
+            callbackHandler.sendMessage(PooledLambda.obtainMessage(
+                    SetStateResultCallback::callback, callback, false));
+            return;
+        }
+        // Changing the SensorState's mEnabled updates the timestamp of its last change.
+        // A nonexistent state -> unmuted should not set the timestamp.
+        SensorState lastState = mPersistedState.getState(toggleType, userId, sensor);
+        if (lastState == null) {
+            if (!enabled) {
+                sendSetStateCallback(callbackHandler, callback, false);
+                return;
+            } else if (enabled) {
+                SensorState sensorState = new SensorState(true);
+                mPersistedState.setState(toggleType, userId, sensor, sensorState);
+                notifyStateChangeLocked(toggleType, userId, sensor, sensorState);
+                sendSetStateCallback(callbackHandler, callback, true);
+                return;
+            }
+        }
+        if (lastState.setEnabled(enabled)) {
+            notifyStateChangeLocked(toggleType, userId, sensor, lastState);
+            sendSetStateCallback(callbackHandler, callback, true);
+            return;
+        }
+        sendSetStateCallback(callbackHandler, callback, false);
+    }
+
+    private void notifyStateChangeLocked(int toggleType, int userId, int sensor,
+            SensorState sensorState) {
+        if (mListenerHandler != null && mListener != null) {
+            mListenerHandler.sendMessage(PooledLambda.obtainMessage(
+                    SensorPrivacyListener::onSensorPrivacyChanged, mListener,
+                    toggleType, userId, sensor, new SensorState(sensorState)));
+        }
+        schedulePersistLocked();
+    }
+
+    @Override
+    void setSensorPrivacyListenerLocked(Handler handler, SensorPrivacyListener listener) {
+        Objects.requireNonNull(handler);
+        Objects.requireNonNull(listener);
+        if (mListener != null) {
+            throw new IllegalStateException("Listener is already set");
+        }
+        mListener = listener;
+        mListenerHandler = handler;
+    }
+
+    @Override
+    void schedulePersistLocked() {
+        mPersistedState.schedulePersist();
+    }
+
+    @Override
+    void forEachStateLocked(SensorPrivacyStateConsumer consumer) {
+        mPersistedState.forEachKnownState(consumer::accept);
+    }
+
+    @Override
+    void resetForTestingImpl() {
+        mPersistedState.resetForTesting();
+        mListener = null;
+        mListenerHandler = null;
+        sInstance = null;
+    }
+
+    @Override
+    void dumpLocked(DualDumpOutputStream dumpStream) {
+        mPersistedState.dump(dumpStream);
+    }
+}
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorState.java b/services/core/java/com/android/server/sensorprivacy/SensorState.java
new file mode 100644
index 0000000..b92e2c8
--- /dev/null
+++ b/services/core/java/com/android/server/sensorprivacy/SensorState.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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.sensorprivacy;
+
+import static android.hardware.SensorPrivacyManager.StateTypes.DISABLED;
+import static android.hardware.SensorPrivacyManager.StateTypes.ENABLED;
+
+import static com.android.server.sensorprivacy.SensorPrivacyService.getCurrentTimeMillis;
+
+class SensorState {
+
+    private int mStateType;
+    private long mLastChange;
+
+    SensorState(int stateType) {
+        mStateType = stateType;
+        mLastChange = getCurrentTimeMillis();
+    }
+
+    SensorState(int stateType, long lastChange) {
+        mStateType = stateType;
+        mLastChange = Math.min(getCurrentTimeMillis(), lastChange);
+    }
+
+    SensorState(SensorState sensorState) {
+        mStateType = sensorState.getState();
+        mLastChange = sensorState.getLastChange();
+    }
+
+    boolean setState(int stateType) {
+        if (mStateType != stateType) {
+            mStateType = stateType;
+            mLastChange = getCurrentTimeMillis();
+            return true;
+        }
+        return false;
+    }
+
+    int getState() {
+        return mStateType;
+    }
+
+    long getLastChange() {
+        return mLastChange;
+    }
+
+    // Below are some convenience members for when dealing with enabled/disabled
+
+    private static int enabledToState(boolean enabled) {
+        return enabled ? ENABLED : DISABLED;
+    }
+
+    SensorState(boolean enabled) {
+        this(enabledToState(enabled));
+    }
+
+    boolean setEnabled(boolean enabled) {
+        return setState(enabledToState(enabled));
+    }
+
+    boolean isEnabled() {
+        return getState() == ENABLED;
+    }
+
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index f77aa37..74e04ed 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -183,6 +183,7 @@
 import com.android.server.security.FileIntegrityService;
 import com.android.server.security.KeyAttestationApplicationIdProviderService;
 import com.android.server.security.KeyChainSystemService;
+import com.android.server.sensorprivacy.SensorPrivacyService;
 import com.android.server.sensors.SensorService;
 import com.android.server.signedconfig.SignedConfigService;
 import com.android.server.soundtrigger.SoundTriggerService;
diff --git a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file1.xml b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file1.xml
index a4de08a..4e0bb36 100644
--- a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file1.xml
+++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file1.xml
@@ -1,7 +1,7 @@
 <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
 <sensor-privacy persistence-version="1" version="1">
     <user id="0" enabled="false">
-        <individual-sensor-privacy sensor="1" enabled="true" />
-        <individual-sensor-privacy sensor="2" enabled="true" />
+        <individual-sensor-privacy sensor="1" enabled="true" last-change="100" />
+        <individual-sensor-privacy sensor="2" enabled="true" last-change="100" />
     </user>
 </sensor-privacy>
diff --git a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file7.xml b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file7.xml
new file mode 100644
index 0000000..2d192db
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file7.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<sensor-privacy persistence-version="2" version="2">
+    <sensor-state toggle-type="1" user-id="0" sensor="1" state-type="1" last-change="123" />
+    <sensor-state toggle-type="1" user-id="0" sensor="2" state-type="2" last-change="123" />
+</sensor-privacy>
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file8.xml b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file8.xml
new file mode 100644
index 0000000..7bb38b4
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file8.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<sensor-privacy persistence-version="2" version="2">
+    <sensor-state toggle-type="2" user-id="0" sensor="1" state-type="1" last-change="1234" />
+    <sensor-state toggle-type="2" user-id="0" sensor="2" state-type="2" last-change="1234" />
+</sensor-privacy>
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java
index 38f01b5..64e8613 100644
--- a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java
@@ -16,43 +16,47 @@
 
 package com.android.server.sensorprivacy;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
+import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
+import static android.hardware.SensorPrivacyManager.ToggleTypes.HARDWARE;
+import static android.hardware.SensorPrivacyManager.ToggleTypes.SOFTWARE;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 
-import android.app.ActivityManager;
-import android.app.ActivityTaskManager;
-import android.app.AppOpsManager;
-import android.app.AppOpsManagerInternal;
 import android.content.Context;
-import android.content.pm.UserInfo;
+import android.hardware.SensorPrivacyManager;
 import android.os.Environment;
-import android.telephony.TelephonyManager;
+import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.server.LocalServices;
-import com.android.server.SensorPrivacyService;
-import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 
-import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
+import org.mockito.ArgumentCaptor;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.util.concurrent.CompletableFuture;
+import java.nio.file.StandardCopyOption;
 
 @RunWith(AndroidTestingRunner.class)
 public class SensorPrivacyServiceMockingTest {
@@ -71,6 +75,10 @@
             String.format(PERSISTENCE_FILE_PATHS_TEMPLATE, 5);
     public static final String PERSISTENCE_FILE6 =
             String.format(PERSISTENCE_FILE_PATHS_TEMPLATE, 6);
+    public static final String PERSISTENCE_FILE7 =
+            String.format(PERSISTENCE_FILE_PATHS_TEMPLATE, 7);
+    public static final String PERSISTENCE_FILE8 =
+            String.format(PERSISTENCE_FILE_PATHS_TEMPLATE, 8);
 
     public static final String PERSISTENCE_FILE_MIC_MUTE_CAM_MUTE =
             "SensorPrivacyServiceMockingTest/persisted_file_micMute_camMute.xml";
@@ -81,176 +89,281 @@
     public static final String PERSISTENCE_FILE_MIC_UNMUTE_CAM_UNMUTE =
             "SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camUnmute.xml";
 
-    private Context mContext;
-    @Mock
-    private AppOpsManager mMockedAppOpsManager;
-    @Mock
-    private AppOpsManagerInternal mMockedAppOpsManagerInternal;
-    @Mock
-    private UserManagerInternal mMockedUserManagerInternal;
-    @Mock
-    private ActivityManager mMockedActivityManager;
-    @Mock
-    private ActivityTaskManager mMockedActivityTaskManager;
-    @Mock
-    private TelephonyManager mMockedTelephonyManager;
+    Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    String mDataDir = mContext.getApplicationInfo().dataDir;
+
+    @Before
+    public void setUp() {
+        new File(mDataDir, "sensor_privacy.xml").delete();
+        new File(mDataDir, "sensor_privacy_impl.xml").delete();
+    }
 
     @Test
-    public void testServiceInit() throws IOException {
+    public void testMigration1() throws IOException {
+        PersistedState ps = migrateFromFile(PERSISTENCE_FILE1);
+
+        assertTrue(ps.getState(SOFTWARE, 0, MICROPHONE).isEnabled());
+        assertTrue(ps.getState(SOFTWARE, 0, CAMERA).isEnabled());
+
+        assertNull(ps.getState(SOFTWARE, 10, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 10, CAMERA));
+
+        assertNull(ps.getState(HARDWARE, 0, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 0, CAMERA));
+        assertNull(ps.getState(HARDWARE, 10, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 10, CAMERA));
+    }
+
+    @Test
+    public void testMigration2() throws IOException {
+        PersistedState ps = migrateFromFile(PERSISTENCE_FILE2);
+
+        assertTrue(ps.getState(SOFTWARE, 0, MICROPHONE).isEnabled());
+        assertTrue(ps.getState(SOFTWARE, 0, CAMERA).isEnabled());
+
+        assertTrue(ps.getState(SOFTWARE, 10, MICROPHONE).isEnabled());
+        assertFalse(ps.getState(SOFTWARE, 10, CAMERA).isEnabled());
+
+        assertNull(ps.getState(SOFTWARE, 11, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 11, CAMERA));
+
+        assertTrue(ps.getState(SOFTWARE, 12, MICROPHONE).isEnabled());
+        assertNull(ps.getState(SOFTWARE, 12, CAMERA));
+
+        assertNull(ps.getState(HARDWARE, 0, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 0, CAMERA));
+        assertNull(ps.getState(HARDWARE, 10, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 10, CAMERA));
+        assertNull(ps.getState(HARDWARE, 11, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 11, CAMERA));
+        assertNull(ps.getState(HARDWARE, 12, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 12, CAMERA));
+    }
+
+    @Test
+    public void testMigration3() throws IOException {
+        PersistedState ps = migrateFromFile(PERSISTENCE_FILE3);
+
+        assertFalse(ps.getState(SOFTWARE, 0, MICROPHONE).isEnabled());
+        assertFalse(ps.getState(SOFTWARE, 0, CAMERA).isEnabled());
+
+        assertNull(ps.getState(SOFTWARE, 10, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 10, CAMERA));
+
+        assertNull(ps.getState(HARDWARE, 0, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 0, CAMERA));
+        assertNull(ps.getState(HARDWARE, 10, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 10, CAMERA));
+    }
+
+    @Test
+    public void testMigration4() throws IOException {
+        PersistedState ps = migrateFromFile(PERSISTENCE_FILE4);
+
+        assertTrue(ps.getState(SOFTWARE, 0, MICROPHONE).isEnabled());
+        assertFalse(ps.getState(SOFTWARE, 0, CAMERA).isEnabled());
+
+        assertFalse(ps.getState(SOFTWARE, 10, MICROPHONE).isEnabled());
+        assertNull(ps.getState(SOFTWARE, 10, CAMERA));
+
+        assertNull(ps.getState(HARDWARE, 0, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 0, CAMERA));
+        assertNull(ps.getState(HARDWARE, 10, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 10, CAMERA));
+    }
+
+    @Test
+    public void testMigration5() throws IOException {
+        PersistedState ps = migrateFromFile(PERSISTENCE_FILE5);
+
+        assertNull(ps.getState(SOFTWARE, 0, MICROPHONE));
+        assertFalse(ps.getState(SOFTWARE, 0, CAMERA).isEnabled());
+
+        assertNull(ps.getState(SOFTWARE, 10, MICROPHONE));
+        assertFalse(ps.getState(SOFTWARE, 10, CAMERA).isEnabled());
+
+        assertNull(ps.getState(HARDWARE, 0, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 0, CAMERA));
+        assertNull(ps.getState(HARDWARE, 10, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 10, CAMERA));
+    }
+
+    @Test
+    public void testMigration6() throws IOException {
+        PersistedState ps = migrateFromFile(PERSISTENCE_FILE6);
+
+        assertNull(ps.getState(SOFTWARE, 0, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 0, CAMERA));
+
+        assertNull(ps.getState(SOFTWARE, 10, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 10, CAMERA));
+
+        assertNull(ps.getState(HARDWARE, 0, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 0, CAMERA));
+        assertNull(ps.getState(HARDWARE, 10, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 10, CAMERA));
+    }
+
+    private PersistedState migrateFromFile(String fileName) throws IOException {
         MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
                 .initMocks(this)
                 .strictness(Strictness.WARN)
                 .spyStatic(LocalServices.class)
                 .spyStatic(Environment.class)
                 .startMocking();
-
         try {
-            mContext = InstrumentationRegistry.getInstrumentation().getContext();
-            spyOn(mContext);
+            doReturn(new File(mDataDir)).when(() -> Environment.getDataSystemDirectory());
 
-            doReturn(mMockedAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
-            doReturn(mMockedUserManagerInternal)
-                    .when(() -> LocalServices.getService(UserManagerInternal.class));
-            doReturn(mMockedActivityManager).when(mContext).getSystemService(ActivityManager.class);
-            doReturn(mMockedActivityTaskManager)
-                    .when(mContext).getSystemService(ActivityTaskManager.class);
-            doReturn(mMockedTelephonyManager).when(mContext).getSystemService(
-                    TelephonyManager.class);
+            UserManagerInternal umi = mock(UserManagerInternal.class);
+            doReturn(umi).when(() -> LocalServices.getService(UserManagerInternal.class));
+            doReturn(new int[] {0}).when(umi).getUserIds();
 
-            String dataDir = mContext.getApplicationInfo().dataDir;
-            doReturn(new File(dataDir)).when(() -> Environment.getDataSystemDirectory());
+            Files.copy(
+                    mContext.getAssets().open(fileName),
+                    new File(mDataDir, "sensor_privacy.xml").toPath(),
+                    StandardCopyOption.REPLACE_EXISTING);
 
-            File onDeviceFile = new File(dataDir, "sensor_privacy.xml");
-            onDeviceFile.delete();
-
-            // Try all files with one known user
-            doReturn(new int[]{0}).when(mMockedUserManagerInternal).getUserIds();
-            doReturn(ExtendedMockito.mock(UserInfo.class)).when(mMockedUserManagerInternal)
-                    .getUserInfo(0);
-            initServiceWithPersistenceFile(onDeviceFile, null);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE1);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE2);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE3);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE4);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE5);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE6);
-
-            // Try all files with two known users
-            doReturn(new int[]{0, 10}).when(mMockedUserManagerInternal).getUserIds();
-            doReturn(ExtendedMockito.mock(UserInfo.class)).when(mMockedUserManagerInternal)
-                    .getUserInfo(0);
-            doReturn(ExtendedMockito.mock(UserInfo.class)).when(mMockedUserManagerInternal)
-                    .getUserInfo(10);
-            initServiceWithPersistenceFile(onDeviceFile, null);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE1);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE2);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE3);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE4);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE5);
-            initServiceWithPersistenceFile(onDeviceFile, PERSISTENCE_FILE6);
-
+            return PersistedState.fromFile("sensor_privacy_impl.xml");
         } finally {
             mockitoSession.finishMocking();
         }
     }
 
     @Test
-    public void testServiceInit_AppOpsRestricted_micMute_camMute() throws IOException {
-        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_MUTE_CAM_MUTE, true, true);
+    public void testPersistence1Version2() throws IOException {
+        PersistedState ps = getPersistedStateV2(PERSISTENCE_FILE7);
+
+        assertEquals(1, ps.getState(SOFTWARE, 0, MICROPHONE).getState());
+        assertEquals(123L, ps.getState(SOFTWARE, 0, MICROPHONE).getLastChange());
+        assertEquals(2, ps.getState(SOFTWARE, 0, CAMERA).getState());
+        assertEquals(123L, ps.getState(SOFTWARE, 0, CAMERA).getLastChange());
+
+        assertNull(ps.getState(HARDWARE, 0, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 0, CAMERA));
+        assertNull(ps.getState(SOFTWARE, 10, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 10, CAMERA));
     }
 
     @Test
-    public void testServiceInit_AppOpsRestricted_micMute_camUnmute() throws IOException {
-        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_MUTE_CAM_UNMUTE, true, false);
+    public void testPersistence2Version2() throws IOException {
+        PersistedState ps = getPersistedStateV2(PERSISTENCE_FILE8);
+
+        assertEquals(1, ps.getState(HARDWARE, 0, MICROPHONE).getState());
+        assertEquals(1234L, ps.getState(HARDWARE, 0, MICROPHONE).getLastChange());
+        assertEquals(2, ps.getState(HARDWARE, 0, CAMERA).getState());
+        assertEquals(1234L, ps.getState(HARDWARE, 0, CAMERA).getLastChange());
+
+        assertNull(ps.getState(SOFTWARE, 0, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 0, CAMERA));
+        assertNull(ps.getState(SOFTWARE, 10, MICROPHONE));
+        assertNull(ps.getState(SOFTWARE, 10, CAMERA));
+        assertNull(ps.getState(HARDWARE, 10, MICROPHONE));
+        assertNull(ps.getState(HARDWARE, 10, CAMERA));
     }
 
-    @Test
-    public void testServiceInit_AppOpsRestricted_micUnmute_camMute() throws IOException {
-        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_UNMUTE_CAM_MUTE, false, true);
-    }
-
-    @Test
-    public void testServiceInit_AppOpsRestricted_micUnmute_camUnmute() throws IOException {
-        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_UNMUTE_CAM_UNMUTE, false, false);
-    }
-
-    private void testServiceInit_AppOpsRestricted(String persistenceFileMicMuteCamMute,
-            boolean expectedMicState, boolean expectedCamState)
-            throws IOException {
+    private PersistedState getPersistedStateV2(String version2FilePath) throws IOException {
         MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
                 .initMocks(this)
                 .strictness(Strictness.WARN)
                 .spyStatic(LocalServices.class)
                 .spyStatic(Environment.class)
                 .startMocking();
-
         try {
-            mContext = InstrumentationRegistry.getInstrumentation().getContext();
-            spyOn(mContext);
+            doReturn(new File(mDataDir)).when(() -> Environment.getDataSystemDirectory());
+            Files.copy(
+                    mContext.getAssets().open(version2FilePath),
+                    new File(mDataDir, "sensor_privacy_impl.xml").toPath(),
+                    StandardCopyOption.REPLACE_EXISTING);
 
-            doReturn(mMockedAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
-            doReturn(mMockedAppOpsManagerInternal)
-                    .when(() -> LocalServices.getService(AppOpsManagerInternal.class));
-            doReturn(mMockedUserManagerInternal)
-                    .when(() -> LocalServices.getService(UserManagerInternal.class));
-            doReturn(mMockedActivityManager).when(mContext).getSystemService(ActivityManager.class);
-            doReturn(mMockedActivityTaskManager)
-                    .when(mContext).getSystemService(ActivityTaskManager.class);
-            doReturn(mMockedTelephonyManager).when(mContext).getSystemService(
-                    TelephonyManager.class);
-
-            String dataDir = mContext.getApplicationInfo().dataDir;
-            doReturn(new File(dataDir)).when(() -> Environment.getDataSystemDirectory());
-
-            File onDeviceFile = new File(dataDir, "sensor_privacy.xml");
-            onDeviceFile.delete();
-
-            doReturn(new int[]{0}).when(mMockedUserManagerInternal).getUserIds();
-            doReturn(ExtendedMockito.mock(UserInfo.class)).when(mMockedUserManagerInternal)
-                    .getUserInfo(0);
-
-            CompletableFuture<Boolean> micState = new CompletableFuture<>();
-            CompletableFuture<Boolean> camState = new CompletableFuture<>();
-            doAnswer(invocation -> {
-                int code = invocation.getArgument(0);
-                boolean restricted = invocation.getArgument(1);
-                if (code == AppOpsManager.OP_RECORD_AUDIO) {
-                    micState.complete(restricted);
-                } else if (code == AppOpsManager.OP_CAMERA) {
-                    camState.complete(restricted);
-                }
-                return null;
-            }).when(mMockedAppOpsManagerInternal).setGlobalRestriction(anyInt(), anyBoolean(),
-                    any());
-
-            initServiceWithPersistenceFile(onDeviceFile, persistenceFileMicMuteCamMute, 0);
-
-            Assert.assertTrue(micState.join() == expectedMicState);
-            Assert.assertTrue(camState.join() == expectedCamState);
-
+            return PersistedState.fromFile("sensor_privacy_impl.xml");
         } finally {
             mockitoSession.finishMocking();
         }
     }
 
-    private void initServiceWithPersistenceFile(File onDeviceFile,
-            String persistenceFilePath) throws IOException {
-        initServiceWithPersistenceFile(onDeviceFile, persistenceFilePath, -1);
+    @Test
+    public void testGetDefaultState() {
+        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.WARN)
+                .spyStatic(PersistedState.class)
+                .startMocking();
+        try {
+            PersistedState persistedState = mock(PersistedState.class);
+            doReturn(persistedState).when(() -> PersistedState.fromFile(any()));
+            doReturn(null).when(persistedState).getState(anyInt(), anyInt(), anyInt());
+
+            SensorPrivacyStateController sensorPrivacyStateController =
+                    getSensorPrivacyStateControllerImpl();
+
+            SensorState micState = sensorPrivacyStateController.getState(SOFTWARE, 0, MICROPHONE);
+            SensorState camState = sensorPrivacyStateController.getState(SOFTWARE, 0, CAMERA);
+
+            assertEquals(SensorPrivacyManager.StateTypes.DISABLED, micState.getState());
+            assertEquals(SensorPrivacyManager.StateTypes.DISABLED, camState.getState());
+            verify(persistedState, times(1)).getState(SOFTWARE, 0, MICROPHONE);
+            verify(persistedState, times(1)).getState(SOFTWARE, 0, CAMERA);
+        } finally {
+            mockitoSession.finishMocking();
+        }
     }
 
-    private void initServiceWithPersistenceFile(File onDeviceFile,
-            String persistenceFilePath, int startingUserId) throws IOException {
-        if (persistenceFilePath != null) {
-            Files.copy(mContext.getAssets().open(persistenceFilePath),
-                    onDeviceFile.toPath());
+    @Test
+    public void testGetSetState() {
+        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.WARN)
+                .spyStatic(PersistedState.class)
+                .startMocking();
+        try {
+            PersistedState persistedState = mock(PersistedState.class);
+            SensorState sensorState = mock(SensorState.class);
+            doReturn(persistedState).when(() -> PersistedState.fromFile(any()));
+            doReturn(sensorState).when(persistedState).getState(SOFTWARE, 0, MICROPHONE);
+            doReturn(SensorPrivacyManager.StateTypes.ENABLED).when(sensorState).getState();
+            doReturn(0L).when(sensorState).getLastChange();
+
+            SensorPrivacyStateController sensorPrivacyStateController =
+                    getSensorPrivacyStateControllerImpl();
+
+            SensorState micState = sensorPrivacyStateController.getState(SOFTWARE, 0, MICROPHONE);
+
+            assertEquals(SensorPrivacyManager.StateTypes.ENABLED, micState.getState());
+            assertEquals(0L, micState.getLastChange());
+        } finally {
+            mockitoSession.finishMocking();
         }
-        SensorPrivacyService service = new SensorPrivacyService(mContext);
-        if (startingUserId != -1) {
-            SystemService.TargetUser mockedTargetUser =
-                    ExtendedMockito.mock(SystemService.TargetUser.class);
-            doReturn(startingUserId).when(mockedTargetUser).getUserIdentifier();
-            service.onUserStarting(mockedTargetUser);
+    }
+
+    @Test
+    public void testSetState() {
+        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.WARN)
+                .spyStatic(PersistedState.class)
+                .startMocking();
+        try {
+            PersistedState persistedState = mock(PersistedState.class);
+            doReturn(persistedState).when(() -> PersistedState.fromFile(any()));
+
+            SensorPrivacyStateController sensorPrivacyStateController =
+                    getSensorPrivacyStateControllerImpl();
+
+            sensorPrivacyStateController.setState(SOFTWARE, 0, MICROPHONE, true,
+                    mock(Handler.class), changed -> {});
+
+            ArgumentCaptor<SensorState> captor = ArgumentCaptor.forClass(SensorState.class);
+
+            verify(persistedState, times(1)).setState(eq(SOFTWARE), eq(0), eq(MICROPHONE),
+                    captor.capture());
+            assertEquals(SensorPrivacyManager.StateTypes.ENABLED, captor.getValue().getState());
+        } finally {
+            mockitoSession.finishMocking();
         }
-        onDeviceFile.delete();
+    }
+
+    private SensorPrivacyStateController getSensorPrivacyStateControllerImpl() {
+        SensorPrivacyStateControllerImpl.getInstance().resetForTestingImpl();
+        return SensorPrivacyStateControllerImpl.getInstance();
     }
 }