Update QS rotation tile text to display when smart-auto-rotate is enabled

Bug: 196079067

Test: locally with flame & atest RotationLockTileTest
Change-Id: Ie6bbbb65b14b37a1785baeb1d34345d19984bf4e
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 99508a5..f0b4044 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1277,6 +1277,10 @@
 
     <!-- [CHAR LIMIT=NONE] Importance Tuner setting title -->
     <string name="tuner_full_importance_settings">Power notification controls</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 0bbb5bd..082dc5c 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,18 @@
 
 package com.android.systemui.qs.tiles;
 
+import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
+
+import static com.android.systemui.statusbar.policy.RotationLockControllerImpl.hasSufficientPermission;
+
 import android.content.Intent;
 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 +44,25 @@
 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.SettingObserver;
 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 SettingObserver mSetting;
 
     @Inject
     public RotationLockTile(
@@ -61,12 +74,41 @@
             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;
+        int currentUser = host.getUserContext().getUserId();
+        mSetting = new SettingObserver(
+                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(null);
+            }
+        };
+        mBatteryController.observe(getLifecycle(), this);
+    }
+
+    @Override
+    protected void handleInitialize() {
+        mPrivacyManager.addSensorPrivacyListener(CAMERA, mSensorPrivacyChangedListener);
+    }
+
+    @Override
+    public void onPowerSaveChanged(boolean isPowerSave) {
+        refreshState();
     }
 
     @Override
@@ -95,14 +137,46 @@
     protected void handleUpdateState(BooleanState state, Object arg) {
         final boolean rotationLocked = mController.isRotationLocked();
 
+        final boolean powerSave = mBatteryController.isPowerSave();
+        final boolean cameraLocked = mPrivacyManager.isSensorPrivacyEnabled(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.stateDescription = state.secondaryLabel;
+
         state.expandedAccessibilityClassName = Switch.class.getName();
         state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
     }
 
+    @Override
+    protected void handleDestroy() {
+        super.handleDestroy();
+        mSetting.setListening(false);
+        mPrivacyManager.removeSensorPrivacyListener(CAMERA, mSensorPrivacyChangedListener);
+    }
+
+    @Override
+    public void handleSetListening(boolean listening) {
+        super.handleSetListening(listening);
+        mSetting.setListening(listening);
+    }
+
+    @Override
+    protected void handleUserSwitch(int newUserId) {
+        mSetting.setUserId(newUserId);
+        handleRefreshState(null);
+    }
+
     public static boolean isCurrentOrientationLockPortrait(RotationLockController controller,
             Resources resources) {
         int lockOrientation = controller.getRotationLockOrientation();
@@ -140,4 +214,8 @@
             refreshState(rotationLocked);
         }
     };
+
+    private final SensorPrivacyManager.OnSensorPrivacyChangedListener
+            mSensorPrivacyChangedListener =
+            (sensor, enabled) -> refreshState();
 }
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 f258fb1..1158324 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 @@
     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 67f5364..3143a47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
@@ -18,6 +18,9 @@
 
 import static com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule.DEVICE_STATE_ROTATION_LOCK_DEFAULTS;
 
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
@@ -34,6 +37,7 @@
 /** Platform implementation of the rotation lock controller. **/
 @SysUISingleton
 public final class RotationLockControllerImpl implements RotationLockController {
+
     private final CopyOnWriteArrayList<RotationLockControllerCallback> mCallbacks =
             new CopyOnWriteArrayList<>();
 
@@ -86,11 +90,15 @@
         return mRotationPolicy.isRotationLocked();
     }
 
+    public boolean isCameraRotationEnabled() {
+        return mRotationPolicy.isCameraRotationEnabled();
+    }
+
     public void setRotationLocked(boolean locked) {
         mRotationPolicy.setRotationLock(locked);
     }
 
-    public void setRotationLockedAtAngle(boolean locked, int rotation){
+    public void setRotationLockedAtAngle(boolean locked, int rotation) {
         mRotationPolicy.setRotationLockAtAngle(locked, rotation);
     }
 
@@ -121,4 +129,11 @@
         callback.onRotationLockStateChanged(mRotationPolicy.isRotationLocked(),
                 mRotationPolicy.isRotationLockToggleVisible());
     }
+
+    public static 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/util/wrapper/RotationPolicyWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
index 2a0cc7d..b64d7be 100644
--- a/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
@@ -17,8 +17,10 @@
 package com.android.systemui.util.wrapper
 
 import android.content.Context
+import android.provider.Settings.Secure.CAMERA_AUTOROTATE
 import com.android.internal.view.RotationPolicy
 import com.android.internal.view.RotationPolicy.RotationPolicyListener
