From 90f391fe8790fd1210d54c0e892fdfa0aa708562 Mon Sep 17 00:00:00 2001 From: Chad Brubaker Date: Fri, 19 Oct 2018 10:26:19 -0700 Subject: [PATCH] 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 --- Android.bp | 2 + api/system-current.txt | 1 + .../android/app/SystemServiceRegistry.java | 8 + core/java/android/content/Context.java | 13 + .../hardware/ISensorPrivacyListener.aidl | 29 ++ .../hardware/ISensorPrivacyManager.aidl | 35 ++ .../hardware/SensorPrivacyManager.java | 171 +++++++ core/java/android/provider/Settings.java | 34 +- core/res/AndroidManifest.xml | 4 + .../android/provider/SettingsBackupTest.java | 1 + data/etc/privapp-permissions-platform.xml | 1 + packages/SystemUI/AndroidManifest.xml | 1 + .../res/drawable/ic_signal_sensors.xml | 28 ++ packages/SystemUI/res/values/config.xml | 2 +- packages/SystemUI/res/values/strings.xml | 6 + .../src/com/android/systemui/Dependency.java | 4 + .../systemui/qs/tileimpl/QSFactoryImpl.java | 3 + .../systemui/qs/tiles/SensorPrivacyTile.java | 126 ++++++ .../qs/tiles/SensorPrivacyTileTest.java | 115 +++++ .../metrics_constants/metrics_constants.proto | 7 + .../server/LocationManagerService.java | 2 +- .../android/server/SensorPrivacyService.java | 426 ++++++++++++++++++ .../java/com/android/server/SystemServer.java | 4 + 23 files changed, 1019 insertions(+), 4 deletions(-) create mode 100644 core/java/android/hardware/ISensorPrivacyListener.aidl create mode 100644 core/java/android/hardware/ISensorPrivacyManager.aidl create mode 100644 core/java/android/hardware/SensorPrivacyManager.java create mode 100644 packages/SystemUI/res/drawable/ic_signal_sensors.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyTile.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/tiles/SensorPrivacyTileTest.java create mode 100644 services/core/java/com/android/server/SensorPrivacyService.java diff --git a/Android.bp b/Android.bp index e096c9df5662b..5ac3e683cbe68 100644 --- a/Android.bp +++ b/Android.bp @@ -207,6 +207,8 @@ java_defaults { "core/java/android/hardware/usb/IUsbSerialReader.aidl", "core/java/android/net/ICaptivePortal.aidl", "core/java/android/net/IConnectivityManager.aidl", + "core/java/android/hardware/ISensorPrivacyListener.aidl", + "core/java/android/hardware/ISensorPrivacyManager.aidl", "core/java/android/net/IIpConnectivityMetrics.aidl", "core/java/android/net/IEthernetManager.aidl", "core/java/android/net/IEthernetServiceListener.aidl", diff --git a/api/system-current.txt b/api/system-current.txt index 5a49f0bf33261..ee18b15276803 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -93,6 +93,7 @@ package android { field public static final java.lang.String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; field public static final java.lang.String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS"; field public static final java.lang.String MANAGE_ROLE_HOLDERS = "android.permission.MANAGE_ROLE_HOLDERS"; + field public static final java.lang.String MANAGE_SENSOR_PRIVACY = "android.permission.MANAGE_SENSOR_PRIVACY"; field public static final java.lang.String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER"; field public static final java.lang.String MANAGE_SUBSCRIPTION_PLANS = "android.permission.MANAGE_SUBSCRIPTION_PLANS"; field public static final java.lang.String MANAGE_USB = "android.permission.MANAGE_USB"; diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index f4fd5d1320699..45e87e045ae24 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -54,6 +54,7 @@ import android.debug.IAdbManager; import android.hardware.ConsumerIrManager; import android.hardware.ISerialManager; import android.hardware.SensorManager; +import android.hardware.SensorPrivacyManager; import android.hardware.SerialManager; import android.hardware.SystemSensorManager; import android.hardware.biometrics.BiometricManager; @@ -503,6 +504,13 @@ final class SystemServiceRegistry { ctx.mMainThread.getHandler().getLooper()); }}); + registerService(Context.SENSOR_PRIVACY_SERVICE, SensorPrivacyManager.class, + new CachedServiceFetcher() { + @Override + public SensorPrivacyManager createService(ContextImpl ctx) { + return SensorPrivacyManager.getInstance(ctx); + }}); + registerService(Context.STATS_MANAGER, StatsManager.class, new CachedServiceFetcher() { @Override diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index b39010d9d14eb..001e328fb0baf 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3080,6 +3080,7 @@ public abstract class Context { //@hide: COUNTRY_DETECTOR, SEARCH_SERVICE, SENSOR_SERVICE, + SENSOR_PRIVACY_SERVICE, STORAGE_SERVICE, STORAGE_STATS_SERVICE, WALLPAPER_SERVICE, @@ -3542,6 +3543,18 @@ public abstract class Context { */ public static final String SENSOR_SERVICE = "sensor"; + /** + * Use with {@link #getSystemService(String)} to retrieve a {@link + * android.hardware.SensorPrivacyManager} for accessing sensor privacy + * functions. + * + * @see #getSystemService(String) + * @see android.hardware.SensorPrivacyManager + * + * @hide + */ + public static final String SENSOR_PRIVACY_SERVICE = "sensor_privacy"; + /** * Use with {@link #getSystemService(String)} to retrieve a {@link * android.os.storage.StorageManager} for accessing system storage diff --git a/core/java/android/hardware/ISensorPrivacyListener.aidl b/core/java/android/hardware/ISensorPrivacyListener.aidl new file mode 100644 index 0000000000000..5d4026541c4ca --- /dev/null +++ b/core/java/android/hardware/ISensorPrivacyListener.aidl @@ -0,0 +1,29 @@ +/** + * 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 android.hardware; + +/** + * @hide + */ +oneway interface ISensorPrivacyListener { + // Since these transactions are also called from native code, these must be kept in sync with + // the ones in + // frameworks/native/libs/sensorprivacy/aidl/android/hardware/ISensorPrivacyListener.aidl + // =============== Beginning of transactions used on native side as well ====================== + void onSensorPrivacyChanged(boolean enabled); + // =============== End of transactions used on native side as well ============================ +} diff --git a/core/java/android/hardware/ISensorPrivacyManager.aidl b/core/java/android/hardware/ISensorPrivacyManager.aidl new file mode 100644 index 0000000000000..1ba7b989a7647 --- /dev/null +++ b/core/java/android/hardware/ISensorPrivacyManager.aidl @@ -0,0 +1,35 @@ +/** + * 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 android.hardware; + +import android.hardware.ISensorPrivacyListener; + +/** @hide */ +interface ISensorPrivacyManager { + // Since these transactions are also called from native code, these must be kept in sync with + // the ones in + // frameworks/native/libs/sensorprivacy/aidl/android/hardware/ISensorPrivacyManager.aidl + // =============== Beginning of transactions used on native side as well ====================== + void addSensorPrivacyListener(in ISensorPrivacyListener listener); + + void removeSensorPrivacyListener(in ISensorPrivacyListener listener); + + boolean isSensorPrivacyEnabled(); + + void setSensorPrivacy(boolean enable); + // =============== End of transactions used on native side as well ============================ +} \ No newline at end of file diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java new file mode 100644 index 0000000000000..274202f7c1901 --- /dev/null +++ b/core/java/android/hardware/SensorPrivacyManager.java @@ -0,0 +1,171 @@ +/* + * 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 android.hardware; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemService; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArrayMap; + +import com.android.internal.annotations.GuardedBy; + +/** + * This class provides access to the sensor privacy services; sensor privacy allows the + * user to disable access to all sensors on the device. This class provides methods to query the + * current state of sensor privacy as well as to register / unregister for notification when + * the sensor privacy state changes. + * + * @hide + */ +@SystemService(Context.SENSOR_PRIVACY_SERVICE) +public final class SensorPrivacyManager { + + /** + * A class implementing this interface can register with the {@link + * android.hardware.SensorPrivacyManager} to receive notification when the sensor privacy + * state changes. + */ + public interface OnSensorPrivacyChangedListener { + /** + * Callback invoked when the sensor privacy state changes. + * + * @param enabled true if sensor privacy is enabled, false otherwise. + */ + void onSensorPrivacyChanged(boolean enabled); + } + + private static final Object sInstanceLock = new Object(); + + @GuardedBy("sInstanceLock") + private static SensorPrivacyManager sInstance; + + @NonNull + private final Context mContext; + + @NonNull + private final ISensorPrivacyManager mService; + + @NonNull + private final ArrayMap mListeners; + + /** + * Private constructor to ensure only a single instance is created. + */ + private SensorPrivacyManager(Context context, ISensorPrivacyManager service) { + mContext = context; + mService = service; + mListeners = new ArrayMap<>(); + } + + /** + * Returns the single instance of the SensorPrivacyManager. + */ + public static SensorPrivacyManager getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + try { + IBinder b = ServiceManager.getServiceOrThrow(Context.SENSOR_PRIVACY_SERVICE); + ISensorPrivacyManager service = ISensorPrivacyManager.Stub.asInterface(b); + sInstance = new SensorPrivacyManager(context, service); + } catch (ServiceManager.ServiceNotFoundException e) { + throw new IllegalStateException(e); + } + } + return sInstance; + } + } + + /** + * Sets sensor privacy to the specified state. + * + * @param enable the state to which sensor privacy should be set. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) + public void setSensorPrivacy(boolean enable) { + try { + mService.setSensorPrivacy(enable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers a new listener to receive notification when the state of sensor privacy + * changes. + * + * @param listener the OnSensorPrivacyChangedListener to be notified when the state of sensor + * privacy changes. + */ + public void addSensorPrivacyListener(final OnSensorPrivacyChangedListener listener) { + synchronized (mListeners) { + ISensorPrivacyListener iListener = mListeners.get(listener); + if (iListener == null) { + iListener = new ISensorPrivacyListener.Stub() { + @Override + public void onSensorPrivacyChanged(boolean enabled) { + listener.onSensorPrivacyChanged(enabled); + } + }; + mListeners.put(listener, iListener); + } + + try { + mService.addSensorPrivacyListener(iListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Unregisters the specified listener from receiving notifications when the state of sensor + * privacy changes. + * + * @param listener the OnSensorPrivacyChangedListener to be unregistered from notifications when + * sensor privacy changes. + */ + public void removeSensorPrivacyListener(OnSensorPrivacyChangedListener listener) { + synchronized (mListeners) { + ISensorPrivacyListener iListener = mListeners.get(listener); + if (iListener != null) { + mListeners.remove(iListener); + try { + mService.removeSensorPrivacyListener(iListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * Returns whether sensor privacy is currently enabled. + * + * @return true if sensor privacy is currently enabled, false otherwise. + */ + public boolean isSensorPrivacyEnabled() { + try { + return mService.isSensorPrivacyEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index cbcc492f1749b..bc7b03af6196c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8261,6 +8261,38 @@ public final class Settings { public static final String PACKAGES_TO_CLEAR_DATA_BEFORE_FULL_RESTORE = "packages_to_clear_data_before_full_restore"; + /** + * Indicates the location state should be maintained after sensor privacy is disabled. + * @hide + */ + public static final String MAINTAIN_LOCATION_AFTER_SP_DISABLED = "0"; + + /** + * Indicates location should be reenabled after sensor privacy is disabled. + * @hide + */ + public static final String REENABLE_LOCATION_AFTER_SP_DISABLED = "1"; + + /** + * Indicates the state of airplane mode should be maintained after sensor privacy is + * disabled. + * @hide + */ + public static final String MAINTAIN_AIRPLANE_MODE_AFTER_SP_DISABLED = "0"; + + /** + * Indicates airplane mode should be disabled after sensor privacy is disabled. + * @hide + */ + public static final String DISABLE_AIRPLANE_MODE_AFTER_SP_DISABLED = "1"; + + /** + * The state of all sensors managed by SensorPrivacyService when sensor privacy is enabled. + * @hide + */ + public static final String SENSOR_PRIVACY_SENSOR_STATE = + "sensor_privacy_sensor_state"; + /** * Setting to determine whether to use the new notification priority handling features. * @hide @@ -13901,7 +13933,6 @@ public final class Settings { */ public static final String LAST_ACTIVE_USER_ID = "last_active_persistent_user_id"; - /** * Whether we've enabled native flags health check on this device. Takes effect on * reboot. The value "1" enables native flags health check; otherwise it's disabled. @@ -13909,7 +13940,6 @@ public final class Settings { */ public static final String NATIVE_FLAGS_HEALTH_CHECK_ENABLED = "native_flags_health_check_enabled"; - } /** diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index dca15bd49ca4e..7fa3e66bae255 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4342,6 +4342,10 @@ @hide --> + + + diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 7d53c2f781517..1c1a1404eacd3 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -77,6 +77,7 @@ + diff --git a/packages/SystemUI/res/drawable/ic_signal_sensors.xml b/packages/SystemUI/res/drawable/ic_signal_sensors.xml new file mode 100644 index 0000000000000..faaddf6473c4b --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_signal_sensors.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 9e97cd8f40990..61efbd5c22485 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -111,7 +111,7 @@ - wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,work,cast,night + wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,work,cast,night,sensorprivacy diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 9917257bba8ae..d45730714c426 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -598,6 +598,10 @@ Data Saver turned off. Data Saver turned on. + + Sensor Privacy turned off. + + Sensor Privacy turned on. Display brightness @@ -2318,4 +2322,6 @@ %d other app %d other apps + + Sensors off diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 5e6d272bd4274..327ffcd247620 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -17,6 +17,7 @@ package com.android.systemui; import android.content.Context; import android.content.res.Configuration; import android.hardware.SensorManager; +import android.hardware.SensorPrivacyManager; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -191,6 +192,9 @@ public class Dependency extends SystemUI { new AsyncSensorManager(mContext.getSystemService(SensorManager.class), getDependency(PluginManager.class))); + mProviders.put(SensorPrivacyManager.class, () -> + mContext.getSystemService(SensorPrivacyManager.class)); + mProviders.put(BluetoothController.class, () -> new BluetoothControllerImpl(mContext, getDependency(BG_LOOPER))); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index 7d52f0b2763dc..fd2c4e35e49b2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -41,6 +41,7 @@ import com.android.systemui.qs.tiles.LocationTile; import com.android.systemui.qs.tiles.NfcTile; import com.android.systemui.qs.tiles.NightDisplayTile; import com.android.systemui.qs.tiles.RotationLockTile; +import com.android.systemui.qs.tiles.SensorPrivacyTile; import com.android.systemui.qs.tiles.UserTile; import com.android.systemui.qs.tiles.WifiTile; import com.android.systemui.qs.tiles.WorkModeTile; @@ -100,6 +101,8 @@ public class QSFactoryImpl implements QSFactory { return new NightDisplayTile(mHost); case "nfc": return new NfcTile(mHost); + case "sensorprivacy": + return new SensorPrivacyTile(mHost); } // Intent tiles. diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyTile.java new file mode 100644 index 0000000000000..ff368f80d06a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyTile.java @@ -0,0 +1,126 @@ +/* + * 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.systemui.qs.tiles; + +import android.content.Intent; +import android.hardware.SensorPrivacyManager; +import android.service.quicksettings.Tile; +import android.widget.Switch; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; +import com.android.systemui.R; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.statusbar.policy.KeyguardMonitor; + +/** Quick settings tile: SensorPrivacy mode **/ +public class SensorPrivacyTile extends QSTileImpl implements + SensorPrivacyManager.OnSensorPrivacyChangedListener { + private static final String TAG = "SensorPrivacy"; + private final Icon mIcon = + ResourceIcon.get(R.drawable.ic_signal_sensors); + private final KeyguardMonitor mKeyguard; + private final SensorPrivacyManager mSensorPrivacyManager; + + public SensorPrivacyTile(QSHost host) { + super(host); + + mSensorPrivacyManager = Dependency.get(SensorPrivacyManager.class); + mKeyguard = Dependency.get(KeyguardMonitor.class); + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleClick() { + final boolean wasEnabled = mState.value; + // Don't allow disabling from the lockscreen. + if (wasEnabled && mKeyguard.isSecure() && mKeyguard.isShowing()) { + Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { + MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); + setEnabled(!wasEnabled); + }); + return; + } + + MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); + setEnabled(!wasEnabled); + } + + private void setEnabled(boolean enabled) { + mSensorPrivacyManager.setSensorPrivacy(enabled); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.sensor_privacy_mode); + } + + @Override + public Intent getLongClickIntent() { + return null; + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final boolean enabled = arg instanceof Boolean ? (Boolean) arg + : mSensorPrivacyManager.isSensorPrivacyEnabled(); + state.value = enabled; + state.label = mContext.getString(R.string.sensor_privacy_mode); + state.icon = mIcon; + state.state = enabled ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + state.contentDescription = state.label; + state.expandedAccessibilityClassName = Switch.class.getName(); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.QS_SENSOR_PRIVACY; + } + + @Override + protected String composeChangeAnnouncement() { + if (mState.value) { + return mContext + .getString(R.string.accessibility_quick_settings_sensor_privacy_changed_on); + } else { + return mContext + .getString(R.string.accessibility_quick_settings_sensor_privacy_changed_off); + } + } + + @Override + protected void handleSetListening(boolean listening) { + if (listening) { + mSensorPrivacyManager.addSensorPrivacyListener(this); + } else { + mSensorPrivacyManager.removeSensorPrivacyListener(this); + } + } + + @Override + public void onSensorPrivacyChanged(boolean enabled) { + refreshState(enabled); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/SensorPrivacyTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/SensorPrivacyTileTest.java new file mode 100644 index 0000000000000..90792e3d958fc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/SensorPrivacyTileTest.java @@ -0,0 +1,115 @@ +/* + * 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.systemui.qs.tiles; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.SensorPrivacyManager; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.systemui.Dependency; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.qs.QSTileHost; +import com.android.systemui.statusbar.policy.KeyguardMonitor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class SensorPrivacyTileTest extends SysuiTestCase { + + @Mock + private KeyguardMonitor mKeyguard; + @Mock + private QSTileHost mHost; + @Mock + SensorPrivacyManager mSensorPrivacyManager; + + private TestableLooper mTestableLooper; + + private SensorPrivacyTile mSensorPrivacyTile; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mTestableLooper = TestableLooper.get(this); + mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); + mKeyguard = mDependency.injectMockDependency(KeyguardMonitor.class); + + mSensorPrivacyManager = mDependency.injectMockDependency(SensorPrivacyManager.class); + + when(mHost.getContext()).thenReturn(mContext); + + mSensorPrivacyTile = new SensorPrivacyTile(mHost); + } + + @Test + public void testSensorPrivacyListenerAdded_handleListeningTrue() { + // To prevent access to privacy related features from apps with WRITE_SECURE_SETTINGS the + // sensor privacy state is not stored in Settings; to receive notification apps must add + // themselves as a listener with the SensorPrivacyManager. This test verifies when + // setListening is called with a value of true the tile adds itself as a listener. + mSensorPrivacyTile.handleSetListening(true); + mTestableLooper.processAllMessages(); + verify(mSensorPrivacyManager).addSensorPrivacyListener(mSensorPrivacyTile); + } + + @Test + public void testSensorPrivacyListenerRemoved_handleListeningFalse() { + // Similar to the test above verifies that the tile removes itself as a listener when + // setListening is called with a value of false. + mSensorPrivacyTile.handleSetListening(false); + mTestableLooper.processAllMessages(); + verify(mSensorPrivacyManager).removeSensorPrivacyListener((mSensorPrivacyTile)); + } + + @Test + public void testSensorPrivacyEnabled_handleClick() { + // Verifies when the SensorPrivacy tile is clicked it invokes the SensorPrivacyManager to + // set sensor privacy. + mSensorPrivacyTile.getState().value = false; + mSensorPrivacyTile.handleClick(); + mTestableLooper.processAllMessages(); + verify(mSensorPrivacyManager).setSensorPrivacy(true); + + mSensorPrivacyTile.getState().value = true; + mSensorPrivacyTile.handleClick(); + mTestableLooper.processAllMessages(); + verify(mSensorPrivacyManager).setSensorPrivacy(false); + } + + @Test + public void testSensorPrivacyNotDisabled_keyguard() { + // Verifies when the device is locked that sensor privacy cannot be disabled + when(mKeyguard.isSecure()).thenReturn(true); + when(mKeyguard.isShowing()).thenReturn(true); + mSensorPrivacyTile.getState().value = true; + mSensorPrivacyTile.handleClick(); + mTestableLooper.processAllMessages(); + verify(mSensorPrivacyManager, never()).setSensorPrivacy(false); + } +} diff --git a/proto/src/metrics_constants/metrics_constants.proto b/proto/src/metrics_constants/metrics_constants.proto index eb0090be47478..66d64b1a5f7a4 100644 --- a/proto/src/metrics_constants/metrics_constants.proto +++ b/proto/src/metrics_constants/metrics_constants.proto @@ -6642,6 +6642,13 @@ message MetricsEvent { // OS: Q SETTINGS_FINANCIAL_APPS_SMS_ACCESS = 1597; + // OPEN: QS Sensor Privacy Mode tile shown + // ACTION: QS Sensor Privacy Mode tile tapped + // SUBTYPE: 0 is off, 1 is on + // CATEGORY: QUICK_SETTINGS + // OS: Q + QS_SENSOR_PRIVACY = 1598; + // ---- End Q Constants, all Q constants go above this line ---- // Add new aosp constants above this line. diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java index 8b992ebcf0597..47396eed6c3fd 100644 --- a/services/core/java/com/android/server/LocationManagerService.java +++ b/services/core/java/com/android/server/LocationManagerService.java @@ -2763,7 +2763,7 @@ public class LocationManagerService extends ILocationManager.Stub { */ @Override public void setLocationEnabledForUser(boolean enabled, int userId) { - mContext.enforceCallingPermission( + mContext.enforceCallingOrSelfPermission( android.Manifest.permission.WRITE_SECURE_SETTINGS, "Requires WRITE_SECURE_SETTINGS permission"); 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 0000000000000..1cbcbe5a8bdb0 --- /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 mListeners = + new RemoteCallbackList<>(); + private final ArrayMap 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())); + } + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index e1b83fc998973..cf03d613634f1 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -724,6 +724,10 @@ public final class SystemServer { mSystemServiceManager.startService(new OverlayManagerService(mSystemContext, installer)); traceEnd(); + traceBeginAndSlog("StartSensorPrivacyService"); + mSystemServiceManager.startService(new SensorPrivacyService(mSystemContext)); + traceEnd(); + // The sensor service needs access to package manager service, app ops // service, and permissions service, therefore we start it after them. // Start sensor service in a separate thread. Completion should be checked