diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9b2cf2412a2..ea4f6ffb2ee 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1694,11 +1694,6 @@ android:value="true" /> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable-night/illustration_accessibility_gesture_two_finger.xml b/res/drawable-night/illustration_accessibility_gesture_two_finger.xml new file mode 100644 index 00000000000..c95523b1655 --- /dev/null +++ b/res/drawable-night/illustration_accessibility_gesture_two_finger.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/illustration_accessibility_gesture_three_finger.xml b/res/drawable/illustration_accessibility_gesture_three_finger.xml new file mode 100644 index 00000000000..c14d7443110 --- /dev/null +++ b/res/drawable/illustration_accessibility_gesture_three_finger.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/illustration_accessibility_gesture_two_finger.xml b/res/drawable/illustration_accessibility_gesture_two_finger.xml new file mode 100644 index 00000000000..3ef4fd1db4b --- /dev/null +++ b/res/drawable/illustration_accessibility_gesture_two_finger.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/tutorial_dialog_launch_by_gesture_navigation_settings.xml b/res/layout/tutorial_dialog_launch_by_gesture_navigation_settings.xml index 524f7c9b7e4..b494e9367b2 100644 --- a/res/layout/tutorial_dialog_launch_by_gesture_navigation_settings.xml +++ b/res/layout/tutorial_dialog_launch_by_gesture_navigation_settings.xml @@ -29,11 +29,15 @@ android:orientation="vertical" android:paddingTop="24dp"> - + - + finish()); - } -} diff --git a/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java index 09f28d72c9b..0ca16cb99b0 100644 --- a/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java +++ b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java @@ -32,7 +32,6 @@ import android.text.style.ImageSpan; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; -import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.Window; @@ -77,13 +76,13 @@ public final class AccessibilityGestureNavigationTutorial { @Retention(RetentionPolicy.SOURCE) @IntDef({ DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON, - DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION, + DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE, DialogType.GESTURE_NAVIGATION_SETTINGS, }) private @interface DialogType { int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0; - int LAUNCH_SERVICE_BY_GESTURE_NAVIGATION = 1; + int LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE = 1; int GESTURE_NAVIGATION_SETTINGS = 2; } @@ -92,13 +91,17 @@ public final class AccessibilityGestureNavigationTutorial { private static final DialogInterface.OnClickListener mOnClickListener = (DialogInterface dialog, int which) -> dialog.dismiss(); - public static void showGestureNavigationSettingsTutorialDialog(Context context, - DialogInterface.OnDismissListener dismissListener) { + /** + * Displays a dialog that guides users to use accessibility features with accessibility + * gestures under system gesture navigation mode. + */ + public static void showGestureNavigationTutorialDialog(Context context, + DialogInterface.OnDismissListener onDismissListener) { final AlertDialog alertDialog = new AlertDialog.Builder(context) .setView(createTutorialDialogContentView(context, DialogType.GESTURE_NAVIGATION_SETTINGS)) .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) - .setOnDismissListener(dismissListener) + .setOnDismissListener(onDismissListener) .create(); alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -117,8 +120,8 @@ public final class AccessibilityGestureNavigationTutorial { return alertDialog; } - static AlertDialog showGestureNavigationTutorialDialog(Context context) { - return createDialog(context, DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION); + static AlertDialog showAccessibilityGestureTutorialDialog(Context context) { + return createDialog(context, DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE); } static AlertDialog createAccessibilityTutorialDialog(Context context, int shortcutTypes) { @@ -129,7 +132,7 @@ public final class AccessibilityGestureNavigationTutorial { } /** - * Get a content View for a dialog to confirm that they want to enable a service. + * Gets a content View for a dialog to confirm that they want to enable a service. * * @param context A valid context * @param dialogType The type of tutorial dialog @@ -146,42 +149,36 @@ public final class AccessibilityGestureNavigationTutorial { content = inflater.inflate( R.layout.tutorial_dialog_launch_service_by_accessibility_button, null); break; - case DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION: + case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE: content = inflater.inflate( R.layout.tutorial_dialog_launch_service_by_gesture_navigation, null); - final TextureView gestureTutorialVideo = content.findViewById( - R.id.gesture_tutorial_video); - final TextView gestureTutorialMessage = content.findViewById( - R.id.gesture_tutorial_message); - VideoPlayer.create(context, AccessibilityUtil.isTouchExploreEnabled(context) - ? R.raw.illustration_accessibility_gesture_three_finger - : R.raw.illustration_accessibility_gesture_two_finger, - gestureTutorialVideo); - gestureTutorialMessage.setText(AccessibilityUtil.isTouchExploreEnabled(context) - ? R.string.accessibility_tutorial_dialog_message_gesture_talkback - : R.string.accessibility_tutorial_dialog_message_gesture); + setupGestureNavigationTextWithImage(context, content); break; case DialogType.GESTURE_NAVIGATION_SETTINGS: content = inflater.inflate( R.layout.tutorial_dialog_launch_by_gesture_navigation_settings, null); - final TextureView gestureSettingsTutorialVideo = content.findViewById( - R.id.gesture_tutorial_video); - final TextView gestureSettingsTutorialMessage = content.findViewById( - R.id.gesture_tutorial_message); - VideoPlayer.create(context, AccessibilityUtil.isTouchExploreEnabled(context) - ? R.raw.illustration_accessibility_gesture_three_finger - : R.raw.illustration_accessibility_gesture_two_finger, - gestureSettingsTutorialVideo); - final int stringResId = AccessibilityUtil.isTouchExploreEnabled(context) - ? R.string.accessibility_tutorial_dialog_message_gesture_settings_talkback - : R.string.accessibility_tutorial_dialog_message_gesture_settings; - gestureSettingsTutorialMessage.setText(stringResId); + setupGestureNavigationTextWithImage(context, content); break; } return content; } + private static void setupGestureNavigationTextWithImage(Context context, View view) { + final boolean isTouchExploreEnabled = AccessibilityUtil.isTouchExploreEnabled(context); + + final ImageView imageView = view.findViewById(R.id.image); + final int gestureSettingsImageResId = + isTouchExploreEnabled ? R.drawable.illustration_accessibility_gesture_three_finger + : R.drawable.illustration_accessibility_gesture_two_finger; + imageView.setImageResource(gestureSettingsImageResId); + + final TextView textView = view.findViewById(R.id.gesture_tutorial_message); + textView.setText(isTouchExploreEnabled + ? R.string.accessibility_tutorial_dialog_message_gesture_settings_talkback + : R.string.accessibility_tutorial_dialog_message_gesture_settings); + } + private static AlertDialog createDialog(Context context, int dialogType) { final AlertDialog alertDialog = new AlertDialog.Builder(context) .setView(createTutorialDialogContentView(context, dialogType)) diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 1c7ce39bab9..c15217490c6 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -141,7 +141,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends switch (dialogId) { case DialogEnums.GESTURE_NAVIGATION_TUTORIAL: return AccessibilityGestureNavigationTutorial - .showGestureNavigationTutorialDialog(getPrefContext()); + .showAccessibilityGestureTutorialDialog(getPrefContext()); case DialogEnums.MAGNIFICATION_EDIT_SHORTCUT: final CharSequence dialogTitle = getPrefContext().getString( R.string.accessibility_shortcut_title, mPackageName); diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java index 2b5576d6743..aeb5e9f2fcc 100644 --- a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java +++ b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java @@ -20,24 +20,47 @@ import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.app.backup.BackupHelper; import android.content.Context; +import android.os.IDeviceIdleController; +import android.os.RemoteException; import android.os.ParcelFileDescriptor; +import android.os.ServiceManager; +import android.os.UserHandle; import android.util.Log; +import androidx.annotation.VisibleForTesting; + +import java.io.IOException; +import java.util.Arrays; + /** An implementation to backup and restore battery configurations. */ public final class BatteryBackupHelper implements BackupHelper { /** An inditifier for {@link BackupHelper}. */ public static final String TAG = "BatteryBackupHelper"; + private static final String DEVICE_IDLE_SERVICE = "deviceidle"; + private static final boolean DEBUG = false; + + @VisibleForTesting + static final CharSequence DELIMITER = ":"; + @VisibleForTesting + static final String KEY_FULL_POWER_LIST = "full_power_list"; + + @VisibleForTesting + IDeviceIdleController mIDeviceIdleController; private final Context mContext; public BatteryBackupHelper(Context context) { - mContext = context; + mContext = context.getApplicationContext(); } @Override public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { - Log.d(TAG, "performBackup()"); + if (!isOwner()) { + Log.w(TAG, "ignore the backup process for non-owner"); + return; + } + backupFullPowerList(getIDeviceIdleController(), data); } @Override @@ -48,4 +71,51 @@ public final class BatteryBackupHelper implements BackupHelper { @Override public void writeNewStateDescription(ParcelFileDescriptor newState) { } + + private void backupFullPowerList( + IDeviceIdleController deviceIdleService, BackupDataOutput data) { + final long timestamp = System.currentTimeMillis(); + String[] allowlistedApps; + try { + allowlistedApps = deviceIdleService.getFullPowerWhitelist(); + } catch (RemoteException e) { + Log.e(TAG, "backupFullPowerList() failed", e); + return; + } + // Ignores unexpected emptty result case. + if (allowlistedApps == null || allowlistedApps.length == 0) { + Log.w(TAG, "no data found in the getFullPowerList()"); + return; + } + debugLog("allowlistedApps:" + Arrays.toString(allowlistedApps)); + final String allowedApps = String.join(DELIMITER, allowlistedApps); + final byte[] allowedAppsBytes = allowedApps.getBytes(); + try { + data.writeEntityHeader(KEY_FULL_POWER_LIST, allowedAppsBytes.length); + data.writeEntityData(allowedAppsBytes, allowedAppsBytes.length); + } catch (IOException e) { + Log.e(TAG, "backup getFullPowerList() failed", e); + return; + } + Log.d(TAG, String.format("backup getFullPowerList() size=%d in %d/ms", + allowlistedApps.length, (System.currentTimeMillis() - timestamp))); + } + + // Provides an opportunity to inject mock IDeviceIdleController for testing. + private IDeviceIdleController getIDeviceIdleController() { + if (mIDeviceIdleController != null) { + return mIDeviceIdleController; + } + mIDeviceIdleController = IDeviceIdleController.Stub.asInterface( + ServiceManager.getService(DEVICE_IDLE_SERVICE)); + return mIDeviceIdleController; + } + + private void debugLog(String debugContent) { + if (DEBUG) Log.d(TAG, debugContent); + } + + private static boolean isOwner() { + return UserHandle.myUserId() == UserHandle.USER_OWNER; + } } diff --git a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java index 4b9cfa36f2f..1a116da91ab 100644 --- a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java +++ b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java @@ -28,17 +28,19 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.om.IOverlayManager; import android.content.om.OverlayInfo; +import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; import android.text.TextUtils; import android.view.accessibility.AccessibilityManager; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; import com.android.settings.R; -import com.android.settings.SettingsTutorialDialogWrapperActivity; +import com.android.settings.accessibility.AccessibilityGestureNavigationTutorial; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; @@ -57,8 +59,6 @@ import java.util.List; public class SystemNavigationGestureSettings extends RadioButtonPickerFragment implements HelpResourceProvider { - private static final String TAG = "SystemNavigationGesture"; - @VisibleForTesting static final String KEY_SYSTEM_NAV_3BUTTONS = "system_nav_3buttons"; @VisibleForTesting @@ -69,10 +69,33 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment i public static final String PREF_KEY_SUGGESTION_COMPLETE = "pref_system_navigation_suggestion_complete"; + private static final String KEY_SHOW_A11Y_TUTORIAL_DIALOG = "show_a11y_tutorial_dialog_bool"; + + private boolean mA11yTutorialDialogShown = false; + private IOverlayManager mOverlayManager; private IllustrationPreference mVideoPreference; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + mA11yTutorialDialogShown = + savedInstanceState.getBoolean(KEY_SHOW_A11Y_TUTORIAL_DIALOG, false); + if (mA11yTutorialDialogShown) { + AccessibilityGestureNavigationTutorial.showGestureNavigationTutorialDialog( + getContext(), dialog -> mA11yTutorialDialogShown = false); + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putBoolean(KEY_SHOW_A11Y_TUTORIAL_DIALOG, mA11yTutorialDialogShown); + super.onSaveInstanceState(outState); + } + @Override public void onAttach(Context context) { super.onAttach(context); @@ -177,14 +200,7 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment i protected boolean setDefaultKey(String key) { setCurrentSystemNavigationMode(mOverlayManager, key); setIllustrationVideo(mVideoPreference, key); - if (TextUtils.equals(KEY_SYSTEM_NAV_GESTURAL, key) - && !isAccessibilityFloatingMenuEnabled() - && (isAnyServiceSupportAccessibilityButton() || isNavBarMagnificationEnabled())) { - final Intent intent = new Intent(getActivity(), - SettingsTutorialDialogWrapperActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } + setGestureNavigationTutorialDialog(key); return true; } @@ -257,6 +273,18 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment i } } + private void setGestureNavigationTutorialDialog(String systemNavKey) { + if (TextUtils.equals(KEY_SYSTEM_NAV_GESTURAL, systemNavKey) + && !isAccessibilityFloatingMenuEnabled() + && (isAnyServiceSupportAccessibilityButton() || isNavBarMagnificationEnabled())) { + mA11yTutorialDialogShown = true; + AccessibilityGestureNavigationTutorial.showGestureNavigationTutorialDialog(getContext(), + dialog -> mA11yTutorialDialogShown = false); + } else { + mA11yTutorialDialogShown = false; + } + } + private boolean isAnyServiceSupportAccessibilityButton() { final AccessibilityManager ams = getContext().getSystemService(AccessibilityManager.class); final List targets = ams.getAccessibilityShortcutTargets( diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java new file mode 100644 index 00000000000..87aa8122caf --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBackupHelperTest.java @@ -0,0 +1,157 @@ +/* + * 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.fuelgauge; + +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.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.backup.BackupDataOutput; +import android.content.Context; +import android.os.IDeviceIdleController; +import android.os.RemoteException; +import android.os.UserHandle; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {BatteryBackupHelperTest.ShadowUserHandle.class}) +public final class BatteryBackupHelperTest { + + private Context mContext; + private BatteryBackupHelper mBatteryBackupHelper; + + @Mock + private BackupDataOutput mBackupDataOutput; + @Mock + private IDeviceIdleController mDeviceController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mBatteryBackupHelper = new BatteryBackupHelper(mContext); + mBatteryBackupHelper.mIDeviceIdleController = mDeviceController; + } + + @After + public void resetShadows() { + ShadowUserHandle.reset(); + } + + @Test + public void performBackup_nullPowerList_notBackupPowerList() throws Exception { + doReturn(null).when(mDeviceController).getFullPowerWhitelist(); + mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null); + + verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt()); + } + + @Test + public void performBackup_emptyPowerList_notBackupPowerList() throws Exception { + doReturn(new String[0]).when(mDeviceController).getFullPowerWhitelist(); + mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null); + + verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt()); + } + + @Test + public void performBackup_remoteException_notBackupPowerList() throws Exception { + doThrow(new RemoteException()).when(mDeviceController).getFullPowerWhitelist(); + mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null); + + verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt()); + } + + @Test + public void performBackup_oneFullPowerListElement_backupFullPowerListData() + throws Exception { + final String[] fullPowerList = {"com.android.package"}; + doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist(); + + mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null); + + final byte[] expectedBytes = fullPowerList[0].getBytes(); + verify(mBackupDataOutput).writeEntityHeader( + BatteryBackupHelper.KEY_FULL_POWER_LIST, expectedBytes.length); + verify(mBackupDataOutput).writeEntityData(expectedBytes, expectedBytes.length); + } + + @Test + public void performBackup_backupFullPowerListData() throws Exception { + final String[] fullPowerList = {"com.android.package1", "com.android.package2"}; + doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist(); + + mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null); + + final String expectedResult = fullPowerList[0] + + BatteryBackupHelper.DELIMITER + fullPowerList[1]; + final byte[] expectedBytes = expectedResult.getBytes(); + verify(mBackupDataOutput).writeEntityHeader( + BatteryBackupHelper.KEY_FULL_POWER_LIST, expectedBytes.length); + verify(mBackupDataOutput).writeEntityData(expectedBytes, expectedBytes.length); + } + + @Test + public void performBackup_nonOwner_ignoreAllBackupAction() throws Exception { + ShadowUserHandle.setUid(1); + final String[] fullPowerList = {"com.android.package"}; + doReturn(fullPowerList).when(mDeviceController).getFullPowerWhitelist(); + + mBatteryBackupHelper.performBackup(null, mBackupDataOutput, null); + + verify(mBackupDataOutput, never()).writeEntityHeader(anyString(), anyInt()); + } + + @Implements(UserHandle.class) + public static class ShadowUserHandle { + // Sets the default as thte OWNER role. + private static int sUid = 0; + + public static void setUid(int uid) { + sUid = uid; + } + + @Implementation + public static int myUserId() { + return sUid; + } + + @Resetter + public static void reset() { + sUid = 0; + } + } +}