+import com.android.systemui.util.settings.SecureSettings
 import javax.inject.Inject
 
 /**
@@ -30,12 +32,16 @@
     fun getRotationLockOrientation(): Int
     fun isRotationLockToggleVisible(): Boolean
     fun isRotationLocked(): Boolean
+    fun isCameraRotationEnabled(): Boolean
     fun registerRotationPolicyListener(listener: RotationPolicyListener, userHandle: Int)
     fun unregisterRotationPolicyListener(listener: RotationPolicyListener)
 }
 
-class RotationPolicyWrapperImpl @Inject constructor(private val context: Context) :
-    RotationPolicyWrapper {
+class RotationPolicyWrapperImpl @Inject constructor(
+    private val context: Context,
+    private val secureSettings: SecureSettings
+) :
+        RotationPolicyWrapper {
 
     override fun setRotationLock(enabled: Boolean) {
         RotationPolicy.setRotationLock(context, enabled)
@@ -54,6 +60,9 @@
     override fun isRotationLocked(): Boolean =
         RotationPolicy.isRotationLocked(context)
 
+    override fun isCameraRotationEnabled(): Boolean =
+            secureSettings.getInt(CAMERA_AUTOROTATE, 0) == 1
+
     override fun registerRotationPolicyListener(
         listener: RotationPolicyListener,
         userHandle: Int
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 0000000..55c51b2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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 junit.framework.TestCase.assertEquals;
+
+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.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.R;
+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.DeviceStateRotationLockSettingController;
+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.wrapper.RotationPolicyWrapper;
+
+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";
+    private static final String[] DEFAULT_SETTINGS = new String[]{
+            "0:0",
+            "1:2"
+    };
+
+    @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;
+    @Mock
+    DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController;
+    @Mock
+    RotationPolicyWrapper mRotationPolicyWrapper;
+
+    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);
+
+        mController = new RotationLockControllerImpl(mRotationPolicyWrapper,
+                mDeviceStateRotationLockSettingController, DEFAULT_SETTINGS);
+
+        mLockTile = new RotationLockTile(
+                mHost,
+                mTestableLooper.getLooper(),
+                new Handler(mTestableLooper.getLooper()),
+                new FalsingManagerFake(),
+                mMetricsLogger,
+                mStatusBarStateController,
+                mActivityStarter,
+                mQSLogger,
+                mController,
+                mPrivacyManager,
+                mBatteryController,
+                new FakeSettings()
+        );
+
+        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(mContext.getString(R.string.rotation_lock_camera_rotation_on),
+                mLockTile.getState().secondaryLabel.toString());
+    }
+
+    @Test
+    public void testSecondaryString_rotateOff_isEmpty() {
+        disableAutoRotation();
+
+        mLockTile.refreshState();
+        mTestableLooper.processAllMessages();
+
+        assertEquals("", mLockTile.getState().secondaryLabel.toString());
+    }
+
+    @Test
+    public void testSecondaryString_cameraRotateOff_isEmpty() {
+        disableCameraBasedRotation();
+
+        mLockTile.refreshState();
+        mTestableLooper.processAllMessages();
+
+        assertEquals("", mLockTile.getState().secondaryLabel.toString());
+    }
+
+    @Test
+    public void testSecondaryString_powerSaveEnabled_isEmpty() {
+        when(mBatteryController.isPowerSave()).thenReturn(true);
+
+        mLockTile.refreshState();
+        mTestableLooper.processAllMessages();
+
+        assertEquals("", mLockTile.getState().secondaryLabel.toString());
+    }
+
+    @Test
+    public void testSecondaryString_cameraDisabled_isEmpty() {
+        when(mPrivacyManager.isSensorPrivacyEnabled(CAMERA)).thenReturn(true);
+
+        mLockTile.refreshState();
+        mTestableLooper.processAllMessages();
+
+        assertEquals("", mLockTile.getState().secondaryLabel.toString());
+    }
+
+    @Test
+    public void testSecondaryString_noCameraPermission_isEmpty() {
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                Manifest.permission.CAMERA, PACKAGE_NAME);
+
+        mLockTile.refreshState();
+        mTestableLooper.processAllMessages();
+
+        assertEquals("", mLockTile.getState().secondaryLabel.toString());
+    }
+
+    private void enableAutoRotation() {
+        when(mRotationPolicyWrapper.isRotationLocked()).thenReturn(false);
+    }
+
+    private void disableAutoRotation() {
+        when(mRotationPolicyWrapper.isRotationLocked()).thenReturn(true);
+    }
+
+    private void enableCameraBasedRotation() {
+        when(mRotationPolicyWrapper.isCameraRotationEnabled()).thenReturn(true);
+    }
+
+    private void disableCameraBasedRotation() {
+        when(mRotationPolicyWrapper.isCameraRotationEnabled()).thenReturn(false);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
index 30717f4..db7b2f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
@@ -225,6 +225,11 @@
         }
 
         @Override
+        public boolean isCameraRotationEnabled() {
+            throw new AssertionError("Not implemented");
+        }
+
+        @Override
         public void registerRotationPolicyListener(RotationPolicy.RotationPolicyListener listener,
                 int userHandle) {
             throw new AssertionError("Not implemented");
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 be11024..4f9cb35 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 @@
     }
 
     @Override
+    public boolean isCameraRotationEnabled() {
+        return false;
+    }
+
+    @Override
     public void setRotationLockedAtAngle(boolean locked, int rotation) {
 
     }