diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java index 7074a2c8a60d7..79153d77a85b9 100644 --- a/core/java/android/hardware/SensorPrivacyManager.java +++ b/core/java/android/hardware/SensorPrivacyManager.java @@ -31,6 +31,7 @@ import android.os.RemoteException; 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 @@ public final class SensorPrivacyManager { private final SparseArray 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 @@ public final class SensorPrivacyManager { /** * 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} @@ -160,6 +161,68 @@ public final class SensorPrivacyManager { } + /** + * 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 @@ -507,7 +570,6 @@ public final class SensorPrivacyManager { /** * 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 @@ public final class SensorPrivacyManager { /** * 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 81d849e7c6e87..97870a16da2b8 100644 --- a/core/proto/android/hardware/sensorprivacy.proto +++ b/core/proto/android/hardware/sensorprivacy.proto @@ -22,6 +22,13 @@ option java_outer_classname = "SensorPrivacyServiceProto"; 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 @@ message SensorPrivacyServiceDumpProto { // Per user settings for sensor privacy repeated SensorPrivacyUserProto user = 3; + + // Implementation + optional string storage_implementation = 4; } message SensorPrivacyUserProto { @@ -43,16 +53,47 @@ message SensorPrivacyUserProto { // 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 @@ message SensorPrivacyIndividualEnabledSensorProto { // 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 71b463a4db8c5..7e89fbced8d01 100644 --- a/services/core/java/com/android/server/OWNERS +++ b/services/core/java/com/android/server/OWNERS @@ -10,9 +10,6 @@ per-file ZramWriteback.java = minchan@google.com, rajekumar@google.com # 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 0000000000000..f797f09d1afbe --- /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 0000000000000..15e3f7af718c9 --- /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 0000000000000..ce9fff599d435 --- /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 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 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 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. + SparseArray>> 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> 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> 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 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 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> 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 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 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> individualEnabled = + version1.mIndividualEnabled; + int numUsers = individualEnabled.size(); + for (int i = 0; i < numUsers; i++) { + int userId = individualEnabled.keyAt(i); + SparseArray 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 7a4d11c1cc7db..e9b5f1156d39e 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.QS_TILE; 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.SensorPrivacyManager; 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.SystemClock; 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.messages.nano.SystemMessageProto.SystemMessage; 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 @@ public final class SensorPrivacyService extends SystemService { 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 final class SensorPrivacyService extends SystemService { public SensorPrivacyService(Context context) { super(context); + mContext = context; mAppOpsManager = context.getSystemService(AppOpsManager.class); mAppOpsManagerInternal = getLocalService(AppOpsManagerInternal.class); @@ -247,12 +213,8 @@ public final class SensorPrivacyService extends SystemService { 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> mIndividualEnabled = new SparseArray<>(); + + private SensorPrivacyStateController mSensorPrivacyStateController; /** * Packages for which not to show sensor use reminders. @@ -266,34 +228,6 @@ public final class SensorPrivacyService extends SystemService { private final ArrayMap> 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 @@ public final class SensorPrivacyService extends SystemService { 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 @@ public final class SensorPrivacyService extends SystemService { } }, 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 @@ public final class SensorPrivacyService extends SystemService { } } - 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 @@ public final class SensorPrivacyService extends SystemService { 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 @@ public final class SensorPrivacyService extends SystemService { 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 @@ public final class SensorPrivacyService extends SystemService { 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 @@ public final class SensorPrivacyService extends SystemService { @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 @@ public final class SensorPrivacyService extends SystemService { private void setIndividualSensorPrivacyUnchecked(int userId, int source, int sensor, boolean enable) { - synchronized (mLock) { - SparseArray 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 @@ public final class SensorPrivacyService extends SystemService { } 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 @@ public final class SensorPrivacyService extends SystemService { @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 @@ public final class SensorPrivacyService extends SystemService { if (userId == UserHandle.USER_CURRENT) { userId = mCurrentUser; } - synchronized (mLock) { - return isIndividualSensorPrivacyEnabledLocked(userId, sensor); - } - } - - private boolean isIndividualSensorPrivacyEnabledLocked(int userId, int sensor) { - SparseArray 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 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 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> 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 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 individualEnabled = - (SparseArray) map.get(VER0_INDIVIDUAL_ENABLED); - - final SparseBooleanArray perUserEnabled = new SparseBooleanArray(); - final SparseArray> 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 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>) 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 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 @@ public final class SensorPrivacyService extends SystemService { } 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,57 +1090,20 @@ public final class SensorPrivacyService extends SystemService { 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); } } - /** - * 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 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}. * @@ -1504,25 +1162,6 @@ public final class SensorPrivacyService extends SystemService { 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 individualEnabled = - mIndividualEnabled.get(userId); - if (individualEnabled != null) { - individualEnabled.delete(sensor); - } - persistSensorPrivacyState(); - } - } - break; default: return handleDefaultCommands(cmd); } @@ -1545,9 +1184,6 @@ public final class SensorPrivacyService extends SystemService { 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 @@ public final class SensorPrivacyService extends SystemService { 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 final class SensorPrivacyService extends SystemService { } public void handleSensorPrivacyChanged(int userId, int sensor, boolean enabled) { + // TODO handle hardware mSensorPrivacyManagerInternal.dispatch(userId, sensor, enabled); SparseArray> listenersForUser = mIndividualSensorListeners.get(userId); @@ -1972,11 +1593,7 @@ public final class SensorPrivacyService extends SystemService { } } - 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 0000000000000..96949589447c9 --- /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 0000000000000..d1ea8e98fe2fa --- /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 0000000000000..b92e2c8a8d748 --- /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 fe5c358bc5c1e..4db2b9cc4a62b 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -181,6 +181,7 @@ import com.android.server.security.AttestationVerificationManagerService; 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 a4de08a854873..4e0bb361105e4 100644 --- a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file1.xml +++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file1.xml @@ -1,7 +1,7 @@ - - + + 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 0000000000000..2d192dbe88828 --- /dev/null +++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file7.xml @@ -0,0 +1,5 @@ + + + + + \ 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 0000000000000..7bb38b4e79f8d --- /dev/null +++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file8.xml @@ -0,0 +1,5 @@ + + + + + \ 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 38f01b5acc0c8..64e86133919ec 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 @@ public class SensorPrivacyServiceMockingTest { 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 class SensorPrivacyServiceMockingTest { 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()); - - 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); + Files.copy( + mContext.getAssets().open(fileName), + new File(mDataDir, "sensor_privacy.xml").toPath(), + StandardCopyOption.REPLACE_EXISTING); + 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(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 micState = new CompletableFuture<>(); - CompletableFuture 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); + doReturn(new File(mDataDir)).when(() -> Environment.getDataSystemDirectory()); + Files.copy( + mContext.getAssets().open(version2FilePath), + new File(mDataDir, "sensor_privacy_impl.xml").toPath(), + StandardCopyOption.REPLACE_EXISTING); + 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 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(); } }