diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f3066ce33ff..ee11802232f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -954,7 +954,7 @@ + android:value="@string/rotate_settings_class" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index 83029f49cd5..5c651b90c62 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -97,6 +97,15 @@ Make larger + + Use Auto-Rotate + + + + Face based Autorotate uses the front camera to see if and how someone is looking at the screen. It allows + for reading while lying down and images are never stored or sent to Google.<br><br> + <a href="http://support.google.com/mobile?p=telephony_rtt">Learn more</a> + Sample text @@ -2704,8 +2713,18 @@ Display + + com.android.settings.DisplaySettings Auto-rotate screen + + Off + + On + + On - Face-based + + Enable Face Detection Colors diff --git a/res/xml/auto_rotate_settings.xml b/res/xml/auto_rotate_settings.xml new file mode 100644 index 00000000000..dfa31f71aaa --- /dev/null +++ b/res/xml/auto_rotate_settings.xml @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 0e0d3eba09d..245389ea280 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -83,6 +83,7 @@ import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment; import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionSettings; import com.android.settings.deviceinfo.legal.ModuleLicensesDashboard; import com.android.settings.display.NightDisplaySettings; +import com.android.settings.display.SmartAutoRotatePreferenceFragment; import com.android.settings.display.darkmode.DarkModeSettingsFragment; import com.android.settings.dream.DreamSettings; import com.android.settings.enterprise.EnterprisePrivacySettings; @@ -176,6 +177,7 @@ public class SettingsGateway { SavedAccessPointsWifiSettings2.class.getName(), AllInOneTetherSettings.class.getName(), TetherSettings.class.getName(), + SmartAutoRotatePreferenceFragment.class.getName(), WifiP2pSettings.class.getName(), WifiTetherSettings.class.getName(), BackgroundCheckSummary.class.getName(), diff --git a/src/com/android/settings/display/AutoRotateSwitchBarController.java b/src/com/android/settings/display/AutoRotateSwitchBarController.java new file mode 100644 index 00000000000..0931a10840c --- /dev/null +++ b/src/com/android/settings/display/AutoRotateSwitchBarController.java @@ -0,0 +1,102 @@ +/* + * 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 android.app.settings.SettingsEnums; +import android.content.Context; +import android.widget.Switch; + +import com.android.internal.view.RotationPolicy; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settingslib.widget.OnMainSwitchChangeListener; + +/** + * The switch controller for the location. + */ +public class AutoRotateSwitchBarController implements OnMainSwitchChangeListener, + LifecycleObserver, OnStart, OnStop { + + private final SettingsMainSwitchBar mSwitchBar; + private final Context mContext; + private boolean mValidListener; + private final MetricsFeatureProvider mMetricsFeatureProvider; + + public AutoRotateSwitchBarController(Context context, SettingsMainSwitchBar switchBar, + Lifecycle lifecycle) { + mSwitchBar = switchBar; + mContext = context; + mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @Override + public void onStart() { + if (!mValidListener) { + mSwitchBar.addOnSwitchChangeListener(this); + mValidListener = true; + } + onChange(); + } + + @Override + public void onStop() { + if (mValidListener) { + mSwitchBar.removeOnSwitchChangeListener(this); + mValidListener = false; + } + } + + /** + * Listens to the state change of the rotation primary switch. + */ + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + setRotationLock(isChecked); + } + + + protected void onChange() { + final boolean isEnabled = !RotationPolicy.isRotationLocked(mContext); + if (isEnabled != mSwitchBar.isChecked()) { + // set listener to null so that that code below doesn't trigger onCheckedChanged() + if (mValidListener) { + mSwitchBar.removeOnSwitchChangeListener(this); + } + mSwitchBar.setChecked(isEnabled); + if (mValidListener) { + mSwitchBar.addOnSwitchChangeListener(this); + } + } + } + + private boolean setRotationLock(boolean isChecked) { + final boolean isLocked = !isChecked; + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ROTATION_LOCK, isLocked); + RotationPolicy.setRotationLock(mContext, isLocked); + return true; + } + +} + diff --git a/src/com/android/settings/display/SmartAutoRotateController.java b/src/com/android/settings/display/SmartAutoRotateController.java new file mode 100644 index 00000000000..228d0c7b177 --- /dev/null +++ b/src/com/android/settings/display/SmartAutoRotateController.java @@ -0,0 +1,65 @@ +/* + * 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 android.provider.Settings.Secure.CAMERA_AUTOROTATE; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.Settings; + +import androidx.preference.Preference; + +import com.android.internal.view.RotationPolicy; +import com.android.settings.core.TogglePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +/** + * SmartAutoRotateController controls whether auto rotation is enabled + */ +public class SmartAutoRotateController extends TogglePreferenceController implements + Preference.OnPreferenceChangeListener { + + private final MetricsFeatureProvider mMetricsFeatureProvider; + + public SmartAutoRotateController(Context context, String preferenceKey) { + super(context, preferenceKey); + mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + } + + @Override + public int getAvailabilityStatus() { + return !RotationPolicy.isRotationLocked(mContext) + ? AVAILABLE : DISABLED_DEPENDENT_SETTING; + } + + @Override + public boolean isChecked() { + return Settings.Secure.getInt(mContext.getContentResolver(), + CAMERA_AUTOROTATE, 0) == 1; + } + + @Override + public boolean setChecked(boolean isChecked) { + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_CAMERA_ROTATE_TOGGLE, + isChecked); + Settings.Secure.putInt(mContext.getContentResolver(), + CAMERA_AUTOROTATE, + isChecked ? 1 : 0); + return true; + } +} diff --git a/src/com/android/settings/display/SmartAutoRotatePreferenceController.java b/src/com/android/settings/display/SmartAutoRotatePreferenceController.java new file mode 100644 index 00000000000..01c8379f5ec --- /dev/null +++ b/src/com/android/settings/display/SmartAutoRotatePreferenceController.java @@ -0,0 +1,70 @@ +/* + * 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 android.provider.Settings.Secure.CAMERA_AUTOROTATE; + +import android.content.Context; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import androidx.preference.Preference; + +import com.android.internal.view.RotationPolicy; +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +/** + * SmartAutoRotatePreferenceController provides auto rotate summary in display settings + */ +public class SmartAutoRotatePreferenceController extends BasePreferenceController { + + private static final String TAG = "SmartAutoRotatePreferenceController"; + + public SmartAutoRotatePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return RotationPolicy.isRotationLockToggleVisible(mContext) + ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + protected void update(Preference preference) { + refreshSummary(preference); + } + + @Override + public CharSequence getSummary() { + int activeStringId = R.string.auto_rotate_option_off; + if (!RotationPolicy.isRotationLocked(mContext)) { + try { + final int cameraRotate = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + CAMERA_AUTOROTATE, + UserHandle.USER_CURRENT); + activeStringId = cameraRotate == 1 ? R.string.auto_rotate_option_face_based + : R.string.auto_rotate_option_on; + } catch (Settings.SettingNotFoundException e) { + Log.w(TAG, "CAMERA_AUTOROTATE setting not found", e); + } + } + return mContext.getString(activeStringId); + } +} diff --git a/src/com/android/settings/display/SmartAutoRotatePreferenceFragment.java b/src/com/android/settings/display/SmartAutoRotatePreferenceFragment.java new file mode 100644 index 00000000000..205317d6150 --- /dev/null +++ b/src/com/android/settings/display/SmartAutoRotatePreferenceFragment.java @@ -0,0 +1,147 @@ +/* + * 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 android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.preference.Preference; + +import com.android.internal.view.RotationPolicy; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.Indexable; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Preference fragment used to auto rotation + */ +@SuppressWarnings("WeakerAccess") +@SearchIndexable +public class SmartAutoRotatePreferenceFragment extends DashboardFragment { + + private static final String TAG = "SmartAutoRotatePreferenceFragment"; + + private RotationPolicy.RotationPolicyListener mRotationPolicyListener; + private AutoRotateSwitchBarController mSwitchBarController; + private static final String FACE_SWITCH_PREFERENCE_ID = "face_based_rotate"; + private static final String SMART_AUTO_ROTATE_CONTROLLER_KEY = "auto_rotate"; + + @Override + protected int getPreferenceScreenResId() { + return R.xml.auto_rotate_settings; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + final SettingsActivity activity = (SettingsActivity) getActivity(); + final SettingsMainSwitchBar switchBar = activity.getSwitchBar(); + switchBar.setTitle( + getContext().getString(R.string.auto_rotate_settings_primary_switch_title)); + switchBar.show(); + mSwitchBarController = new AutoRotateSwitchBarController(activity, switchBar, + getSettingsLifecycle()); + return view; + } + + @Override + public void onResume() { + super.onResume(); + if (mRotationPolicyListener == null) { + mRotationPolicyListener = new RotationPolicy.RotationPolicyListener() { + @Override + public void onChange() { + mSwitchBarController.onChange(); + final boolean isLocked = RotationPolicy.isRotationLocked(getContext()); + final Preference preference = findPreference(FACE_SWITCH_PREFERENCE_ID); + if (preference != null) { + preference.setEnabled(!isLocked); + } + } + }; + } + RotationPolicy.registerRotationPolicyListener(getPrefContext(), + mRotationPolicyListener); + + findPreference(FooterPreference.KEY_FOOTER).setTitle( + Html.fromHtml(getString(R.string.smart_rotate_text_headline), + Html.FROM_HTML_MODE_COMPACT)); + } + + + @Override + public void onPause() { + super.onPause(); + + if (mRotationPolicyListener != null) { + RotationPolicy.unregisterRotationPolicyListener(getPrefContext(), + mRotationPolicyListener); + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DISPLAY_AUTO_ROTATE_SETTINGS; + } + + @Override + protected List createPreferenceControllers(Context context) { + return buildPreferenceControllers(context); + } + + private static List buildPreferenceControllers( + Context context) { + final List controllers = new ArrayList<>(); + controllers.add( + new SmartAutoRotatePreferenceController(context, SMART_AUTO_ROTATE_CONTROLLER_KEY)); + return controllers; + } + + @Override + protected String getLogTag() { + return TAG; + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.auto_rotate_settings) { + + @Override + public List createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context); + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return false; + } + }; +} diff --git a/tests/robotests/src/com/android/settings/display/SmartAutoRotatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/SmartAutoRotatePreferenceControllerTest.java new file mode 100644 index 00000000000..b65785f5085 --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/SmartAutoRotatePreferenceControllerTest.java @@ -0,0 +1,173 @@ +/* + * 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 android.provider.Settings.Secure.CAMERA_AUTOROTATE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.testutils.FakeFeatureFactory; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class SmartAutoRotatePreferenceControllerTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mContext; + @Mock + private PackageManager mPackageManager; + private ContentResolver mContentResolver; + private SmartAutoRotatePreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + FakeFeatureFactory.setupForTest(); + mContentResolver = RuntimeEnvironment.application.getContentResolver(); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + when(mContext.getString(R.string.auto_rotate_option_off)) + .thenReturn("Off"); + when(mContext.getString(R.string.auto_rotate_option_on)) + .thenReturn("On"); + when(mContext.getString(R.string.auto_rotate_option_face_based)) + .thenReturn("On - Face-based"); + + disableCameraBasedRotation(); + + mController = new SmartAutoRotatePreferenceController(mContext, "smart_auto_rotate"); + } + + @Test + public void isAvailableWhenPolicyAllows() { + assertThat(mController.isAvailable()).isFalse(); + + enableAutoRotationPreference(); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void updatePreference_settingsIsOff_shouldTurnOffToggle() { + disableAutoRotation(); + + assertThat(mController.getSummary()).isEqualTo("Off"); + } + + @Test + public void updatePreference_settingsIsOn_shouldTurnOnToggle() { + enableAutoRotation(); + + assertThat(mController.getSummary()).isEqualTo("On"); + } + + @Test + public void updatePreference_settingsIsCameraBased_shouldTurnOnToggle() { + enableCameraBasedRotation(); + enableAutoRotation(); + + assertThat(mController.getSummary()).isEqualTo("On - Face-based"); + + disableAutoRotation(); + + assertThat(mController.getSummary()).isEqualTo("Off"); + } + + @Test + public void testGetAvailabilityStatus() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(BasePreferenceController + .UNSUPPORTED_ON_DEVICE); + + enableAutoRotationPreference(); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(BasePreferenceController + .AVAILABLE); + + disableAutoRotationPreference(); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(BasePreferenceController + .UNSUPPORTED_ON_DEVICE); + } + + @Test + public void isSliceableCorrectKey_returnsTrue() { + final AutoRotatePreferenceController controller = + new AutoRotatePreferenceController(mContext, "auto_rotate"); + assertThat(controller.isSliceable()).isTrue(); + } + + @Test + public void isSliceableIncorrectKey_returnsFalse() { + final AutoRotatePreferenceController controller = + new AutoRotatePreferenceController(mContext, "bad_key"); + assertThat(controller.isSliceable()).isFalse(); + } + + private void enableAutoRotationPreference() { + when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true); + when(mContext.getResources().getBoolean(anyInt())).thenReturn(true); + Settings.System.putInt(mContentResolver, + Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0); + } + + private void disableAutoRotationPreference() { + when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true); + when(mContext.getResources().getBoolean(anyInt())).thenReturn(true); + Settings.System.putInt(mContentResolver, + Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 1); + } + + private void enableAutoRotation() { + Settings.System.putIntForUser(mContentResolver, + Settings.System.ACCELEROMETER_ROTATION, 1, UserHandle.USER_CURRENT); + } + + private void disableAutoRotation() { + Settings.System.putIntForUser(mContentResolver, + Settings.System.ACCELEROMETER_ROTATION, 0, UserHandle.USER_CURRENT); + } + + private void enableCameraBasedRotation() { + Settings.Secure.putIntForUser(mContentResolver, + CAMERA_AUTOROTATE, 1, UserHandle.USER_CURRENT); + } + + private void disableCameraBasedRotation() { + Settings.Secure.putIntForUser(mContentResolver, + CAMERA_AUTOROTATE, 0, UserHandle.USER_CURRENT); + } +}