1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.settings.accessibility;
18 
19 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
20 
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.res.TypedArray;
24 import android.graphics.drawable.Drawable;
25 import android.text.Spannable;
26 import android.text.SpannableString;
27 import android.text.style.ImageSpan;
28 import android.view.LayoutInflater;
29 import android.view.TextureView;
30 import android.view.View;
31 import android.view.Window;
32 import android.view.accessibility.AccessibilityManager;
33 import android.widget.TextView;
34 
35 import androidx.annotation.ColorInt;
36 import androidx.annotation.IntDef;
37 import androidx.appcompat.app.AlertDialog;
38 import androidx.core.content.ContextCompat;
39 
40 import com.android.settings.R;
41 
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 
45 /**
46  * Utility class for creating the dialog that guides users for gesture navigation for
47  * accessibility services.
48  */
49 public class AccessibilityGestureNavigationTutorial {
50 
51     /** IntDef enum for dialog type. */
52     @Retention(RetentionPolicy.SOURCE)
53     @IntDef({
54             DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON,
55             DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION,
56             DialogType.GESTURE_NAVIGATION_SETTINGS,
57     })
58 
59     private @interface DialogType {
60         int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0;
61         int LAUNCH_SERVICE_BY_GESTURE_NAVIGATION = 1;
62         int GESTURE_NAVIGATION_SETTINGS = 2;
63     }
64 
65     private static final DialogInterface.OnClickListener mOnClickListener =
66             (DialogInterface dialog, int which) -> dialog.dismiss();
67 
showGestureNavigationSettingsTutorialDialog(Context context, DialogInterface.OnDismissListener dismissListener)68     public static void showGestureNavigationSettingsTutorialDialog(Context context,
69             DialogInterface.OnDismissListener dismissListener) {
70         final AlertDialog alertDialog = new AlertDialog.Builder(context)
71                 .setView(createTutorialDialogContentView(context,
72                         DialogType.GESTURE_NAVIGATION_SETTINGS))
73                 .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener)
74                 .setOnDismissListener(dismissListener)
75                 .create();
76 
77         alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
78         alertDialog.setCanceledOnTouchOutside(false);
79         alertDialog.show();
80     }
81 
showAccessibilityButtonTutorialDialog(Context context)82     static AlertDialog showAccessibilityButtonTutorialDialog(Context context) {
83         final AlertDialog alertDialog = createDialog(context,
84                 DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON);
85 
86         if (!isGestureNavigateEnabled(context)) {
87             updateMessageWithIcon(context, alertDialog);
88         }
89 
90         return alertDialog;
91     }
92 
showGestureNavigationTutorialDialog(Context context)93     static AlertDialog showGestureNavigationTutorialDialog(Context context) {
94         return createDialog(context, DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION);
95     }
96 
97     /**
98      * Get a content View for a dialog to confirm that they want to enable a service.
99      *
100      * @param context    A valid context
101      * @param dialogType The type of tutorial dialog
102      * @return A content view suitable for viewing
103      */
createTutorialDialogContentView(Context context, int dialogType)104     private static View createTutorialDialogContentView(Context context, int dialogType) {
105         final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
106                 Context.LAYOUT_INFLATER_SERVICE);
107 
108         View content = null;
109 
110         switch (dialogType) {
111             case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON:
112                 content = inflater.inflate(
113                         R.layout.tutorial_dialog_launch_service_by_accessibility_button, null);
114                 break;
115             case DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION:
116                 content = inflater.inflate(
117                         R.layout.tutorial_dialog_launch_service_by_gesture_navigation, null);
118                 final TextureView gestureTutorialVideo = content.findViewById(
119                         R.id.gesture_tutorial_video);
120                 final TextView gestureTutorialMessage = content.findViewById(
121                         R.id.gesture_tutorial_message);
122                 VideoPlayer.create(context, isTouchExploreOn(context)
123                                 ? R.raw.illustration_accessibility_gesture_three_finger
124                                 : R.raw.illustration_accessibility_gesture_two_finger,
125                         gestureTutorialVideo);
126                 gestureTutorialMessage.setText(isTouchExploreOn(context)
127                         ? R.string.accessibility_tutorial_dialog_message_gesture_with_talkback
128                         : R.string.accessibility_tutorial_dialog_message_gesture_without_talkback);
129                 break;
130             case DialogType.GESTURE_NAVIGATION_SETTINGS:
131                 content = inflater.inflate(
132                         R.layout.tutorial_dialog_launch_by_gesture_navigation_settings, null);
133                 final TextureView gestureSettingsTutorialVideo = content.findViewById(
134                         R.id.gesture_tutorial_video);
135                 final TextView gestureSettingsTutorialMessage = content.findViewById(
136                         R.id.gesture_tutorial_message);
137                 VideoPlayer.create(context, isTouchExploreOn(context)
138                                 ? R.raw.illustration_accessibility_gesture_three_finger
139                                 : R.raw.illustration_accessibility_gesture_two_finger,
140                         gestureSettingsTutorialVideo);
141                 gestureSettingsTutorialMessage.setText(isTouchExploreOn(context)
142                         ?
143                         R.string.accessibility_tutorial_dialog_message_gesture_settings_with_talkback
144                         : R.string.accessibility_tutorial_dialog_message_gesture_settings_without_talkback);
145                 break;
146         }
147 
148         return content;
149     }
150 
createDialog(Context context, int dialogType)151     private static AlertDialog createDialog(Context context, int dialogType) {
152         final AlertDialog alertDialog = new AlertDialog.Builder(context)
153                 .setView(createTutorialDialogContentView(context, dialogType))
154                 .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener)
155                 .create();
156 
157         alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
158         alertDialog.setCanceledOnTouchOutside(false);
159         alertDialog.show();
160 
161         return alertDialog;
162     }
163 
updateMessageWithIcon(Context context, AlertDialog alertDialog)164     private static void updateMessageWithIcon(Context context, AlertDialog alertDialog) {
165         final TextView gestureTutorialMessage = alertDialog.findViewById(
166                 R.id.button_tutorial_message);
167 
168         // Get the textView line height to update [icon] size. Must be called after show()
169         final int lineHeight = gestureTutorialMessage.getLineHeight();
170         gestureTutorialMessage.setText(getMessageStringWithIcon(context, lineHeight));
171     }
172 
getMessageStringWithIcon(Context context, int lineHeight)173     private static SpannableString getMessageStringWithIcon(Context context, int lineHeight) {
174         final String messageString = context
175                 .getString(R.string.accessibility_tutorial_dialog_message_button);
176         final SpannableString spannableMessage = SpannableString.valueOf(messageString);
177 
178         // Icon
179         final int indexIconStart = messageString.indexOf("%s");
180         final int indexIconEnd = indexIconStart + 2;
181         final Drawable icon = context.getDrawable(R.drawable.ic_accessibility_new);
182         icon.setTint(getThemeAttrColor(context, android.R.attr.textColorPrimary));
183         icon.setBounds(0, 0, lineHeight, lineHeight);
184         spannableMessage.setSpan(
185                 new ImageSpan(icon), indexIconStart, indexIconEnd,
186                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
187 
188         return spannableMessage;
189     }
190 
191     /** Returns the color associated with the specified attribute in the context's theme. */
192     @ColorInt
getThemeAttrColor(final Context context, final int attributeColor)193     private static int getThemeAttrColor(final Context context, final int attributeColor) {
194         final int colorResId = getAttrResourceId(context, attributeColor);
195         return ContextCompat.getColor(context, colorResId);
196     }
197 
198     /** Returns the identifier of the resolved resource assigned to the given attribute. */
getAttrResourceId(final Context context, final int attributeColor)199     private static int getAttrResourceId(final Context context, final int attributeColor) {
200         final int[] attrs = {attributeColor};
201         final TypedArray typedArray = context.obtainStyledAttributes(attrs);
202         final int colorResId = typedArray.getResourceId(0, 0);
203         typedArray.recycle();
204         return colorResId;
205     }
206 
isGestureNavigateEnabled(Context context)207     private static boolean isGestureNavigateEnabled(Context context) {
208         return context.getResources().getInteger(
209                 com.android.internal.R.integer.config_navBarInteractionMode)
210                 == NAV_BAR_MODE_GESTURAL;
211     }
212 
isTouchExploreOn(Context context)213     private static boolean isTouchExploreOn(Context context) {
214         return ((AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE))
215                 .isTouchExplorationEnabled();
216     }
217 }
218