diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f7465fec324..ef68318799b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1500,6 +1500,11 @@ android:value="true" /> + + diff --git a/res/drawable/ic_accessibility_new.xml b/res/drawable/ic_accessibility_new.xml new file mode 100644 index 00000000000..6bfc6c60f92 --- /dev/null +++ b/res/drawable/ic_accessibility_new.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/illustration_accessibility_button.xml b/res/drawable/illustration_accessibility_button.xml new file mode 100644 index 00000000000..264f3d361ed --- /dev/null +++ b/res/drawable/illustration_accessibility_button.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/res/layout/tutorial_dialog_launch_by_gesture_navigation_settings.xml b/res/layout/tutorial_dialog_launch_by_gesture_navigation_settings.xml new file mode 100644 index 00000000000..fb4b6ccfdb2 --- /dev/null +++ b/res/layout/tutorial_dialog_launch_by_gesture_navigation_settings.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/tutorial_dialog_launch_service_by_accessibility_button.xml b/res/layout/tutorial_dialog_launch_service_by_accessibility_button.xml new file mode 100644 index 00000000000..caa91a84510 --- /dev/null +++ b/res/layout/tutorial_dialog_launch_service_by_accessibility_button.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/tutorial_dialog_launch_service_by_gesture_navigation.xml b/res/layout/tutorial_dialog_launch_service_by_gesture_navigation.xml new file mode 100644 index 00000000000..454029b0041 --- /dev/null +++ b/res/layout/tutorial_dialog_launch_service_by_gesture_navigation.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/raw/illustration_accessibility_gesture_three_finger.mp4 b/res/raw/illustration_accessibility_gesture_three_finger.mp4 new file mode 100644 index 00000000000..d48371b0748 Binary files /dev/null and b/res/raw/illustration_accessibility_gesture_three_finger.mp4 differ diff --git a/res/raw/illustration_accessibility_gesture_two_finger.mp4 b/res/raw/illustration_accessibility_gesture_two_finger.mp4 new file mode 100644 index 00000000000..7607abc0e66 Binary files /dev/null and b/res/raw/illustration_accessibility_gesture_two_finger.mp4 differ diff --git a/src/com/android/settings/SettingsTutorialDialogWrapperActivity.java b/src/com/android/settings/SettingsTutorialDialogWrapperActivity.java new file mode 100644 index 00000000000..ce4468963f0 --- /dev/null +++ b/src/com/android/settings/SettingsTutorialDialogWrapperActivity.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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; + +import android.app.Activity; +import android.os.Bundle; + +import com.android.settings.accessibility.AccessibilityGestureNavigationTutorial; +import com.android.settings.R; + +/** + * This activity is to create the tutorial dialog in gesture navigation settings since we couldn't + * use the dialog utils because SystemNavigationGestureSettings extends RadioButtonPickerFragment, + * not SettingsPreferenceFragment. + */ +public class SettingsTutorialDialogWrapperActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + showDialog(); + } + + private void showDialog() { + AccessibilityGestureNavigationTutorial + .showGestureNavigationSettingsTutorialDialog(this, dialog -> finish()); + } +} \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java new file mode 100644 index 00000000000..8c0bdabe959 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ImageSpan; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.Window; +import android.view.accessibility.AccessibilityManager; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; + +import com.android.settings.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Utility class for creating the dialog that guides users for gesture navigation for + * accessibility services. + */ +public class AccessibilityGestureNavigationTutorial { + + /** IntDef enum for dialog type. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON, + DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION, + DialogType.GESTURE_NAVIGATION_SETTINGS, + }) + + private @interface DialogType { + int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0; + int LAUNCH_SERVICE_BY_GESTURE_NAVIGATION = 1; + int GESTURE_NAVIGATION_SETTINGS = 2; + } + + private static final DialogInterface.OnClickListener mOnClickListener = + (DialogInterface dialog, int which) -> dialog.dismiss(); + + public static void showGestureNavigationSettingsTutorialDialog(Context context, + DialogInterface.OnDismissListener dismissListener) { + final AlertDialog alertDialog = new AlertDialog.Builder(context) + .setView(createTutorialDialogContentView(context, + DialogType.GESTURE_NAVIGATION_SETTINGS)) + .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) + .setOnDismissListener(dismissListener) + .create(); + + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + alertDialog.setCanceledOnTouchOutside(false); + alertDialog.show(); + } + + static AlertDialog showAccessibilityButtonTutorialDialog(Context context) { + final AlertDialog alertDialog = createDialog(context, + DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON); + + if (!isGestureNavigateEnabled(context)) { + updateMessageWithIcon(context, alertDialog); + } + + return alertDialog; + } + + static AlertDialog showGestureNavigationTutorialDialog(Context context) { + return createDialog(context, DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION); + } + + /** + * Get 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 + * @return A content view suitable for viewing + */ + private static View createTutorialDialogContentView(Context context, int dialogType) { + final LayoutInflater inflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + View content = null; + + switch (dialogType) { + case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON: + content = inflater.inflate( + R.layout.tutorial_dialog_launch_service_by_accessibility_button, null); + break; + case DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION: + 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, isTouchExploreOn(context) + ? R.raw.illustration_accessibility_gesture_three_finger + : R.raw.illustration_accessibility_gesture_two_finger, + gestureTutorialVideo); + gestureTutorialMessage.setText(isTouchExploreOn(context) + ? R.string.accessibility_tutorial_dialog_message_gesture_with_talkback + : R.string.accessibility_tutorial_dialog_message_gesture_without_talkback); + 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, isTouchExploreOn(context) + ? R.raw.illustration_accessibility_gesture_three_finger + : R.raw.illustration_accessibility_gesture_two_finger, + gestureSettingsTutorialVideo); + gestureSettingsTutorialMessage.setText(isTouchExploreOn(context) + ? + R.string.accessibility_tutorial_dialog_message_gesture_settings_with_talkback + : R.string.accessibility_tutorial_dialog_message_gesture_settings_without_talkback); + break; + } + + return content; + } + + private static AlertDialog createDialog(Context context, int dialogType) { + final AlertDialog alertDialog = new AlertDialog.Builder(context) + .setView(createTutorialDialogContentView(context, dialogType)) + .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) + .create(); + + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + alertDialog.setCanceledOnTouchOutside(false); + alertDialog.show(); + + return alertDialog; + } + + private static void updateMessageWithIcon(Context context, AlertDialog alertDialog) { + final TextView gestureTutorialMessage = alertDialog.findViewById( + R.id.button_tutorial_message); + + // Get the textView line height to update [icon] size. Must be called after show() + final int lineHeight = gestureTutorialMessage.getLineHeight(); + gestureTutorialMessage.setText(getMessageStringWithIcon(context, lineHeight)); + } + + private static SpannableString getMessageStringWithIcon(Context context, int lineHeight) { + final String messageString = context + .getString(R.string.accessibility_tutorial_dialog_message_button); + final SpannableString spannableMessage = SpannableString.valueOf(messageString); + + // Icon + final int indexIconStart = messageString.indexOf("%s"); + final int indexIconEnd = indexIconStart + 2; + final Drawable icon = context.getDrawable(R.drawable.ic_accessibility_new); + icon.setTint(getThemeAttrColor(context, android.R.attr.textColorPrimary)); + icon.setBounds(0, 0, lineHeight, lineHeight); + spannableMessage.setSpan( + new ImageSpan(icon), indexIconStart, indexIconEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spannableMessage; + } + + /** Returns the color associated with the specified attribute in the context's theme. */ + @ColorInt + private static int getThemeAttrColor(final Context context, final int attributeColor) { + final int colorResId = getAttrResourceId(context, attributeColor); + return ContextCompat.getColor(context, colorResId); + } + + /** Returns the identifier of the resolved resource assigned to the given attribute. */ + private static int getAttrResourceId(final Context context, final int attributeColor) { + final int[] attrs = {attributeColor}; + final TypedArray typedArray = context.obtainStyledAttributes(attrs); + final int colorResId = typedArray.getResourceId(0, 0); + typedArray.recycle(); + return colorResId; + } + + private static boolean isGestureNavigateEnabled(Context context) { + return context.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == NAV_BAR_MODE_GESTURAL; + } + + private static boolean isTouchExploreOn(Context context) { + return ((AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE)) + .isTouchExplorationEnabled(); + } +} \ No newline at end of file diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index b28d8b5e541..ec8df88d977 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -16,14 +16,18 @@ package com.android.settings.accessibility; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Activity; import android.app.Dialog; import android.app.admin.DevicePolicyManager; import android.app.settings.SettingsEnums; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -50,6 +54,7 @@ public class ToggleAccessibilityServicePreferenceFragment private static final int DIALOG_ID_ENABLE_WARNING = 1; private static final int DIALOG_ID_DISABLE_WARNING = 2; + private static final int DIALOG_ID_LAUNCH_ACCESSIBILITY_TUTORIAL = 3; public static final int ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION = 1; @@ -57,7 +62,7 @@ public class ToggleAccessibilityServicePreferenceFragment private final SettingsContentObserver mSettingsContentObserver = new SettingsContentObserver(new Handler()) { - @Override + @Override public void onChange(boolean selfChange, Uri uri) { updateSwitchBarToggleSwitch(); } @@ -144,6 +149,16 @@ public class ToggleAccessibilityServicePreferenceFragment .createDisableDialog(getActivity(), info, this); break; } + case DIALOG_ID_LAUNCH_ACCESSIBILITY_TUTORIAL: { + if (isGestureNavigateEnabled()) { + mDialog = AccessibilityGestureNavigationTutorial + .showGestureNavigationTutorialDialog(getActivity()); + } else { + mDialog = AccessibilityGestureNavigationTutorial + .showAccessibilityButtonTutorialDialog(getActivity()); + } + break; + } default: { throw new IllegalArgumentException(); } @@ -197,32 +212,54 @@ public class ToggleAccessibilityServicePreferenceFragment @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.permission_enable_allow_button: - if (isFullDiskEncrypted()) { - String title = createConfirmCredentialReasonMessage(); - Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null); - startActivityForResult(intent, - ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION); - } else { - handleConfirmServiceEnabled(true); - } - break; - case R.id.permission_enable_deny_button: - handleConfirmServiceEnabled(false); - break; - case R.id.permission_disable_stop_button: - handleConfirmServiceEnabled(false); - break; - case R.id.permission_disable_cancel_button: + if (view.getId() == R.id.permission_enable_allow_button) { + if (isFullDiskEncrypted()) { + String title = createConfirmCredentialReasonMessage(); + Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null); + startActivityForResult(intent, + ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION); + } else { handleConfirmServiceEnabled(true); - break; - default: - throw new IllegalArgumentException(); + if (isServiceSupportAccessibilityButton()) { + showDialog(DIALOG_ID_LAUNCH_ACCESSIBILITY_TUTORIAL); + } + } + } else if (view.getId() == R.id.permission_enable_deny_button) { + handleConfirmServiceEnabled(false); + } else if (view.getId() == R.id.permission_disable_stop_button) { + handleConfirmServiceEnabled(false); + } else if (view.getId() == R.id.permission_disable_cancel_button) { + handleConfirmServiceEnabled(true); + } else { + throw new IllegalArgumentException(); } mDialog.dismiss(); } + private boolean isGestureNavigateEnabled() { + return getContext().getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == NAV_BAR_MODE_GESTURAL; + } + + private boolean isServiceSupportAccessibilityButton() { + final AccessibilityManager ams = (AccessibilityManager) getContext().getSystemService( + Context.ACCESSIBILITY_SERVICE); + final List services = ams.getInstalledAccessibilityServiceList(); + + for (AccessibilityServiceInfo info : services) { + if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) { + ServiceInfo serviceInfo = info.getResolveInfo().serviceInfo; + if (serviceInfo != null && TextUtils.equals(serviceInfo.name, + getAccessibilityServiceInfo().getResolveInfo().serviceInfo.name)) { + return true; + } + } + } + + return false; + } + private void handleConfirmServiceEnabled(boolean confirmed) { mSwitchBar.setCheckedInternal(confirmed); getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed); @@ -234,11 +271,13 @@ public class ToggleAccessibilityServicePreferenceFragment switch (mLockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId())) { case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: { resId = R.string.enable_service_pattern_reason; - } break; + } + break; case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: { resId = R.string.enable_service_pin_reason; - } break; + } + break; } return getString(resId, getAccessibilityServiceInfo().getResolveInfo() .loadLabel(getPackageManager())); @@ -248,7 +287,7 @@ public class ToggleAccessibilityServicePreferenceFragment protected void onInstallSwitchBarToggleSwitch() { super.onInstallSwitchBarToggleSwitch(); mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { - @Override + @Override public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { if (checked) { mSwitchBar.setCheckedInternal(false); diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 5ed5169f1b7..ec3ebcc1a68 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -16,6 +16,9 @@ package com.android.settings.accessibility; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + +import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; @@ -26,6 +29,8 @@ import android.media.MediaPlayer; import android.media.MediaPlayer.OnPreparedListener; import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; import android.view.Display; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.WindowManager; @@ -44,6 +49,10 @@ import com.android.settings.widget.SwitchBar; public class ToggleScreenMagnificationPreferenceFragment extends ToggleFeaturePreferenceFragment implements SwitchBar.OnSwitchChangeListener { + private static final int DIALOG_ID_GESTURE_NAVIGATION_TUTORIAL = 1; + + private Dialog mDialog; + protected class VideoPreference extends Preference { private ImageView mVideoBackgroundView; private OnGlobalLayoutListener mLayoutListener; @@ -161,12 +170,32 @@ public class ToggleScreenMagnificationPreferenceFragment extends updateConfigurationWarningIfNeeded(); } + @Override + public Dialog onCreateDialog(int dialogId) { + if (dialogId == DIALOG_ID_GESTURE_NAVIGATION_TUTORIAL) { + if (isGestureNavigateEnabled()) { + mDialog = AccessibilityGestureNavigationTutorial + .showGestureNavigationTutorialDialog(getActivity()); + } else { + mDialog = AccessibilityGestureNavigationTutorial + .showAccessibilityButtonTutorialDialog(getActivity()); + } + } + + return mDialog; + } + @Override public int getMetricsCategory() { // TODO: Distinguish between magnification modes return SettingsEnums.ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION; } + @Override + public int getDialogMetricsCategory(int dialogId) { + return SettingsEnums.ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION; + } + @Override public void onSwitchChanged(Switch switchView, boolean isChecked) { onPreferenceToggled(mPreferenceKey, isChecked); @@ -174,6 +203,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends @Override protected void onPreferenceToggled(String preferenceKey, boolean enabled) { + if (enabled && TextUtils.equals( + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, + preferenceKey)) { + showDialog(DIALOG_ID_GESTURE_NAVIGATION_TUTORIAL); + } MagnificationPreferenceFragment.setChecked(getContentResolver(), preferenceKey, enabled); updateConfigurationWarningIfNeeded(); } @@ -224,6 +258,12 @@ public class ToggleScreenMagnificationPreferenceFragment extends } } + private boolean isGestureNavigateEnabled() { + return getContext().getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == NAV_BAR_MODE_GESTURAL; + } + private void updateConfigurationWarningIfNeeded() { final CharSequence warningMessage = MagnificationPreferenceFragment.getConfigurationWarningStringForSecureSettingsKey( diff --git a/src/com/android/settings/accessibility/VideoPlayer.java b/src/com/android/settings/accessibility/VideoPlayer.java new file mode 100644 index 00000000000..8f94b768f67 --- /dev/null +++ b/src/com/android/settings/accessibility/VideoPlayer.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaPlayer; +import android.view.Surface; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; + +import androidx.annotation.GuardedBy; +import androidx.annotation.RawRes; + +/** + * Plays the video by {@link MediaPlayer} on {@link TextureView}, calls {@link #create(Context, int, + * TextureView)} to setup the listener for TextureView and start to play the video. Once this player + * is no longer used, call {@link #release()} so that MediaPlayer object can be released. + */ +public class VideoPlayer implements SurfaceTextureListener { + private final Context context; + private final Object mediaPlayerLock = new Object(); + // Media player object can't be used after it has been released, so it will be set to null. But + // VideoPlayer is asynchronized, media player object might be paused or resumed again before + // released media player is set to null. Therefore, lock mediaPlayer and mediaPlayerState by + // mediaPlayerLock keep their states consistent. + @GuardedBy("mediaPlayerLock") + private MediaPlayer mediaPlayer; + @GuardedBy("mediaPlayerLock") + private State mediaPlayerState = State.NONE; + @RawRes + private final int videoRes; + private Surface animationSurface; + + + /** + * Creates a {@link MediaPlayer} for a given resource id and starts playback when the surface + * for + * a given {@link TextureView} is ready. + */ + public static VideoPlayer create(Context context, @RawRes int videoRes, + TextureView textureView) { + return new VideoPlayer(context, videoRes, textureView); + } + + private VideoPlayer(Context context, @RawRes int videoRes, TextureView textureView) { + this.context = context; + this.videoRes = videoRes; + textureView.setSurfaceTextureListener(this); + } + + public void pause() { + synchronized (mediaPlayerLock) { + if (mediaPlayerState == State.STARTED) { + mediaPlayerState = State.PAUSED; + mediaPlayer.pause(); + } + } + } + + public void resume() { + synchronized (mediaPlayerLock) { + if (mediaPlayerState == State.PAUSED) { + mediaPlayer.start(); + mediaPlayerState = State.STARTED; + } + } + } + + /** Release media player when it's no longer needed. */ + public void release() { + synchronized (mediaPlayerLock) { + if (mediaPlayerState != State.NONE && mediaPlayerState != State.END) { + mediaPlayerState = State.END; + mediaPlayer.release(); + mediaPlayer = null; + } + } + if (animationSurface != null) { + animationSurface.release(); + animationSurface = null; + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + animationSurface = new Surface(surface); + synchronized (mediaPlayerLock) { + mediaPlayer = MediaPlayer.create(context, videoRes); + mediaPlayerState = State.PREPARED; + mediaPlayer.setSurface(animationSurface); + mediaPlayer.setLooping(true); + mediaPlayer.start(); + mediaPlayerState = State.STARTED; + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + release(); + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + } + + /** + * The state of MediaPlayer object. Refer to + * https://developer.android.com/reference/android/media/MediaPlayer#StateDiagram. + */ + public enum State { + /** MediaPlayer objects has not be created. */ + NONE, + /** MediaPlayer objects is created by create() method. */ + PREPARED, + /** MediaPlayer is started. It can be paused by pause() method. */ + STARTED, + /** MediaPlayer object is paused. Calling start() to resume it. */ + PAUSED, + /** + * MediaPlayer object is stopped and cannot be started until calling prepare() or + * prepareAsync() + * methods. + */ + STOPPED, + /** MediaPlayer object is released. It cannot be used again. */ + END + } +} + diff --git a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java index 40e8831e5c2..28b76b2b011 100644 --- a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java +++ b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java @@ -25,18 +25,24 @@ import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.E import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_INFO; import android.app.AlertDialog; +import android.accessibilityservice.AccessibilityServiceInfo; import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.om.IOverlayManager; import android.graphics.drawable.Drawable; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.SearchIndexableResource; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.accessibility.AccessibilityManager; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; +import com.android.settings.SettingsTutorialDialogWrapperActivity; import com.android.settings.R; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; @@ -165,6 +171,12 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment { setCurrentSystemNavigationMode(mOverlayManager, key); setIllustrationVideo(mVideoPreference, key); + if (TextUtils.equals(KEY_SYSTEM_NAV_GESTURAL, key) && ( + isAnyServiceSupportAccessibilityButton() || isNavBarMagnificationEnabled())) { + Intent intent = new Intent(getActivity(), SettingsTutorialDialogWrapperActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } return true; } @@ -256,6 +268,26 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment { .show(); } + private boolean isAnyServiceSupportAccessibilityButton() { + final AccessibilityManager ams = (AccessibilityManager) getContext().getSystemService( + Context.ACCESSIBILITY_SERVICE); + final List services = ams.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + + for (AccessibilityServiceInfo info : services) { + if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) { + return true; + } + } + + return false; + } + + private boolean isNavBarMagnificationEnabled() { + return Settings.Secure.getInt(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1; + } + static class NavModeCandidateInfo extends CandidateInfo { private final CharSequence mLabel; private final CharSequence mSummary;