diff options
6 files changed, 301 insertions, 9 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index ebf6e4888de4..b828c76efb67 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1779,6 +1779,8 @@ <string name="tuner_full_importance_settings">Power notification controls</string> <string name="tuner_full_importance_settings_on">On</string> <string name="tuner_full_importance_settings_off">Off</string> + <!-- [CHAR LIMIT=NONE] Notification camera based rotation enabled description --> + <string name="rotation_lock_camera_rotation_on">On - Face-based</string> <string name="power_notification_controls_description">With power notification controls, you can set an importance level from 0 to 5 for an app\'s notifications. \n\n<b>Level 5</b> \n- Show at the top of the notification list diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java index 0bbb5bdd851a..4e936b8137af 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -16,12 +16,19 @@ package com.android.systemui.qs.tiles; +import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; + +import android.Manifest; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; +import android.hardware.SensorPrivacyManager; import android.os.Handler; import android.os.Looper; import android.provider.Settings; +import android.provider.Settings.Secure; import android.service.quicksettings.Tile; import android.view.View; import android.widget.Switch; @@ -38,18 +45,25 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile.BooleanState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.SecureSetting; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.RotationLockController; import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback; +import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; /** Quick settings tile: Rotation **/ -public class RotationLockTile extends QSTileImpl<BooleanState> { +public class RotationLockTile extends QSTileImpl<BooleanState> implements + BatteryController.BatteryStateChangeCallback { private final Icon mIcon = ResourceIcon.get(com.android.internal.R.drawable.ic_qs_auto_rotate); private final RotationLockController mController; + private final SensorPrivacyManager mPrivacyManager; + private final BatteryController mBatteryController; + private final SecureSetting mSetting; @Inject public RotationLockTile( @@ -61,12 +75,38 @@ public class RotationLockTile extends QSTileImpl<BooleanState> { StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, - RotationLockController rotationLockController + RotationLockController rotationLockController, + SensorPrivacyManager privacyManager, + BatteryController batteryController, + SecureSettings secureSettings ) { super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mController = rotationLockController; mController.observe(this, mCallback); + mPrivacyManager = privacyManager; + mBatteryController = batteryController; + mPrivacyManager + .addSensorPrivacyListener(CAMERA, (sensor, enabled) -> refreshState()); + int currentUser = host.getUserContext().getUserId(); + mSetting = new SecureSetting( + secureSettings, + mHandler, + Secure.CAMERA_AUTOROTATE, + currentUser + ) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + // mHandler is the background handler so calling this is OK + handleRefreshState(value); + } + }; + mBatteryController.observe(getLifecycle(), this); + } + + @Override + public void onPowerSaveChanged(boolean isPowerSave) { + refreshState(); } @Override @@ -95,14 +135,33 @@ public class RotationLockTile extends QSTileImpl<BooleanState> { protected void handleUpdateState(BooleanState state, Object arg) { final boolean rotationLocked = mController.isRotationLocked(); + final boolean powerSave = mBatteryController.isPowerSave(); + final boolean cameraLocked = mPrivacyManager.isSensorPrivacyEnabled( + SensorPrivacyManager.Sensors.CAMERA); + final boolean cameraRotation = + !powerSave && !cameraLocked && hasSufficientPermission(mContext) + && mController.isCameraRotationEnabled(); state.value = !rotationLocked; state.label = mContext.getString(R.string.quick_settings_rotation_unlocked_label); state.icon = mIcon; state.contentDescription = getAccessibilityString(rotationLocked); + if (!rotationLocked && cameraRotation) { + state.secondaryLabel = mContext.getResources().getString( + R.string.rotation_lock_camera_rotation_on); + } else { + state.secondaryLabel = ""; + } + state.expandedAccessibilityClassName = Switch.class.getName(); state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; } + @Override + protected void handleUserSwitch(int newUserId) { + mSetting.setUserId(newUserId); + handleRefreshState(mSetting.getValue()); + } + public static boolean isCurrentOrientationLockPortrait(RotationLockController controller, Resources resources) { int lockOrientation = controller.getRotationLockOrientation(); @@ -140,4 +199,11 @@ public class RotationLockTile extends QSTileImpl<BooleanState> { refreshState(rotationLocked); } }; + + private boolean hasSufficientPermission(Context context) { + final PackageManager packageManager = context.getPackageManager(); + final String rotationPackage = packageManager.getRotationResolverPackageName(); + return rotationPackage != null && packageManager.checkPermission( + Manifest.permission.CAMERA, rotationPackage) == PackageManager.PERMISSION_GRANTED; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java index f258fb19ff7d..1158324567ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java @@ -23,6 +23,7 @@ public interface RotationLockController extends Listenable, int getRotationLockOrientation(); boolean isRotationLockAffordanceVisible(); boolean isRotationLocked(); + boolean isCameraRotationEnabled(); void setRotationLocked(boolean locked); void setRotationLockedAtAngle(boolean locked, int rotation); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java index 53d68d0ff0ac..c185928998c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java @@ -18,11 +18,13 @@ package com.android.systemui.statusbar.policy; import android.content.Context; import android.os.UserHandle; +import android.provider.Settings.Secure; import androidx.annotation.NonNull; import com.android.internal.view.RotationPolicy; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.util.settings.SecureSettings; import java.util.concurrent.CopyOnWriteArrayList; @@ -32,20 +34,22 @@ import javax.inject.Inject; @SysUISingleton public final class RotationLockControllerImpl implements RotationLockController { private final Context mContext; + private final SecureSettings mSecureSettings; private final CopyOnWriteArrayList<RotationLockControllerCallback> mCallbacks = new CopyOnWriteArrayList<RotationLockControllerCallback>(); private final RotationPolicy.RotationPolicyListener mRotationPolicyListener = new RotationPolicy.RotationPolicyListener() { - @Override - public void onChange() { - notifyChanged(); - } - }; + @Override + public void onChange() { + notifyChanged(); + } + }; @Inject - public RotationLockControllerImpl(Context context) { + public RotationLockControllerImpl(Context context, SecureSettings secureSettings) { mContext = context; + mSecureSettings = secureSettings; setListening(true); } @@ -68,11 +72,16 @@ public final class RotationLockControllerImpl implements RotationLockController return RotationPolicy.isRotationLocked(mContext); } + public boolean isCameraRotationEnabled() { + return mSecureSettings.getIntForUser(Secure.CAMERA_AUTOROTATE, 0, UserHandle.USER_CURRENT) + == 1; + } + public void setRotationLocked(boolean locked) { RotationPolicy.setRotationLock(mContext, locked); } - public void setRotationLockedAtAngle(boolean locked, int rotation){ + public void setRotationLockedAtAngle(boolean locked, int rotation) { RotationPolicy.setRotationLockAtAngle(mContext, locked, rotation); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java new file mode 100644 index 000000000000..49ab777624e3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java @@ -0,0 +1,209 @@ +/* + * 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.systemui.qs.tiles; + +import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; +import static android.provider.Settings.Secure.CAMERA_AUTOROTATE; + +import static junit.framework.TestCase.assertEquals; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.hardware.SensorPrivacyManager; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.text.TextUtils; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.classifier.FalsingManagerFake; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSTileHost; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.RotationLockController; +import com.android.systemui.statusbar.policy.RotationLockControllerImpl; +import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.util.settings.SecureSettings; + +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(setAsMainLooper = true) +@SmallTest +public class RotationLockTileTest extends SysuiTestCase { + + private static final String PACKAGE_NAME = "package_name"; + + @Mock + private PackageManager mPackageManager; + @Mock + private ActivityStarter mActivityStarter; + @Mock + private QSTileHost mHost; + @Mock + private MetricsLogger mMetricsLogger; + @Mock + private StatusBarStateController mStatusBarStateController; + @Mock + private QSLogger mQSLogger; + @Mock + private SensorPrivacyManager mPrivacyManager; + @Mock + private BatteryController mBatteryController; + + private SecureSettings mSecureSettings; + private RotationLockController mController; + private TestableLooper mTestableLooper; + private RotationLockTile mLockTile; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTestableLooper = TestableLooper.get(this); + + when(mHost.getContext()).thenReturn(mContext); + when(mHost.getUserContext()).thenReturn(mContext); + + mSecureSettings = new FakeSettings(); + mController = new RotationLockControllerImpl(mContext, mSecureSettings); + + mLockTile = new RotationLockTile( + mHost, + mTestableLooper.getLooper(), + new Handler(mTestableLooper.getLooper()), + new FalsingManagerFake(), + mMetricsLogger, + mStatusBarStateController, + mActivityStarter, + mQSLogger, + mController, + mPrivacyManager, + mBatteryController, + mSecureSettings + ); + + mLockTile.initialize(); + + // We are not setting the mocks to listening, so we trigger a first refresh state to + // set the initial state + mLockTile.refreshState(); + + mTestableLooper.processAllMessages(); + + mContext.setMockPackageManager(mPackageManager); + doReturn(PACKAGE_NAME).when(mPackageManager).getRotationResolverPackageName(); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager).checkPermission( + Manifest.permission.CAMERA, PACKAGE_NAME); + + when(mBatteryController.isPowerSave()).thenReturn(false); + when(mPrivacyManager.isSensorPrivacyEnabled(CAMERA)).thenReturn(false); + enableAutoRotation(); + enableCameraBasedRotation(); + + mLockTile.refreshState(); + mTestableLooper.processAllMessages(); + } + + @Test + public void testSecondaryString_cameraRotateOn_returnsFaceBased() { + assertEquals("On - Face-based", mLockTile.getState().secondaryLabel); + } + + @Test + public void testSecondaryString_rotateOff_isEmpty() { + disableAutoRotation(); + + mLockTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel)); + } + + @Test + public void testSecondaryString_cameraRotateOff_isEmpty() { + disableCameraBasedRotation(); + + mLockTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel)); + } + + @Test + public void testSecondaryString_powerSaveEnabled_isEmpty() { + when(mBatteryController.isPowerSave()).thenReturn(true); + + mLockTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel)); + } + + @Test + public void testSecondaryString_cameraDisabled_isEmpty() { + when(mPrivacyManager.isSensorPrivacyEnabled(CAMERA)).thenReturn(true); + + mLockTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel)); + } + + @Test + public void testSecondaryString_noCameraPermission_isEmpty() { + doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission( + Manifest.permission.CAMERA, PACKAGE_NAME); + + mLockTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel)); + } + + private void enableAutoRotation() { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.ACCELEROMETER_ROTATION, 1, UserHandle.USER_CURRENT); + } + + private void disableAutoRotation() { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.ACCELEROMETER_ROTATION, 0, UserHandle.USER_CURRENT); + } + + private void enableCameraBasedRotation() { + mSecureSettings.putIntForUser( + CAMERA_AUTOROTATE, 1, UserHandle.USER_CURRENT); + } + + private void disableCameraBasedRotation() { + mSecureSettings.putIntForUser( + CAMERA_AUTOROTATE, 0, UserHandle.USER_CURRENT); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java index be110242a3eb..4f9cb35db1a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java @@ -51,6 +51,11 @@ public class FakeRotationLockController extends BaseLeakChecker<RotationLockCont } @Override + public boolean isCameraRotationEnabled() { + return false; + } + + @Override public void setRotationLockedAtAngle(boolean locked, int rotation) { } |