diff --git a/res/values/strings.xml b/res/values/strings.xml index ed9b5059177..69dc31a5db9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2944,6 +2944,10 @@ Turn on screen attention Keep screen on when looking at it + + Camera is locked + + Camera must be unlocked for Face Detection Camera access is required for Face Detection. Tap to manage permissions for Device Personalization Services diff --git a/res/xml/auto_rotate_settings.xml b/res/xml/auto_rotate_settings.xml index 7c46d29a544..bc47b81d0be 100644 --- a/res/xml/auto_rotate_settings.xml +++ b/res/xml/auto_rotate_settings.xml @@ -26,6 +26,12 @@ android:summary="@string/auto_rotate_summary_no_permission" settings:controller="com.android.settings.display.SmartAutoRotatePermissionController" /> + + { + mPreference.setVisible(enabled); + updateState(mPreference); + }); + } + + @VisibleForTesting + boolean isCameraLocked() { + return mPrivacyManager.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + ((BannerMessagePreference) mPreference) + .setPositiveButtonText(R.string.allow) + .setPositiveButtonOnClickListener(v -> { + mPrivacyManager.setSensorPrivacy(CAMERA, false); + }); + } + + @Override + @AvailabilityStatus + public int getAvailabilityStatus() { + return isRotationResolverServiceAvailable(mContext) + && isCameraLocked() ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/display/SmartAutoRotateController.java b/src/com/android/settings/display/SmartAutoRotateController.java index ca196ba9c8f..e3b2665195b 100644 --- a/src/com/android/settings/display/SmartAutoRotateController.java +++ b/src/com/android/settings/display/SmartAutoRotateController.java @@ -15,6 +15,7 @@ */ package com.android.settings.display; +import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; import static android.provider.Settings.Secure.CAMERA_AUTOROTATE; import android.Manifest; @@ -23,12 +24,15 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.hardware.SensorPrivacyManager; import android.provider.Settings; import android.service.rotationresolver.RotationResolverService; import android.text.TextUtils; import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.view.RotationPolicy; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -41,10 +45,14 @@ public class SmartAutoRotateController extends TogglePreferenceController implem Preference.OnPreferenceChangeListener { private final MetricsFeatureProvider mMetricsFeatureProvider; + private final SensorPrivacyManager mPrivacyManager; + private Preference mPreference; public SmartAutoRotateController(Context context, String preferenceKey) { super(context, preferenceKey); mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + mPrivacyManager = SensorPrivacyManager.getInstance(context); + mPrivacyManager.addSensorPrivacyListener(CAMERA, enabled -> updateState(mPreference)); } @Override @@ -53,16 +61,39 @@ public class SmartAutoRotateController extends TogglePreferenceController implem return UNSUPPORTED_ON_DEVICE; } return !RotationPolicy.isRotationLocked(mContext) && hasSufficientPermission(mContext) - ? AVAILABLE : DISABLED_DEPENDENT_SETTING; + && !isCameraLocked() ? AVAILABLE : DISABLED_DEPENDENT_SETTING; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (preference != null) { + preference.setEnabled(getAvailabilityStatus() == AVAILABLE); + } + } + + /** + * Need this because all controller tests use RoboElectric. No easy way to mock this service, + * so we mock the call we need + */ + @VisibleForTesting + boolean isCameraLocked() { + return mPrivacyManager.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA); } @Override public boolean isChecked() { - return hasSufficientPermission(mContext) && Settings.Secure.getInt( + return hasSufficientPermission(mContext) && !isCameraLocked() && Settings.Secure.getInt( mContext.getContentResolver(), CAMERA_AUTOROTATE, 0) == 1; } + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + @Override public boolean setChecked(boolean isChecked) { mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_CAMERA_ROTATE_TOGGLE, diff --git a/src/com/android/settings/display/SmartAutoRotatePreferenceFragment.java b/src/com/android/settings/display/SmartAutoRotatePreferenceFragment.java index 130bbd80f17..1a91775c408 100644 --- a/src/com/android/settings/display/SmartAutoRotatePreferenceFragment.java +++ b/src/com/android/settings/display/SmartAutoRotatePreferenceFragment.java @@ -19,6 +19,7 @@ import static com.android.settings.display.SmartAutoRotateController.hasSufficie import static com.android.settings.display.SmartAutoRotateController.isRotationResolverServiceAvailable; import android.app.settings.SettingsEnums; +import android.hardware.SensorPrivacyManager; import android.os.Bundle; import android.text.Html; import android.view.LayoutInflater; @@ -47,6 +48,7 @@ public class SmartAutoRotatePreferenceFragment extends DashboardFragment { private static final String TAG = "SmartAutoRotatePreferenceFragment"; private RotationPolicy.RotationPolicyListener mRotationPolicyListener; + private SensorPrivacyManager mPrivacyManager; private AutoRotateSwitchBarController mSwitchBarController; private static final String FACE_SWITCH_PREFERENCE_ID = "face_based_rotate"; @@ -66,6 +68,7 @@ public class SmartAutoRotatePreferenceFragment extends DashboardFragment { switchBar.show(); mSwitchBarController = new AutoRotateSwitchBarController(activity, switchBar, getSettingsLifecycle()); + mPrivacyManager = SensorPrivacyManager.getInstance(activity); final Preference footerPreference = findPreference(FooterPreference.KEY_FOOTER); if (footerPreference != null) { footerPreference.setTitle(Html.fromHtml(getString(R.string.smart_rotate_text_headline), @@ -84,9 +87,11 @@ public class SmartAutoRotatePreferenceFragment extends DashboardFragment { public void onChange() { mSwitchBarController.onChange(); final boolean isLocked = RotationPolicy.isRotationLocked(getContext()); + final boolean isCameraLocked = mPrivacyManager.isSensorPrivacyEnabled( + SensorPrivacyManager.Sensors.CAMERA); final Preference preference = findPreference(FACE_SWITCH_PREFERENCE_ID); if (preference != null && hasSufficientPermission(getContext())) { - preference.setEnabled(!isLocked); + preference.setEnabled(!isLocked && !isCameraLocked); } } }; diff --git a/tests/robotests/src/com/android/settings/display/SmartAutoRotateCameraStateControllerTest.java b/tests/robotests/src/com/android/settings/display/SmartAutoRotateCameraStateControllerTest.java new file mode 100644 index 00000000000..ffd6411d329 --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/SmartAutoRotateCameraStateControllerTest.java @@ -0,0 +1,82 @@ +/* + * 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.settings.display; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; + +import com.android.settings.testutils.ResolveInfoBuilder; +import com.android.settings.testutils.shadow.ShadowSensorPrivacyManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowSensorPrivacyManager.class) +public class SmartAutoRotateCameraStateControllerTest { + + private static final String PACKAGE_NAME = "package_name"; + + private SmartAutoRotateCameraStateController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + final Context context = Mockito.spy(RuntimeEnvironment.application); + final ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver(); + when(context.getContentResolver()).thenReturn(contentResolver); + final PackageManager packageManager = Mockito.mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + doReturn(PACKAGE_NAME).when(packageManager).getRotationResolverPackageName(); + mController = new SmartAutoRotateCameraStateController(context, "smart_auto_rotate"); + when(mController.isCameraLocked()).thenReturn(false); + + final ResolveInfo resolveInfo = new ResolveInfoBuilder(PACKAGE_NAME).build(); + resolveInfo.serviceInfo = new ServiceInfo(); + when(packageManager.resolveService(any(), anyInt())).thenReturn(resolveInfo); + } + + @Test + public void getAvailabilityStatus_returnUnsupportedOnDevice() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getAvailabilityStatus_cameraNotEnabled_returnAvailableUnSearchAble() { + when(mController.isCameraLocked()).thenReturn(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE); + } +} diff --git a/tests/robotests/src/com/android/settings/display/SmartAutoRotateControllerTest.java b/tests/robotests/src/com/android/settings/display/SmartAutoRotateControllerTest.java index 2d56c0efd88..a65d8806f77 100644 --- a/tests/robotests/src/com/android/settings/display/SmartAutoRotateControllerTest.java +++ b/tests/robotests/src/com/android/settings/display/SmartAutoRotateControllerTest.java @@ -39,6 +39,7 @@ import android.provider.Settings; import androidx.preference.Preference; import com.android.settings.testutils.ResolveInfoBuilder; +import com.android.settings.testutils.shadow.ShadowSensorPrivacyManager; import org.junit.Before; import org.junit.Test; @@ -48,8 +49,10 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowSensorPrivacyManager.class) public class SmartAutoRotateControllerTest { private static final String PACKAGE_NAME = "package_name"; @@ -72,6 +75,7 @@ public class SmartAutoRotateControllerTest { doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager).checkPermission( Manifest.permission.CAMERA, PACKAGE_NAME); mController = new SmartAutoRotateController(context, "test_key"); + when(mController.isCameraLocked()).thenReturn(false); doReturn(mController.getPreferenceKey()).when(mPreference).getKey(); final ResolveInfo resolveInfo = new ResolveInfoBuilder(PACKAGE_NAME).build(); @@ -105,6 +109,12 @@ public class SmartAutoRotateControllerTest { assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING); } + @Test + public void getAvailabilityStatus_cameraDisabled_returnDisableDependentSetting() { + when(mController.isCameraLocked()).thenReturn(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING); + } + private void enableAutoRotation() { Settings.System.putIntForUser(mContentResolver, Settings.System.ACCELEROMETER_ROTATION, 1, UserHandle.USER_CURRENT); diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowSensorPrivacyManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowSensorPrivacyManager.java new file mode 100644 index 00000000000..b15311066ed --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowSensorPrivacyManager.java @@ -0,0 +1,34 @@ +/* + * 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.settings.testutils.shadow; + +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.hardware.SensorPrivacyManager; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(value = SensorPrivacyManager.class) +public class ShadowSensorPrivacyManager { + + @Implementation + public static SensorPrivacyManager getInstance(Context context) { + return mock(SensorPrivacyManager.class); + } +}