From 2c0663891241784105f0b7481bbe14eb442397e8 Mon Sep 17 00:00:00 2001 From: Jason Chang Date: Thu, 21 Oct 2021 15:53:31 +0800 Subject: [PATCH] (1/n) Implement One-handed mode Quick Settings tile Integrate One-handed mode Quick Settings tile with One-handed mode SettingsProvider changes. Bug: 201743873 Bug: 202132480 Test: manually test One-handed mode quick settings tile actions. Test: atest SystemUITests Change-Id: Ib624ad3196f534dcd07467a8d47ea5ee413d0cd5 --- core/java/android/content/Intent.java | 9 ++ .../res/drawable/ic_qs_one_handed_mode.xml | 26 +++ core/res/res/values/symbols.xml | 1 + .../android/wm/shell/onehanded/OneHanded.java | 4 + packages/SystemUI/res/values/config.xml | 2 +- packages/SystemUI/res/values/strings.xml | 3 + .../res/values/tiles_states_strings.xml | 7 + .../systemui/qs/tileimpl/QSFactoryImpl.java | 8 +- .../systemui/qs/tileimpl/QSTileViewImpl.kt | 3 +- .../systemui/qs/tiles/OneHandedModeTile.java | 149 ++++++++++++++++++ .../qs/tiles/OneHandedModeTileTest.java | 112 +++++++++++++ 11 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 core/res/res/drawable/ic_qs_one_handed_mode.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 3c9b1f81da8ad..fccb8a7851668 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1563,6 +1563,15 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_POWER_USAGE_SUMMARY = "android.intent.action.POWER_USAGE_SUMMARY"; + /** + * Activity Action: Show one-handed mode settings to the user. + *

Input: Nothing. + *

Output: Nothing. + * @hide + */ + public static final String ACTION_ONE_HANDED_SETTINGS = + "android.intent.action.ONE_HANDED_SETTINGS"; + /** * Activity Action: Setup wizard action provided for OTA provisioning to determine if it needs * to run. diff --git a/core/res/res/drawable/ic_qs_one_handed_mode.xml b/core/res/res/drawable/ic_qs_one_handed_mode.xml new file mode 100644 index 0000000000000..18e56187efb33 --- /dev/null +++ b/core/res/res/drawable/ic_qs_one_handed_mode.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b0e4b1ff93d19..baa30e8f0792e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1438,6 +1438,7 @@ + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java index 3253bb06c835e..b00182f36cc83 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -17,6 +17,7 @@ package com.android.wm.shell.onehanded; import android.content.res.Configuration; +import android.os.SystemProperties; import com.android.wm.shell.common.annotations.ExternalThread; @@ -26,6 +27,9 @@ import com.android.wm.shell.common.annotations.ExternalThread; @ExternalThread public interface OneHanded { + boolean sIsSupportOneHandedMode = SystemProperties.getBoolean( + OneHandedController.SUPPORT_ONE_HANDED_MODE, false); + /** * Returns a binder that can be passed to an external process to manipulate OneHanded. */ diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 3ed363fa30584..56464e46c3921 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -82,7 +82,7 @@ - internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner + internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 937cbed634b69..21b4a4257cf8c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -722,6 +722,9 @@ Stop + + One-handed mode + Unblock device microphone? diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml index 2d51cbe34cb98..ed29bc76b3508 100644 --- a/packages/SystemUI/res/values/tiles_states_strings.xml +++ b/packages/SystemUI/res/values/tiles_states_strings.xml @@ -291,4 +291,11 @@ Off On + + + + Unavailable + Off + On + \ No newline at end of file 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 3e850ab84b0b5..16be99827afb3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -43,6 +43,7 @@ import com.android.systemui.qs.tiles.LocationTile; import com.android.systemui.qs.tiles.MicrophoneToggleTile; import com.android.systemui.qs.tiles.NfcTile; import com.android.systemui.qs.tiles.NightDisplayTile; +import com.android.systemui.qs.tiles.OneHandedModeTile; import com.android.systemui.qs.tiles.QRCodeScannerTile; import com.android.systemui.qs.tiles.QuickAccessWalletTile; import com.android.systemui.qs.tiles.ReduceBrightColorsTile; @@ -92,6 +93,7 @@ public class QSFactoryImpl implements QSFactory { private final Provider mAlarmTileProvider; private final Provider mQuickAccessWalletTileProvider; private final Provider mQRCodeScannerTileProvider; + private final Provider mOneHandedModeTileProvider; private final Lazy mQsHostLazy; private final Provider mCustomTileBuilderProvider; @@ -127,7 +129,8 @@ public class QSFactoryImpl implements QSFactory { Provider deviceControlsTileProvider, Provider alarmTileProvider, Provider quickAccessWalletTileProvider, - Provider qrCodeScannerTileProvider) { + Provider qrCodeScannerTileProvider, + Provider oneHandedModeTileProvider) { mQsHostLazy = qsHostLazy; mCustomTileBuilderProvider = customTileBuilderProvider; @@ -159,6 +162,7 @@ public class QSFactoryImpl implements QSFactory { mAlarmTileProvider = alarmTileProvider; mQuickAccessWalletTileProvider = quickAccessWalletTileProvider; mQRCodeScannerTileProvider = qrCodeScannerTileProvider; + mOneHandedModeTileProvider = oneHandedModeTileProvider; } public QSTile createTile(String tileSpec) { @@ -227,6 +231,8 @@ public class QSFactoryImpl implements QSFactory { return mQuickAccessWalletTileProvider.get(); case "qr_code_scanner": return mQRCodeScannerTileProvider.get(); + case "onehanded": + return mOneHandedModeTileProvider.get(); } // Custom tiles diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 25ed2e776f008..4bdeb567074ee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -648,7 +648,8 @@ internal object SubtitleArrayMapping { "controls" to R.array.tile_states_controls, "wallet" to R.array.tile_states_wallet, "qr_code_scanner" to R.array.tile_states_qr_code_scanner, - "alarm" to R.array.tile_states_alarm + "alarm" to R.array.tile_states_alarm, + "onehanded" to R.array.tile_states_onehanded ) fun getSubtitleId(spec: String?): Int { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java new file mode 100644 index 0000000000000..c8d2cdc1e4784 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java @@ -0,0 +1,149 @@ +/* + * 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 android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.service.quicksettings.Tile; +import android.view.View; +import android.widget.Switch; + +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +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.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; +import com.android.wm.shell.onehanded.OneHanded; + +import javax.inject.Inject; + +/** Quick settings tile: One-handed mode **/ +public class OneHandedModeTile extends QSTileImpl { + private final Icon mIcon = ResourceIcon.get( + com.android.internal.R.drawable.ic_qs_one_handed_mode); + private final SecureSetting mSetting; + + @Inject + public OneHandedModeTile( + QSHost host, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + UserTracker userTracker, + SecureSettings secureSettings) { + super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mSetting = new SecureSetting(secureSettings, mHandler, + Settings.Secure.ONE_HANDED_MODE_ENABLED, userTracker.getUserId()) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + // mHandler is the background handler so calling this is OK + handleRefreshState(value); + } + }; + } + + @Override + public boolean isAvailable() { + return isSupportOneHandedMode(); + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + mSetting.setListening(false); + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + super.handleSetListening(listening); + mSetting.setListening(listening); + } + + @Override + protected void handleUserSwitch(int newUserId) { + mSetting.setUserId(newUserId); + handleRefreshState(mSetting.getValue()); + } + + @Override + public Intent getLongClickIntent() { + return new Intent(Intent.ACTION_ONE_HANDED_SETTINGS); + } + + @Override + protected void handleClick(@Nullable View view) { + mSetting.setValue(mState.value ? 0 : 1); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_onehanded_label); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final int value = arg instanceof Integer ? (Integer) arg : mSetting.getValue(); + final boolean enabled = value != 0; + state.value = enabled; + state.label = mContext.getString(R.string.quick_settings_onehanded_label); + state.icon = mIcon; + if (state.slash == null) { + state.slash = new SlashState(); + } + state.slash.isSlashed = !state.value; + state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + state.contentDescription = state.label; + state.expandedAccessibilityClassName = Switch.class.getName(); + } + + @Override + public int getMetricsCategory() { + // MetricsProto/MetricsEvent is deprecated, and QSTileImpl has code to log events to Tron, + // as well as UiEventLogger, so just simply return 0 here. + return 0; + } + + @VisibleForTesting + public boolean isSupportOneHandedMode() { + return OneHanded.sIsSupportOneHandedMode; + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java new file mode 100644 index 0000000000000..803187545a181 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java @@ -0,0 +1,112 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.os.Handler; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +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.settings.UserTracker; +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 OneHandedModeTileTest extends SysuiTestCase { + + private final String mOneHandedTitle = "One-handed mode"; + + @Mock + private ActivityStarter mActivityStarter; + @Mock + private QSTileHost mHost; + @Mock + private MetricsLogger mMetricsLogger; + @Mock + private StatusBarStateController mStatusBarStateController; + @Mock + private QSLogger mQSLogger; + @Mock + private UserTracker mUserTracker; + @Mock + private SecureSettings mSecureSettings; + + private TestableLooper mTestableLooper; + private OneHandedModeTile mTile; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTestableLooper = TestableLooper.get(this); + + when(mHost.getContext()).thenReturn(mContext); + + mTile = spy(new OneHandedModeTile( + mHost, + mTestableLooper.getLooper(), + new Handler(mTestableLooper.getLooper()), + new FalsingManagerFake(), + mMetricsLogger, + mStatusBarStateController, + mActivityStarter, + mQSLogger, + mUserTracker, + mSecureSettings)); + + mTestableLooper.processAllMessages(); + mTile.initialize(); + } + + @Test + public void testIsAvailable_unsupportOneHandedProperty_shouldReturnsFalse() { + when(mTile.isSupportOneHandedMode()).thenReturn(false); + + assertThat(mTile.isAvailable()).isFalse(); + } + + @Test + public void testIsAvailable_supportOneHandedProperty_shouldReturnsTrue() { + when(mTile.isSupportOneHandedMode()).thenReturn(true); + + assertThat(mTile.isAvailable()).isTrue(); + } + + @Test + public void testGetTileLabel_shouldReturnOneHandedModeTitle() { + assertThat(mTile.getTileLabel()).isEqualTo(mOneHandedTitle); + } +}