Prototype Spaceship mode qstile
Initial prototype disabling location/sensors and enabling airplane mode.
Camera/Mic will come in a followup.
Test: manual
Bug: 110842805
Change-Id: I26132fcc9ffea83e3e78a0e54882d23c99ee590c
diff --git a/services/core/java/com/android/server/SensorPrivacyService.java b/services/core/java/com/android/server/SensorPrivacyService.java
new file mode 100644
index 0000000..1cbcbe5
--- /dev/null
+++ b/services/core/java/com/android/server/SensorPrivacyService.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2018 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;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.hardware.ISensorPrivacyListener;
+import android.hardware.ISensorPrivacyManager;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.NoSuchElementException;
+
+/** @hide */
+public final class SensorPrivacyService extends SystemService {
+
+ private static final String TAG = "SensorPrivacyService";
+
+ 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_ATTRIBUTE_ENABLED = "enabled";
+
+ private final SensorPrivacyServiceImpl mSensorPrivacyServiceImpl;
+
+ public SensorPrivacyService(Context context) {
+ super(context);
+ mSensorPrivacyServiceImpl = new SensorPrivacyServiceImpl(context);
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.SENSOR_PRIVACY_SERVICE, mSensorPrivacyServiceImpl);
+ }
+
+ class SensorPrivacyServiceImpl extends ISensorPrivacyManager.Stub {
+
+ private final SensorPrivacyHandler mHandler;
+ private final Context mContext;
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final AtomicFile mAtomicFile;
+ @GuardedBy("mLock")
+ private boolean mEnabled;
+
+ SensorPrivacyServiceImpl(Context context) {
+ mContext = context;
+ mHandler = new SensorPrivacyHandler(FgThread.get().getLooper(), mContext);
+ File sensorPrivacyFile = new File(Environment.getDataSystemDirectory(),
+ SENSOR_PRIVACY_XML_FILE);
+ mAtomicFile = new AtomicFile(sensorPrivacyFile);
+ synchronized (mLock) {
+ mEnabled = readPersistedSensorPrivacyEnabledLocked();
+ }
+ }
+
+ /**
+ * Sets the sensor privacy to the provided state and notifies all listeners of the new
+ * state.
+ */
+ @Override
+ public void setSensorPrivacy(boolean enable) {
+ enforceSensorPrivacyPermission();
+ synchronized (mLock) {
+ mEnabled = enable;
+ FileOutputStream outputStream = null;
+ try {
+ XmlSerializer serializer = new FastXmlSerializer();
+ outputStream = mAtomicFile.startWrite();
+ serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
+ serializer.startDocument(null, true);
+ serializer.startTag(null, XML_TAG_SENSOR_PRIVACY);
+ serializer.attribute(null, XML_ATTRIBUTE_ENABLED, String.valueOf(enable));
+ 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);
+ }
+ }
+ mHandler.onSensorPrivacyChanged(enable);
+ }
+
+ /**
+ * Enforces the caller contains the necessary permission to change the state of sensor
+ * privacy.
+ */
+ private void enforceSensorPrivacyPermission() {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.MANAGE_SENSOR_PRIVACY) == PERMISSION_GRANTED) {
+ return;
+ }
+ throw new SecurityException(
+ "Changing sensor privacy requires the following permission: "
+ + android.Manifest.permission.MANAGE_SENSOR_PRIVACY);
+ }
+
+ /**
+ * Returns whether sensor privacy is enabled.
+ */
+ @Override
+ public boolean isSensorPrivacyEnabled() {
+ synchronized (mLock) {
+ return mEnabled;
+ }
+ }
+
+ /**
+ * Returns the state of sensor privacy from persistent storage.
+ */
+ private boolean readPersistedSensorPrivacyEnabledLocked() {
+ // if the file does not exist then sensor privacy has not yet been enabled on
+ // the device.
+ if (!mAtomicFile.exists()) {
+ return false;
+ }
+ boolean enabled;
+ try (FileInputStream inputStream = mAtomicFile.openRead()) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(inputStream, StandardCharsets.UTF_8.name());
+ XmlUtils.beginDocument(parser, XML_TAG_SENSOR_PRIVACY);
+ parser.next();
+ String tagName = parser.getName();
+ enabled = Boolean.valueOf(parser.getAttributeValue(null, XML_ATTRIBUTE_ENABLED));
+ } 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();
+ enabled = false;
+ }
+ return enabled;
+ }
+
+ /**
+ * Persists the state of sensor privacy.
+ */
+ private void persistSensorPrivacyState() {
+ synchronized (mLock) {
+ FileOutputStream outputStream = null;
+ try {
+ XmlSerializer serializer = new FastXmlSerializer();
+ outputStream = mAtomicFile.startWrite();
+ serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
+ serializer.startDocument(null, true);
+ serializer.startTag(null, XML_TAG_SENSOR_PRIVACY);
+ serializer.attribute(null, XML_ATTRIBUTE_ENABLED, String.valueOf(mEnabled));
+ 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);
+ }
+ }
+ }
+
+ /**
+ * Registers a listener to be notified when the sensor privacy state changes.
+ */
+ @Override
+ public void addSensorPrivacyListener(ISensorPrivacyListener listener) {
+ if (listener == null) {
+ throw new NullPointerException("listener cannot be null");
+ }
+ mHandler.addListener(listener);
+ }
+
+ /**
+ * Unregisters a listener from sensor privacy state change notifications.
+ */
+ @Override
+ public void removeSensorPrivacyListener(ISensorPrivacyListener listener) {
+ if (listener == null) {
+ throw new NullPointerException("listener cannot be null");
+ }
+ mHandler.removeListener(listener);
+ }
+ }
+
+ /**
+ * Handles sensor privacy state changes and notifying listeners of the change.
+ */
+ private final class SensorPrivacyHandler extends Handler {
+ private static final int MESSAGE_SENSOR_PRIVACY_CHANGED = 1;
+
+ private final Object mListenerLock = new Object();
+
+ @GuardedBy("mListenerLock")
+ private final RemoteCallbackList<ISensorPrivacyListener> mListeners =
+ new RemoteCallbackList<>();
+ private final ArrayMap<ISensorPrivacyListener, DeathRecipient> mDeathRecipients;
+ private final Context mContext;
+
+ SensorPrivacyHandler(Looper looper, Context context) {
+ super(looper);
+ mDeathRecipients = new ArrayMap<>();
+ mContext = context;
+ }
+
+ public void onSensorPrivacyChanged(boolean enabled) {
+ sendMessage(PooledLambda.obtainMessage(SensorPrivacyHandler::handleSensorPrivacyChanged,
+ this, enabled));
+ sendMessage(
+ PooledLambda.obtainMessage(SensorPrivacyServiceImpl::persistSensorPrivacyState,
+ mSensorPrivacyServiceImpl));
+ }
+
+ public void addListener(ISensorPrivacyListener listener) {
+ synchronized (mListenerLock) {
+ DeathRecipient deathRecipient = new DeathRecipient(listener);
+ mDeathRecipients.put(listener, deathRecipient);
+ mListeners.register(listener);
+ }
+ }
+
+ public void removeListener(ISensorPrivacyListener listener) {
+ synchronized (mListenerLock) {
+ DeathRecipient deathRecipient = mDeathRecipients.remove(listener);
+ if (deathRecipient != null) {
+ deathRecipient.destroy();
+ }
+ mListeners.unregister(listener);
+ }
+ }
+
+ public void handleSensorPrivacyChanged(boolean enabled) {
+ final int count = mListeners.beginBroadcast();
+ for (int i = 0; i < count; i++) {
+ ISensorPrivacyListener listener = mListeners.getBroadcastItem(i);
+ try {
+ listener.onSensorPrivacyChanged(enabled);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Caught an exception notifying listener " + listener + ": ", e);
+ }
+ }
+ mListeners.finishBroadcast();
+ // Handle the state of all sensors managed by this service.
+ SensorState.handleSensorPrivacyToggled(mContext, enabled);
+ }
+ }
+
+ private final class DeathRecipient implements IBinder.DeathRecipient {
+
+ private ISensorPrivacyListener mListener;
+
+ DeathRecipient(ISensorPrivacyListener listener) {
+ mListener = listener;
+ try {
+ mListener.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ mSensorPrivacyServiceImpl.removeSensorPrivacyListener(mListener);
+ }
+
+ public void destroy() {
+ try {
+ mListener.asBinder().unlinkToDeath(this, 0);
+ } catch (NoSuchElementException e) {
+ }
+ }
+ }
+
+ /**
+ * Maintains the state of the sensors when sensor privacy is enabled to return them to their
+ * original state when sensor privacy is disabled.
+ */
+ private static final class SensorState {
+
+ private static Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static SensorState sPreviousState;
+
+ private boolean mAirplaneEnabled;
+ private boolean mLocationEnabled;
+
+ SensorState(boolean airplaneEnabled, boolean locationEnabled) {
+ mAirplaneEnabled = airplaneEnabled;
+ mLocationEnabled = locationEnabled;
+ }
+
+ public static void handleSensorPrivacyToggled(Context context, boolean enabled) {
+ synchronized (sLock) {
+ SensorState state;
+ if (enabled) {
+ // if sensor privacy is being enabled then obtain the current state of the
+ // sensors to be persisted and restored when sensor privacy is disabled.
+ state = getCurrentSensorState(context);
+ } else {
+ // else obtain the previous sensor state to be restored, first from the saved
+ // state if available, otherwise attempt to read it from Settings.
+ if (sPreviousState != null) {
+ state = sPreviousState;
+ } else {
+ state = getPersistedSensorState(context);
+ }
+ // if the previous state is not available then return without attempting to
+ // modify the sensor state.
+ if (state == null) {
+ return;
+ }
+ }
+ // The SensorState represents the state of the sensor before sensor privacy was
+ // enabled; if airplane mode was not enabled then the state of airplane mode should
+ // be the same as the state of sensor privacy.
+ if (!state.mAirplaneEnabled) {
+ setAirplaneMode(context, enabled);
+ }
+ // Similar to airplane mode the state of location should be the opposite of sensor
+ // privacy mode, if it was enabled when sensor privacy was enabled then it should be
+ // disabled. If location is disabled when sensor privacy is enabled then it will be
+ // left disabled when sensor privacy is disabled.
+ if (state.mLocationEnabled) {
+ setLocationEnabled(context, !enabled);
+ }
+
+ // if sensor privacy is being enabled then persist the current state.
+ if (enabled) {
+ sPreviousState = state;
+ persistState(context, sPreviousState);
+ }
+ }
+ }
+
+ public static SensorState getCurrentSensorState(Context context) {
+ LocationManager locationManager = (LocationManager) context.getSystemService(
+ Context.LOCATION_SERVICE);
+ boolean airplaneEnabled = Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
+ boolean locationEnabled = locationManager.isLocationEnabled();
+ return new SensorState(airplaneEnabled, locationEnabled);
+ }
+
+ public static void persistState(Context context, SensorState state) {
+ StringBuilder stateValue = new StringBuilder();
+ stateValue.append(state.mAirplaneEnabled
+ ? Settings.Secure.MAINTAIN_AIRPLANE_MODE_AFTER_SP_DISABLED
+ : Settings.Secure.DISABLE_AIRPLANE_MODE_AFTER_SP_DISABLED);
+ stateValue.append(",");
+ stateValue.append(
+ state.mLocationEnabled ? Settings.Secure.REENABLE_LOCATION_AFTER_SP_DISABLED
+ : Settings.Secure.MAINTAIN_LOCATION_AFTER_SP_DISABLED);
+ Settings.Secure.putString(context.getContentResolver(),
+ Settings.Secure.SENSOR_PRIVACY_SENSOR_STATE, stateValue.toString());
+ }
+
+ public static SensorState getPersistedSensorState(Context context) {
+ String persistedState = Settings.Secure.getString(context.getContentResolver(),
+ Settings.Secure.SENSOR_PRIVACY_SENSOR_STATE);
+ if (persistedState == null) {
+ Log.e(TAG, "The persisted sensor state could not be obtained from Settings");
+ return null;
+ }
+ String[] sensorStates = persistedState.split(",");
+ if (sensorStates.length < 2) {
+ Log.e(TAG, "The persisted sensor state does not contain the expected values: "
+ + persistedState);
+ return null;
+ }
+ boolean airplaneEnabled = sensorStates[0].equals(
+ Settings.Secure.MAINTAIN_AIRPLANE_MODE_AFTER_SP_DISABLED);
+ boolean locationEnabled = sensorStates[1].equals(
+ Settings.Secure.REENABLE_LOCATION_AFTER_SP_DISABLED);
+ return new SensorState(airplaneEnabled, locationEnabled);
+ }
+
+ private static void setAirplaneMode(Context context, boolean enable) {
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ connectivityManager.setAirplaneMode(enable);
+ }
+
+ private static void setLocationEnabled(Context context, boolean enable) {
+ LocationManager locationManager = (LocationManager) context.getSystemService(
+ Context.LOCATION_SERVICE);
+ locationManager.setLocationEnabledForUser(enable,
+ UserHandle.of(ActivityManager.getCurrentUser()));
+ }
+ }
+}