From 0cefec2d4f766082a653bd45bfb1e64b2ed8ffb1 Mon Sep 17 00:00:00 2001 From: Evan Severson Date: Wed, 6 Oct 2021 08:28:51 -0700 Subject: [PATCH] Refactor SensorPrivacyService to separate state from service This refactoring will let us have alternative implementations for the state of microphone/camera. It also seperates the storage of the other feature that is known as the "Sensor mute" so that the reuse of this service doesn't get too convoluted. Also extract the persitable state from the state implementation so that alternative implementations can resuse the persistence code. Some smaller changes: * Start the sensor use dailog as user 0 always since system UI requires user 0 * Use SystemClock.elapsedRealTime() instead of network time since it's guaranteed to my monotonic. This will also change that intervals used in telemetry aren't going to measure time the device is powered off. Ignore-AOSP-First: New OWNERS file Test: atest CtsSensorPrivacyTestCases SensorPrivacyServiceMockingTest Bug: 200649586 Change-Id: I4c23c2509d7d1bdf49360fc9bbadb79ccd451dcc --- .../hardware/SensorPrivacyManager.java | 71 +- .../android/hardware/sensorprivacy.proto | 52 +- services/core/java/com/android/server/OWNERS | 3 - .../AllSensorStateController.java | 155 +++++ .../com/android/server/sensorprivacy/OWNERS | 1 + .../server/sensorprivacy/PersistedState.java | 494 ++++++++++++++ .../SensorPrivacyService.java | 613 ++++-------------- .../SensorPrivacyStateController.java | 158 +++++ .../SensorPrivacyStateControllerImpl.java | 142 ++++ .../server/sensorprivacy/SensorState.java | 79 +++ .../java/com/android/server/SystemServer.java | 1 + .../persisted_file1.xml | 4 +- .../persisted_file7.xml | 5 + .../persisted_file8.xml | 5 + .../SensorPrivacyServiceMockingTest.java | 403 +++++++----- 15 files changed, 1532 insertions(+), 654 deletions(-) create mode 100644 services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java create mode 100644 services/core/java/com/android/server/sensorprivacy/OWNERS create mode 100644 services/core/java/com/android/server/sensorprivacy/PersistedState.java rename services/core/java/com/android/server/{ => sensorprivacy}/SensorPrivacyService.java (71%) create mode 100644 services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java create mode 100644 services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java create mode 100644 services/core/java/com/android/server/sensorprivacy/SensorState.java create mode 100644 services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file7.xml create mode 100644 services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file8.xml 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(); } }