Merge "Refactor SensorPrivacyService to separate state from service"

This commit is contained in:
Evan Severson
2022-01-27 22:10:37 +00:00
committed by Android (Google) Code Review
15 changed files with 1532 additions and 654 deletions

View File

@@ -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<Boolean> mToggleSupportCache = new SparseArray<>();
/**
* Individual sensors not listed in {@link Sensors}
* Sensor constants which are used in {@link SensorPrivacyManager}
*/
public static class Sensors {
@@ -84,12 +85,12 @@ 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
*

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -0,0 +1 @@
include platform/frameworks/native:/libs/sensorprivacy/OWNERS

View File

@@ -0,0 +1,494 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.sensorprivacy;
import android.hardware.SensorPrivacyManager;
import android.os.Environment;
import android.os.UserHandle;
import android.service.SensorPrivacyIndividualEnabledSensorProto;
import android.service.SensorPrivacySensorProto;
import android.service.SensorPrivacyServiceDumpProto;
import android.service.SensorPrivacyUserProto;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;
import com.android.internal.util.XmlUtils;
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.internal.util.function.QuadConsumer;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.IoThread;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Class for managing persisted state. Synchronization must be handled by the caller.
*/
class PersistedState {
private static final String LOG_TAG = PersistedState.class.getSimpleName();
/** Version number indicating compatibility parsing the persisted file */
private static final int CURRENT_PERSISTENCE_VERSION = 2;
/** Version number indicating the persisted data needs upgraded to match new internal data
* structures and features */
private static final int CURRENT_VERSION = 2;
private static final String XML_TAG_SENSOR_PRIVACY = "sensor-privacy";
private static final String XML_TAG_SENSOR_STATE = "sensor-state";
private static final String XML_ATTRIBUTE_PERSISTENCE_VERSION = "persistence-version";
private static final String XML_ATTRIBUTE_VERSION = "version";
private static final String XML_ATTRIBUTE_TOGGLE_TYPE = "toggle-type";
private static final String XML_ATTRIBUTE_USER_ID = "user-id";
private static final String XML_ATTRIBUTE_SENSOR = "sensor";
private static final String XML_ATTRIBUTE_STATE_TYPE = "state-type";
private static final String XML_ATTRIBUTE_LAST_CHANGE = "last-change";
private final AtomicFile mAtomicFile;
private ArrayMap<TypeUserSensor, SensorState> mStates = new ArrayMap<>();
static PersistedState fromFile(String fileName) {
return new PersistedState(fileName);
}
private PersistedState(String fileName) {
mAtomicFile = new AtomicFile(new File(Environment.getDataSystemDirectory(), fileName));
readState();
}
private void readState() {
AtomicFile file = mAtomicFile;
if (!file.exists()) {
AtomicFile fileToMigrateFrom =
new AtomicFile(new File(Environment.getDataSystemDirectory(),
"sensor_privacy.xml"));
if (fileToMigrateFrom.exists()) {
// Sample the start tag to determine if migration is needed
try (FileInputStream inputStream = fileToMigrateFrom.openRead()) {
TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
XmlUtils.beginDocument(parser, XML_TAG_SENSOR_PRIVACY);
file = fileToMigrateFrom;
} catch (IOException e) {
Log.e(LOG_TAG, "Caught an exception reading the state from storage: ", e);
// Delete the file to prevent the same error on subsequent calls and assume
// sensor privacy is not enabled.
fileToMigrateFrom.delete();
} catch (XmlPullParserException e) {
// No migration needed
}
}
}
Object nonupgradedState = null;
if (file.exists()) {
try (FileInputStream inputStream = file.openRead()) {
TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
XmlUtils.beginDocument(parser, XML_TAG_SENSOR_PRIVACY);
final int persistenceVersion = parser.getAttributeInt(null,
XML_ATTRIBUTE_PERSISTENCE_VERSION, 0);
// Use inline string literals for xml tags/attrs when parsing old versions since
// these should never be changed even with refactorings.
if (persistenceVersion == 0) {
int version = 0;
PVersion0 version0 = new PVersion0(version);
nonupgradedState = version0;
readPVersion0(parser, version0);
} else if (persistenceVersion == 1) {
int version = parser.getAttributeInt(null,
"version", 1);
PVersion1 version1 = new PVersion1(version);
nonupgradedState = version1;
readPVersion1(parser, version1);
} else if (persistenceVersion == CURRENT_PERSISTENCE_VERSION) {
int version = parser.getAttributeInt(null,
XML_ATTRIBUTE_VERSION, 2);
PVersion2 version2 = new PVersion2(version);
nonupgradedState = version2;
readPVersion2(parser, version2);
} else {
Log.e(LOG_TAG, "Unknown persistence version: " + persistenceVersion
+ ". Deleting.",
new RuntimeException());
file.delete();
nonupgradedState = null;
}
} catch (IOException | XmlPullParserException | RuntimeException e) {
Log.e(LOG_TAG, "Caught an exception reading the state from storage: ", e);
// Delete the file to prevent the same error on subsequent calls and assume
// sensor privacy is not enabled.
file.delete();
nonupgradedState = null;
}
}
if (nonupgradedState == null) {
// New file, default state for current version goes here.
nonupgradedState = new PVersion2(2);
}
if (nonupgradedState instanceof PVersion0) {
nonupgradedState = PVersion1.fromPVersion0((PVersion0) nonupgradedState);
}
if (nonupgradedState instanceof PVersion1) {
nonupgradedState = PVersion2.fromPVersion1((PVersion1) nonupgradedState);
}
if (nonupgradedState instanceof PVersion2) {
PVersion2 upgradedState = (PVersion2) nonupgradedState;
mStates = upgradedState.mStates;
} else {
Log.e(LOG_TAG, "State not successfully upgraded.");
mStates = new ArrayMap<>();
}
}
private static void readPVersion0(TypedXmlPullParser parser, PVersion0 version0)
throws XmlPullParserException, IOException {
XmlUtils.nextElement(parser);
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
if ("individual-sensor-privacy".equals(parser.getName())) {
int sensor = XmlUtils.readIntAttribute(parser, "sensor");
boolean indEnabled = XmlUtils.readBooleanAttribute(parser,
"enabled");
version0.addState(sensor, indEnabled);
XmlUtils.skipCurrentTag(parser);
} else {
XmlUtils.nextElement(parser);
}
}
}
private static void readPVersion1(TypedXmlPullParser parser, PVersion1 version1)
throws XmlPullParserException, IOException {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
XmlUtils.nextElement(parser);
if ("user".equals(parser.getName())) {
int currentUserId = parser.getAttributeInt(null, "id");
int depth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, depth)) {
if ("individual-sensor-privacy".equals(parser.getName())) {
int sensor = parser.getAttributeInt(null, "sensor");
boolean isEnabled = parser.getAttributeBoolean(null,
"enabled");
version1.addState(currentUserId, sensor, isEnabled);
}
}
}
}
}
private static void readPVersion2(TypedXmlPullParser parser, PVersion2 version2)
throws XmlPullParserException, IOException {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
XmlUtils.nextElement(parser);
if (XML_TAG_SENSOR_STATE.equals(parser.getName())) {
int toggleType = parser.getAttributeInt(null, XML_ATTRIBUTE_TOGGLE_TYPE);
int userId = parser.getAttributeInt(null, XML_ATTRIBUTE_USER_ID);
int sensor = parser.getAttributeInt(null, XML_ATTRIBUTE_SENSOR);
int state = parser.getAttributeInt(null, XML_ATTRIBUTE_STATE_TYPE);
long lastChange = parser.getAttributeLong(null, XML_ATTRIBUTE_LAST_CHANGE);
version2.addState(toggleType, userId, sensor, state, lastChange);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
}
public SensorState getState(int toggleType, int userId, int sensor) {
return mStates.get(new TypeUserSensor(toggleType, userId, sensor));
}
public SensorState setState(int toggleType, int userId, int sensor, SensorState sensorState) {
return mStates.put(new TypeUserSensor(toggleType, userId, sensor), sensorState);
}
private static class TypeUserSensor {
int mType;
int mUserId;
int mSensor;
TypeUserSensor(int type, int userId, int sensor) {
mType = type;
mUserId = userId;
mSensor = sensor;
}
TypeUserSensor(TypeUserSensor typeUserSensor) {
this(typeUserSensor.mType, typeUserSensor.mUserId, typeUserSensor.mSensor);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TypeUserSensor)) return false;
TypeUserSensor that = (TypeUserSensor) o;
return mType == that.mType && mUserId == that.mUserId && mSensor == that.mSensor;
}
@Override
public int hashCode() {
return 31 * (31 * mType + mUserId) + mSensor;
}
}
void schedulePersist() {
int numStates = mStates.size();
ArrayMap<TypeUserSensor, SensorState> statesCopy = new ArrayMap<>();
for (int i = 0; i < numStates; i++) {
statesCopy.put(new TypeUserSensor(mStates.keyAt(i)),
new SensorState(mStates.valueAt(i)));
}
IoThread.getHandler().sendMessage(
PooledLambda.obtainMessage(PersistedState::persist, this, statesCopy));
}
private void persist(ArrayMap<TypeUserSensor, SensorState> states) {
FileOutputStream outputStream = null;
try {
outputStream = mAtomicFile.startWrite();
TypedXmlSerializer serializer = Xml.resolveSerializer(outputStream);
serializer.startDocument(null, true);
serializer.startTag(null, XML_TAG_SENSOR_PRIVACY);
serializer.attributeInt(null, XML_ATTRIBUTE_PERSISTENCE_VERSION,
CURRENT_PERSISTENCE_VERSION);
serializer.attributeInt(null, XML_ATTRIBUTE_VERSION, CURRENT_VERSION);
for (int i = 0; i < states.size(); i++) {
TypeUserSensor userSensor = states.keyAt(i);
SensorState sensorState = states.valueAt(i);
serializer.startTag(null, XML_TAG_SENSOR_STATE);
serializer.attributeInt(null, XML_ATTRIBUTE_TOGGLE_TYPE,
userSensor.mType);
serializer.attributeInt(null, XML_ATTRIBUTE_USER_ID,
userSensor.mUserId);
serializer.attributeInt(null, XML_ATTRIBUTE_SENSOR,
userSensor.mSensor);
serializer.attributeInt(null, XML_ATTRIBUTE_STATE_TYPE,
sensorState.getState());
serializer.attributeLong(null, XML_ATTRIBUTE_LAST_CHANGE,
sensorState.getLastChange());
serializer.endTag(null, XML_TAG_SENSOR_STATE);
}
serializer.endTag(null, XML_TAG_SENSOR_PRIVACY);
serializer.endDocument();
mAtomicFile.finishWrite(outputStream);
} catch (IOException e) {
Log.e(LOG_TAG, "Caught an exception persisting the sensor privacy state: ", e);
mAtomicFile.failWrite(outputStream);
}
}
void dump(DualDumpOutputStream dumpStream) {
// Collect per user, then per sensor. <toggle type, state>
SparseArray<SparseArray<Pair<Integer, SensorState>>> statesMatrix = new SparseArray<>();
int numStates = mStates.size();
for (int i = 0; i < numStates; i++) {
int toggleType = mStates.keyAt(i).mType;
int userId = mStates.keyAt(i).mUserId;
int sensor = mStates.keyAt(i).mSensor;
SparseArray<Pair<Integer, SensorState>> userStates = statesMatrix.get(userId);
if (userStates == null) {
userStates = new SparseArray<>();
statesMatrix.put(userId, userStates);
}
userStates.put(sensor, new Pair<>(toggleType, mStates.valueAt(i)));
}
dumpStream.write("storage_implementation",
SensorPrivacyServiceDumpProto.STORAGE_IMPLEMENTATION,
SensorPrivacyStateControllerImpl.class.getName());
int numUsers = statesMatrix.size();
for (int i = 0; i < numUsers; i++) {
int userId = statesMatrix.keyAt(i);
long userToken = dumpStream.start("users", SensorPrivacyServiceDumpProto.USER);
dumpStream.write("user_id", SensorPrivacyUserProto.USER_ID, userId);
SparseArray<Pair<Integer, SensorState>> userStates = statesMatrix.valueAt(i);
int numSensors = userStates.size();
for (int j = 0; j < numSensors; j++) {
int sensor = userStates.keyAt(j);
int toggleType = userStates.valueAt(j).first;
SensorState sensorState = userStates.valueAt(j).second;
long sensorToken = dumpStream.start("sensors", SensorPrivacyUserProto.SENSORS);
dumpStream.write("sensor", SensorPrivacySensorProto.SENSOR, sensor);
long toggleToken = dumpStream.start("toggles", SensorPrivacySensorProto.TOGGLES);
dumpStream.write("toggle_type",
SensorPrivacyIndividualEnabledSensorProto.TOGGLE_TYPE,
toggleType);
dumpStream.write("state_type",
SensorPrivacyIndividualEnabledSensorProto.STATE_TYPE,
sensorState.getState());
dumpStream.write("last_change",
SensorPrivacyIndividualEnabledSensorProto.LAST_CHANGE,
sensorState.getLastChange());
dumpStream.end(toggleToken);
dumpStream.end(sensorToken);
}
dumpStream.end(userToken);
}
}
void forEachKnownState(QuadConsumer<Integer, Integer, Integer, SensorState> consumer) {
int numStates = mStates.size();
for (int i = 0; i < numStates; i++) {
TypeUserSensor tus = mStates.keyAt(i);
SensorState sensorState = mStates.valueAt(i);
consumer.accept(tus.mType, tus.mUserId, tus.mSensor, sensorState);
}
}
// Structure for persistence version 0
private static class PVersion0 {
private SparseArray<SensorState> mIndividualEnabled = new SparseArray<>();
private PVersion0(int version) {
if (version != 0) {
throw new RuntimeException("Only version 0 supported");
}
}
private void addState(int sensor, boolean enabled) {
mIndividualEnabled.put(sensor, new SensorState(enabled));
}
private void upgrade() {
// No op, only version 0 is supported
}
}
// Structure for persistence version 1
private static class PVersion1 {
private SparseArray<SparseArray<SensorState>> mIndividualEnabled = new SparseArray<>();
private PVersion1(int version) {
if (version != 1) {
throw new RuntimeException("Only version 1 supported");
}
}
private static PVersion1 fromPVersion0(PVersion0 version0) {
version0.upgrade();
PVersion1 result = new PVersion1(1);
int[] users = {UserHandle.USER_SYSTEM};
try {
users = LocalServices.getService(UserManagerInternal.class).getUserIds();
} catch (Exception e) {
Log.e(LOG_TAG, "Unable to get users.", e);
}
// Copy global state to each user
for (int i = 0; i < users.length; i++) {
int userId = users[i];
for (int j = 0; j < version0.mIndividualEnabled.size(); j++) {
final int sensor = version0.mIndividualEnabled.keyAt(j);
final SensorState sensorState = version0.mIndividualEnabled.valueAt(j);
result.addState(userId, sensor, sensorState.isEnabled());
}
}
return result;
}
private void addState(int userId, int sensor, boolean enabled) {
SparseArray<SensorState> userIndividualSensorEnabled =
mIndividualEnabled.get(userId, new SparseArray<>());
mIndividualEnabled.put(userId, userIndividualSensorEnabled);
userIndividualSensorEnabled
.put(sensor, new SensorState(enabled));
}
private void upgrade() {
// No op, only version 1 is supported
}
}
// Structure for persistence version 2
private static class PVersion2 {
private ArrayMap<TypeUserSensor, SensorState> mStates = new ArrayMap<>();
private PVersion2(int version) {
if (version != 2) {
throw new RuntimeException("Only version 2 supported");
}
}
private static PVersion2 fromPVersion1(PVersion1 version1) {
version1.upgrade();
PVersion2 result = new PVersion2(2);
SparseArray<SparseArray<SensorState>> individualEnabled =
version1.mIndividualEnabled;
int numUsers = individualEnabled.size();
for (int i = 0; i < numUsers; i++) {
int userId = individualEnabled.keyAt(i);
SparseArray<SensorState> userIndividualEnabled = individualEnabled.valueAt(i);
int numSensors = userIndividualEnabled.size();
for (int j = 0; j < numSensors; j++) {
int sensor = userIndividualEnabled.keyAt(j);
SensorState sensorState = userIndividualEnabled.valueAt(j);
result.addState(SensorPrivacyManager.ToggleTypes.SOFTWARE,
userId, sensor, sensorState.getState(), sensorState.getLastChange());
}
}
return result;
}
private void addState(int toggleType, int userId, int sensor, int state,
long lastChange) {
mStates.put(new TypeUserSensor(toggleType, userId, sensor),
new SensorState(state, lastChange));
}
}
public void resetForTesting() {
mStates = new ArrayMap<>();
}
}

