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 266e91ab4a563..0144b5ad90bfa 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);
+ }
+}