View File

@@ -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<SparseArray<SensorState>> 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<SensorUseReminderDialogInfo, ArraySet<Integer>>
mQueuedSensorUseReminderDialogs = new ArrayMap<>();
private class SensorState {
private boolean mEnabled;
private long mLastChange;
SensorState(boolean enabled) {
mEnabled = enabled;
mLastChange = getCurrentTimeMillis();
}
SensorState(boolean enabled, long lastChange) {
mEnabled = enabled;
if (lastChange < 0) {
mLastChange = getCurrentTimeMillis();
} else {
mLastChange = lastChange;
}
}
boolean setEnabled(boolean enabled) {
if (mEnabled != enabled) {
mEnabled = enabled;
mLastChange = getCurrentTimeMillis();
return true;
}
return false;
}
}
private class SensorUseReminderDialogInfo {
private int mTaskId;
private UserHandle mUser;
@@ -323,14 +257,7 @@ 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<SensorState> userIndividualEnabled = mIndividualEnabled.get(userId,
new SparseArray<>());
SensorState sensorState = userIndividualEnabled.get(sensor);
long lastChange;
if (sensorState != null) {
lastChange = sensorState.mLastChange;
if (!sensorState.setEnabled(enable)) {
// State not changing
return;
}
} else {
sensorState = new SensorState(enable);
lastChange = sensorState.mLastChange;
userIndividualEnabled.put(sensor, sensorState);
}
mIndividualEnabled.put(userId, userIndividualEnabled);
if (userId == mUserManagerInternal.getProfileParentId(userId)) {
logSensorPrivacyToggle(source, sensor, sensorState.mEnabled, lastChange);
}
if (!enable) {
final long token = Binder.clearCallingIdentity();
try {
// Remove any notifications prompting the user to disable sensory privacy
NotificationManager notificationManager =
mContext.getSystemService(NotificationManager.class);
notificationManager.cancel(sensor);
} finally {
Binder.restoreCallingIdentity(token);
}
}
persistSensorPrivacyState();
}
mHandler.onSensorPrivacyChanged(userId, sensor, enable);
final long[] lastChange = new long[1];
mSensorPrivacyStateController.atomic(() -> {
SensorState sensorState = mSensorPrivacyStateController
.getState(SensorPrivacyManager.ToggleTypes.SOFTWARE, userId, sensor);
lastChange[0] = sensorState.getLastChange();
mSensorPrivacyStateController.setState(
SensorPrivacyManager.ToggleTypes.SOFTWARE, userId, sensor, enable, mHandler,
changeSuccessful -> {
if (changeSuccessful) {
if (userId == mUserManagerInternal.getProfileParentId(userId)) {
mHandler.sendMessage(PooledLambda.obtainMessage(
SensorPrivacyServiceImpl::logSensorPrivacyToggle, this,
source, sensor, enable, lastChange[0], false));
}
}
});
});
}
private boolean canChangeIndividualSensorPrivacy(@UserIdInt int userId, int sensor) {
@@ -801,14 +724,23 @@ 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<SensorState> states = mIndividualEnabled.get(userId);
if (states == null) {
return false;
}
SensorState state = states.get(sensor);
if (state == null) {
return false;
}
return state.mEnabled;
}
/**
* Returns the state of sensor privacy from persistent storage.
*/
private boolean readPersistedSensorPrivacyStateLocked() {
// if the file does not exist then sensor privacy has not yet been enabled on
// the device.
SparseArray<Object> map = new SparseArray<>();
int version = -1;
if (mAtomicFile.exists()) {
try (FileInputStream inputStream = mAtomicFile.openRead()) {
TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
XmlUtils.beginDocument(parser, XML_TAG_SENSOR_PRIVACY);
final int persistenceVersion = parser.getAttributeInt(null,
XML_ATTRIBUTE_PERSISTENCE_VERSION, 0);
// Use inline string literals for xml tags/attrs when parsing old versions since
// these should never be changed even with refactorings.
if (persistenceVersion == 0) {
boolean enabled = parser.getAttributeBoolean(null, "enabled", false);
SparseArray<SensorState> individualEnabled = new SparseArray<>();
version = 0;
XmlUtils.nextElement(parser);
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
String tagName = parser.getName();
if ("individual-sensor-privacy".equals(tagName)) {
int sensor = XmlUtils.readIntAttribute(parser, "sensor");
boolean indEnabled = XmlUtils.readBooleanAttribute(parser,
"enabled");
individualEnabled.put(sensor, new SensorState(indEnabled));
XmlUtils.skipCurrentTag(parser);
} else {
XmlUtils.nextElement(parser);
}
}
map.put(VER0_ENABLED, enabled);
map.put(VER0_INDIVIDUAL_ENABLED, individualEnabled);
} else if (persistenceVersion == CURRENT_PERSISTENCE_VERSION) {
SparseBooleanArray enabled = new SparseBooleanArray();
SparseArray<SparseArray<SensorState>> individualEnabled =
new SparseArray<>();
version = parser.getAttributeInt(null,
XML_ATTRIBUTE_VERSION, 1);
int currentUserId = -1;
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
XmlUtils.nextElement(parser);
String tagName = parser.getName();
if (XML_TAG_USER.equals(tagName)) {
currentUserId = parser.getAttributeInt(null, XML_ATTRIBUTE_ID);
boolean isEnabled = parser.getAttributeBoolean(null,
XML_ATTRIBUTE_ENABLED);
if (enabled.indexOfKey(currentUserId) >= 0) {
Log.e(TAG, "User listed multiple times in file.",
new RuntimeException());
mAtomicFile.delete();
version = -1;
break;
}
if (mUserManagerInternal.getUserInfo(currentUserId) == null) {
// User may no longer exist, skip this user
currentUserId = -1;
continue;
}
enabled.put(currentUserId, isEnabled);
}
if (XML_TAG_INDIVIDUAL_SENSOR_PRIVACY.equals(tagName)) {
if (mUserManagerInternal.getUserInfo(currentUserId) == null) {
// User may no longer exist or isn't set
continue;
}
int sensor = parser.getAttributeInt(null, XML_ATTRIBUTE_SENSOR);
boolean isEnabled = parser.getAttributeBoolean(null,
XML_ATTRIBUTE_ENABLED);
long lastChange = parser
.getAttributeLong(null, XML_ATTRIBUTE_LAST_CHANGE, -1);
SparseArray<SensorState> userIndividualEnabled =
individualEnabled.get(currentUserId, new SparseArray<>());
userIndividualEnabled
.put(sensor, new SensorState(isEnabled, lastChange));
individualEnabled.put(currentUserId, userIndividualEnabled);
}
}
map.put(VER1_ENABLED, enabled);
map.put(VER1_INDIVIDUAL_ENABLED, individualEnabled);
} else {
Log.e(TAG, "Unknown persistence version: " + persistenceVersion
+ ". Deleting.",
new RuntimeException());
mAtomicFile.delete();
version = -1;
}
} catch (IOException | XmlPullParserException e) {
Log.e(TAG, "Caught an exception reading the state from storage: ", e);
// Delete the file to prevent the same error on subsequent calls and assume
// sensor privacy is not enabled.
mAtomicFile.delete();
version = -1;
}
}
try {
return upgradeAndInit(version, map);
} catch (Exception e) {
Log.wtf(TAG, "Failed to upgrade and set sensor privacy state,"
+ " resetting to default.", e);
mEnabled = new SparseBooleanArray();
mIndividualEnabled = new SparseArray<>();
return true;
}
}
private boolean upgradeAndInit(int version, SparseArray map) {
if (version == -1) {
// New file, default state for current version goes here.
mEnabled = new SparseBooleanArray();
mIndividualEnabled = new SparseArray<>();
forAllUsers(userId -> mEnabled.put(userId, false));
forAllUsers(userId -> mIndividualEnabled.put(userId, new SparseArray<>()));
return true;
}
boolean upgraded = false;
final int[] users = getLocalService(UserManagerInternal.class).getUserIds();
if (version == 0) {
final boolean enabled = (boolean) map.get(VER0_ENABLED);
final SparseArray<SensorState> individualEnabled =
(SparseArray<SensorState>) map.get(VER0_INDIVIDUAL_ENABLED);
final SparseBooleanArray perUserEnabled = new SparseBooleanArray();
final SparseArray<SparseArray<SensorState>> perUserIndividualEnabled =
new SparseArray<>();
// Copy global state to each user
for (int i = 0; i < users.length; i++) {
int user = users[i];
perUserEnabled.put(user, enabled);
SparseArray<SensorState> userIndividualSensorEnabled = new SparseArray<>();
perUserIndividualEnabled.put(user, userIndividualSensorEnabled);
for (int j = 0; j < individualEnabled.size(); j++) {
final int sensor = individualEnabled.keyAt(j);
final SensorState isSensorEnabled = individualEnabled.valueAt(j);
userIndividualSensorEnabled.put(sensor, isSensorEnabled);
}
}
map.clear();
map.put(VER1_ENABLED, perUserEnabled);
map.put(VER1_INDIVIDUAL_ENABLED, perUserIndividualEnabled);
version = 1;
upgraded = true;
}
if (version == CURRENT_VERSION) {
mEnabled = (SparseBooleanArray) map.get(VER1_ENABLED);
mIndividualEnabled =
(SparseArray<SparseArray<SensorState>>) map.get(VER1_INDIVIDUAL_ENABLED);
}
return upgraded;
}
/**
* Persists the state of sensor privacy.
*/
private void persistSensorPrivacyState() {
synchronized (mLock) {
persistSensorPrivacyStateLocked();
}
}
private void persistSensorPrivacyStateLocked() {
FileOutputStream outputStream = null;
try {
outputStream = mAtomicFile.startWrite();
TypedXmlSerializer serializer = Xml.resolveSerializer(outputStream);
serializer.startDocument(null, true);
serializer.startTag(null, XML_TAG_SENSOR_PRIVACY);
serializer.attributeInt(
null, XML_ATTRIBUTE_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION);
serializer.attributeInt(null, XML_ATTRIBUTE_VERSION, CURRENT_VERSION);
forAllUsers(userId -> {
serializer.startTag(null, XML_TAG_USER);
serializer.attributeInt(null, XML_ATTRIBUTE_ID, userId);
serializer.attributeBoolean(
null, XML_ATTRIBUTE_ENABLED, isSensorPrivacyEnabled(userId));
SparseArray<SensorState> individualEnabled =
mIndividualEnabled.get(userId, new SparseArray<>());
int numIndividual = individualEnabled.size();
for (int i = 0; i < numIndividual; i++) {
serializer.startTag(null, XML_TAG_INDIVIDUAL_SENSOR_PRIVACY);
int sensor = individualEnabled.keyAt(i);
SensorState sensorState = individualEnabled.valueAt(i);
boolean enabled = sensorState.mEnabled;
long lastChange = sensorState.mLastChange;
serializer.attributeInt(null, XML_ATTRIBUTE_SENSOR, sensor);
serializer.attributeBoolean(null, XML_ATTRIBUTE_ENABLED, enabled);
serializer.attributeLong(null, XML_ATTRIBUTE_LAST_CHANGE, lastChange);
serializer.endTag(null, XML_TAG_INDIVIDUAL_SENSOR_PRIVACY);
}
serializer.endTag(null, XML_TAG_USER);
});
serializer.endTag(null, XML_TAG_SENSOR_PRIVACY);
serializer.endDocument();
mAtomicFile.finishWrite(outputStream);
} catch (IOException e) {
Log.e(TAG, "Caught an exception persisting the sensor privacy state: ", e);
mAtomicFile.failWrite(outputStream);
}
return mSensorPrivacyStateController.getState(SensorPrivacyManager.ToggleTypes.SOFTWARE,
userId, sensor).isEnabled();
}
@Override
@@ -1285,23 +980,23 @@ 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<SensorState> individualEnabled = mIndividualEnabled.get(userId);
if (individualEnabled != null) {
int numIndividualEnabled = individualEnabled.size();
for (int i = 0; i < numIndividualEnabled; i++) {
long individualToken = dumpStream.start("individual_enabled_sensor",
SensorPrivacyUserProto.INDIVIDUAL_ENABLED_SENSOR);
dumpStream.write("sensor",
SensorPrivacyIndividualEnabledSensorProto.SENSOR,
individualEnabled.keyAt(i));
dumpStream.write("is_enabled",
SensorPrivacyIndividualEnabledSensorProto.IS_ENABLED,
individualEnabled.valueAt(i).mEnabled);
// TODO dump last change
dumpStream.end(individualToken);
}
}
dumpStream.end(userToken);
});
}
dumpStream.flush();
}
/**
* Convert a string into a {@link SensorPrivacyManager.Sensors.Sensor id}.
*
@@ -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<SensorState> 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<RemoteCallbackList<ISensorPrivacyListener>> 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();
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -183,6 +183,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;

View File

@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<sensor-privacy persistence-version="1" version="1">
<user id="0" enabled="false">
<individual-sensor-privacy sensor="1" enabled="true" />
<individual-sensor-privacy sensor="2" enabled="true" />
<individual-sensor-privacy sensor="1" enabled="true" last-change="100" />
<individual-sensor-privacy sensor="2" enabled="true" last-change="100" />
</user>
</sensor-privacy>

View File

@@ -0,0 +1,5 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<sensor-privacy persistence-version="2" version="2">
<sensor-state toggle-type="1" user-id="0" sensor="1" state-type="1" last-change="123" />
<sensor-state toggle-type="1" user-id="0" sensor="2" state-type="2" last-change="123" />
</sensor-privacy>

View File

@@ -0,0 +1,5 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<sensor-privacy persistence-version="2" version="2">
<sensor-state toggle-type="2" user-id="0" sensor="1" state-type="1" last-change="1234" />
<sensor-state toggle-type="2" user-id="0" sensor="2" state-type="2" last-change="1234" />
</sensor-privacy>

View File

@@ -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<Boolean> micState = new CompletableFuture<>();
CompletableFuture<Boolean> camState = new CompletableFuture<>();
doAnswer(invocation -> {
int code = invocation.getArgument(0);
boolean restricted = invocation.getArgument(1);
if (code == AppOpsManager.OP_RECORD_AUDIO) {
micState.complete(restricted);
} else if (code == AppOpsManager.OP_CAMERA) {
camState.complete(restricted);
}
return null;
}).when(mMockedAppOpsManagerInternal).setGlobalRestriction(anyInt(), anyBoolean(),
any());
initServiceWithPersistenceFile(onDeviceFile, persistenceFileMicMuteCamMute, 0);
Assert.assertTrue(micState.join() == expectedMicState);
Assert.assertTrue(camState.join() == expectedCamState);
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<SensorState> captor = ArgumentCaptor.forClass(SensorState.class);
verify(persistedState, times(1)).setState(eq(SOFTWARE), eq(0), eq(MICROPHONE),
captor.capture());
assertEquals(SensorPrivacyManager.StateTypes.ENABLED, captor.getValue().getState());
} finally {
mockitoSession.finishMocking();
}
onDeviceFile.delete();
}
private SensorPrivacyStateController getSensorPrivacyStateControllerImpl() {
SensorPrivacyStateControllerImpl.getInstance().resetForTestingImpl();
return SensorPrivacyStateControllerImpl.getInstance();
}
}