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.View.GONE; 20 import static android.view.View.VISIBLE; 21 22 import static com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; 23 24 import android.app.settings.SettingsEnums; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.graphics.drawable.Drawable; 28 import android.text.Spannable; 29 import android.text.SpannableString; 30 import android.text.SpannableStringBuilder; 31 import android.text.style.ImageSpan; 32 import android.util.ArrayMap; 33 import android.util.Log; 34 import android.view.Gravity; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.Window; 39 import android.widget.Button; 40 import android.widget.FrameLayout; 41 import android.widget.ImageView; 42 import android.widget.LinearLayout; 43 import android.widget.TextSwitcher; 44 import android.widget.TextView; 45 46 import androidx.annotation.AnimRes; 47 import androidx.annotation.DrawableRes; 48 import androidx.annotation.IntDef; 49 import androidx.annotation.NonNull; 50 import androidx.annotation.Nullable; 51 import androidx.annotation.RawRes; 52 import androidx.annotation.VisibleForTesting; 53 import androidx.appcompat.app.AlertDialog; 54 import androidx.core.util.Preconditions; 55 import androidx.core.widget.TextViewCompat; 56 import androidx.viewpager.widget.PagerAdapter; 57 import androidx.viewpager.widget.ViewPager; 58 59 import com.android.server.accessibility.Flags; 60 import com.android.settings.R; 61 import com.android.settings.core.SubSettingLauncher; 62 import com.android.settingslib.utils.StringUtil; 63 import com.android.settingslib.widget.LottieColorUtils; 64 65 import com.airbnb.lottie.LottieAnimationView; 66 import com.airbnb.lottie.LottieDrawable; 67 68 import java.lang.annotation.Retention; 69 import java.lang.annotation.RetentionPolicy; 70 import java.util.ArrayList; 71 import java.util.List; 72 import java.util.Map; 73 74 /** 75 * Utility class for creating the dialog that shows tutorials on how to use the selected 76 * accessibility shortcut types 77 */ 78 public final class AccessibilityShortcutsTutorial { 79 private static final String TAG = "AccessibilityGestureNavigationTutorial"; 80 81 /** IntDef enum for dialog type. */ 82 @Retention(RetentionPolicy.SOURCE) 83 @IntDef({ 84 DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON, 85 DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE, 86 DialogType.GESTURE_NAVIGATION_SETTINGS, 87 }) 88 89 private @interface DialogType { 90 int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0; 91 int LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE = 1; 92 int GESTURE_NAVIGATION_SETTINGS = 2; 93 } 94 AccessibilityShortcutsTutorial()95 private AccessibilityShortcutsTutorial() {} 96 97 private static final DialogInterface.OnClickListener ON_CLICK_LISTENER = 98 (DialogInterface dialog, int which) -> dialog.dismiss(); 99 100 /** 101 * Displays a dialog that guides users to use accessibility features with accessibility 102 * gestures under system gesture navigation mode. 103 */ showGestureNavigationTutorialDialog(Context context, DialogInterface.OnDismissListener onDismissListener)104 public static AlertDialog showGestureNavigationTutorialDialog(Context context, 105 DialogInterface.OnDismissListener onDismissListener) { 106 final AlertDialog alertDialog = new AlertDialog.Builder(context) 107 .setView(createTutorialDialogContentView(context, 108 DialogType.GESTURE_NAVIGATION_SETTINGS)) 109 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, ON_CLICK_LISTENER) 110 .setOnDismissListener(onDismissListener) 111 .create(); 112 113 alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 114 alertDialog.setCanceledOnTouchOutside(false); 115 alertDialog.show(); 116 117 return alertDialog; 118 } 119 showAccessibilityGestureTutorialDialog(Context context)120 static AlertDialog showAccessibilityGestureTutorialDialog(Context context) { 121 return createDialog(context, DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE); 122 } 123 createAccessibilityTutorialDialog( @onNull Context context, int shortcutTypes, @NonNull CharSequence featureName)124 static AlertDialog createAccessibilityTutorialDialog( 125 @NonNull Context context, int shortcutTypes, @NonNull CharSequence featureName) { 126 return createAccessibilityTutorialDialog( 127 context, shortcutTypes, ON_CLICK_LISTENER, featureName); 128 } 129 createAccessibilityTutorialDialog( @onNull Context context, int shortcutTypes, @Nullable DialogInterface.OnClickListener actionButtonListener, @NonNull CharSequence featureName)130 static AlertDialog createAccessibilityTutorialDialog( 131 @NonNull Context context, 132 int shortcutTypes, 133 @Nullable DialogInterface.OnClickListener actionButtonListener, 134 @NonNull CharSequence featureName) { 135 136 final int category = SettingsEnums.SWITCH_SHORTCUT_DIALOG_ACCESSIBILITY_BUTTON_SETTINGS; 137 final DialogInterface.OnClickListener linkButtonListener = 138 (dialog, which) -> new SubSettingLauncher(context) 139 .setDestination(AccessibilityButtonFragment.class.getName()) 140 .setSourceMetricsCategory(category) 141 .launch(); 142 143 final AlertDialog alertDialog = new AlertDialog.Builder(context) 144 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, 145 actionButtonListener) 146 .setNegativeButton(R.string.accessibility_tutorial_dialog_link_button, 147 linkButtonListener) 148 .create(); 149 150 final List<TutorialPage> tutorialPages = createShortcutTutorialPages( 151 context, shortcutTypes, featureName, /* isInSetupWizard= */ false); 152 Preconditions.checkArgument(!tutorialPages.isEmpty(), 153 /* errorMessage= */ "Unexpected tutorial pages size"); 154 155 final TutorialPageChangeListener.OnPageSelectedCallback callback = 156 index -> updateTutorialNegativeButtonTextAndVisibility( 157 alertDialog, tutorialPages, index); 158 159 alertDialog.setView(createShortcutNavigationContentView(context, tutorialPages, callback)); 160 161 // Showing first page won't invoke onPageSelectedCallback. Need to check the first tutorial 162 // page type manually to set correct visibility of the link button. 163 alertDialog.setOnShowListener( 164 dialog -> updateTutorialNegativeButtonTextAndVisibility( 165 alertDialog, tutorialPages, /* selectedPageIndex= */ 0)); 166 167 return alertDialog; 168 } 169 updateTutorialNegativeButtonTextAndVisibility( AlertDialog dialog, List<TutorialPage> pages, int selectedPageIndex)170 private static void updateTutorialNegativeButtonTextAndVisibility( 171 AlertDialog dialog, List<TutorialPage> pages, int selectedPageIndex) { 172 final Button button = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); 173 final int pageType = pages.get(selectedPageIndex).getType(); 174 final int buttonVisibility = pageType == UserShortcutType.SOFTWARE ? VISIBLE : GONE; 175 button.setVisibility(buttonVisibility); 176 if (buttonVisibility == VISIBLE) { 177 final int textResId = AccessibilityUtil.isFloatingMenuEnabled(dialog.getContext()) 178 ? R.string.accessibility_tutorial_dialog_link_button 179 : R.string.accessibility_tutorial_dialog_configure_software_shortcut_type; 180 button.setText(textResId); 181 } 182 } 183 createAccessibilityTutorialDialogForSetupWizard(Context context, int shortcutTypes, CharSequence featureName)184 static AlertDialog createAccessibilityTutorialDialogForSetupWizard(Context context, 185 int shortcutTypes, CharSequence featureName) { 186 return createAccessibilityTutorialDialogForSetupWizard(context, shortcutTypes, 187 ON_CLICK_LISTENER, featureName); 188 } 189 createAccessibilityTutorialDialogForSetupWizard( @onNull Context context, int shortcutTypes, @Nullable DialogInterface.OnClickListener actionButtonListener, @NonNull CharSequence featureName)190 static AlertDialog createAccessibilityTutorialDialogForSetupWizard( 191 @NonNull Context context, 192 int shortcutTypes, 193 @Nullable DialogInterface.OnClickListener actionButtonListener, 194 @NonNull CharSequence featureName) { 195 196 final AlertDialog alertDialog = new AlertDialog.Builder(context) 197 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, 198 actionButtonListener) 199 .create(); 200 201 final List<TutorialPage> tutorialPages = createShortcutTutorialPages( 202 context, shortcutTypes, featureName, /* inSetupWizard= */ true); 203 Preconditions.checkArgument(!tutorialPages.isEmpty(), 204 /* errorMessage= */ "Unexpected tutorial pages size"); 205 206 alertDialog.setView(createShortcutNavigationContentView(context, tutorialPages, null)); 207 208 return alertDialog; 209 } 210 211 /** 212 * Gets a content View for a dialog to confirm that they want to enable a service. 213 * 214 * @param context A valid context 215 * @param dialogType The type of tutorial dialog 216 * @return A content view suitable for viewing 217 */ createTutorialDialogContentView(Context context, int dialogType)218 private static View createTutorialDialogContentView(Context context, int dialogType) { 219 final LayoutInflater inflater = (LayoutInflater) context.getSystemService( 220 Context.LAYOUT_INFLATER_SERVICE); 221 222 View content = null; 223 224 switch (dialogType) { 225 case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON: 226 content = inflater.inflate( 227 R.layout.tutorial_dialog_launch_service_by_accessibility_button, null); 228 break; 229 case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE: 230 content = inflater.inflate( 231 R.layout.tutorial_dialog_launch_service_by_gesture_navigation, null); 232 setupGestureNavigationTextWithImage(context, content); 233 break; 234 case DialogType.GESTURE_NAVIGATION_SETTINGS: 235 content = inflater.inflate( 236 R.layout.tutorial_dialog_launch_by_gesture_navigation_settings, null); 237 setupGestureNavigationTextWithImage(context, content); 238 break; 239 } 240 241 return content; 242 } 243 setupGestureNavigationTextWithImage(Context context, View view)244 private static void setupGestureNavigationTextWithImage(Context context, View view) { 245 final boolean isTouchExploreEnabled = AccessibilityUtil.isTouchExploreEnabled(context); 246 247 final ImageView imageView = view.findViewById(R.id.image); 248 final int gestureSettingsImageResId = 249 isTouchExploreEnabled 250 ? R.drawable.accessibility_shortcut_type_gesture_preview_touch_explore_on 251 : R.drawable.accessibility_shortcut_type_gesture_preview; 252 imageView.setImageResource(gestureSettingsImageResId); 253 254 final TextView textView = view.findViewById(R.id.gesture_tutorial_message); 255 textView.setText(isTouchExploreEnabled 256 ? R.string.accessibility_tutorial_dialog_message_gesture_settings_talkback 257 : R.string.accessibility_tutorial_dialog_message_gesture_settings); 258 } 259 createDialog(Context context, int dialogType)260 private static AlertDialog createDialog(Context context, int dialogType) { 261 final AlertDialog alertDialog = new AlertDialog.Builder(context) 262 .setView(createTutorialDialogContentView(context, dialogType)) 263 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, ON_CLICK_LISTENER) 264 .create(); 265 266 alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 267 alertDialog.setCanceledOnTouchOutside(false); 268 alertDialog.show(); 269 270 return alertDialog; 271 } 272 273 private static class TutorialPagerAdapter extends PagerAdapter { 274 private final List<TutorialPage> mTutorialPages; TutorialPagerAdapter(List<TutorialPage> tutorialPages)275 private TutorialPagerAdapter(List<TutorialPage> tutorialPages) { 276 this.mTutorialPages = tutorialPages; 277 } 278 279 @NonNull 280 @Override instantiateItem(@onNull ViewGroup container, int position)281 public Object instantiateItem(@NonNull ViewGroup container, int position) { 282 final View itemView = mTutorialPages.get(position).getIllustrationView(); 283 container.addView(itemView); 284 return itemView; 285 } 286 287 @Override getCount()288 public int getCount() { 289 return mTutorialPages.size(); 290 } 291 292 @Override isViewFromObject(@onNull View view, @NonNull Object o)293 public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { 294 return view == o; 295 } 296 297 @Override destroyItem(@onNull ViewGroup container, int position, @NonNull Object object)298 public void destroyItem(@NonNull ViewGroup container, int position, 299 @NonNull Object object) { 300 final View itemView = mTutorialPages.get(position).getIllustrationView(); 301 container.removeView(itemView); 302 } 303 } 304 createImageView(Context context, int imageRes)305 private static ImageView createImageView(Context context, int imageRes) { 306 final ImageView imageView = new ImageView(context); 307 imageView.setImageResource(imageRes); 308 imageView.setAdjustViewBounds(true); 309 310 return imageView; 311 } 312 createIllustrationView(Context context, @DrawableRes int imageRes)313 private static View createIllustrationView(Context context, @DrawableRes int imageRes) { 314 final View illustrationFrame = inflateAndInitIllustrationFrame(context); 315 final LottieAnimationView lottieView = illustrationFrame.findViewById(R.id.image); 316 lottieView.setImageResource(imageRes); 317 318 return illustrationFrame; 319 } 320 createIllustrationViewWithImageRawResource(Context context, @RawRes int imageRawRes)321 private static View createIllustrationViewWithImageRawResource(Context context, 322 @RawRes int imageRawRes) { 323 final View illustrationFrame = inflateAndInitIllustrationFrame(context); 324 final LottieAnimationView lottieView = illustrationFrame.findViewById(R.id.image); 325 lottieView.setFailureListener( 326 result -> Log.w(TAG, "Invalid image raw resource id: " + imageRawRes, 327 result)); 328 lottieView.setAnimation(imageRawRes); 329 lottieView.setRepeatCount(LottieDrawable.INFINITE); 330 LottieColorUtils.applyDynamicColors(context, lottieView); 331 lottieView.playAnimation(); 332 333 return illustrationFrame; 334 } 335 inflateAndInitIllustrationFrame(Context context)336 private static View inflateAndInitIllustrationFrame(Context context) { 337 final LayoutInflater inflater = context.getSystemService(LayoutInflater.class); 338 339 return inflater.inflate(R.layout.accessibility_lottie_animation_view, /* root= */ null); 340 } 341 createShortcutNavigationContentView(Context context, List<TutorialPage> tutorialPages, TutorialPageChangeListener.OnPageSelectedCallback onPageSelectedCallback)342 private static View createShortcutNavigationContentView(Context context, 343 List<TutorialPage> tutorialPages, 344 TutorialPageChangeListener.OnPageSelectedCallback onPageSelectedCallback) { 345 346 final LayoutInflater inflater = context.getSystemService(LayoutInflater.class); 347 final View contentView = inflater.inflate( 348 R.layout.accessibility_shortcut_tutorial_dialog, /* root= */ null); 349 350 final LinearLayout indicatorContainer = contentView.findViewById(R.id.indicator_container); 351 indicatorContainer.setVisibility(tutorialPages.size() > 1 ? VISIBLE : GONE); 352 for (TutorialPage page : tutorialPages) { 353 indicatorContainer.addView(page.getIndicatorIcon()); 354 } 355 tutorialPages.get(/* firstIndex */ 0).getIndicatorIcon().setEnabled(true); 356 357 final TextSwitcher title = contentView.findViewById(R.id.title); 358 title.setFactory(() -> makeTitleView(context)); 359 title.setText(tutorialPages.get(/* firstIndex */ 0).getTitle()); 360 361 final TextSwitcher instruction = contentView.findViewById(R.id.instruction); 362 instruction.setFactory(() -> makeInstructionView(context)); 363 instruction.setText(tutorialPages.get(/* firstIndex */ 0).getInstruction()); 364 365 final ViewPager viewPager = contentView.findViewById(R.id.view_pager); 366 viewPager.setAdapter(new TutorialPagerAdapter(tutorialPages)); 367 viewPager.setContentDescription(context.getString(R.string.accessibility_tutorial_pager, 368 /* firstPage */ 1, tutorialPages.size())); 369 viewPager.setImportantForAccessibility(tutorialPages.size() > 1 370 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 371 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 372 373 TutorialPageChangeListener listener = new TutorialPageChangeListener(context, viewPager, 374 title, instruction, tutorialPages); 375 listener.setOnPageSelectedCallback(onPageSelectedCallback); 376 377 return contentView; 378 } 379 makeTitleView(Context context)380 private static View makeTitleView(Context context) { 381 final TextView textView = new TextView(context); 382 // Sets the text color, size, style, hint color, and highlight color from the specified 383 // TextAppearance resource. 384 TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogTitle); 385 textView.setGravity(Gravity.CENTER); 386 return textView; 387 } 388 makeInstructionView(Context context)389 private static View makeInstructionView(Context context) { 390 final TextView textView = new TextView(context); 391 TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogDescription); 392 return textView; 393 } 394 createSoftwareTutorialPage(@onNull Context context)395 private static TutorialPage createSoftwareTutorialPage(@NonNull Context context) { 396 final int type = UserShortcutType.SOFTWARE; 397 final CharSequence title = getSoftwareTitle(context); 398 final View image = createSoftwareImage(context); 399 final CharSequence instruction = getSoftwareInstruction(context); 400 final ImageView indicatorIcon = 401 createImageView(context, R.drawable.ic_accessibility_page_indicator); 402 indicatorIcon.setEnabled(false); 403 404 return new TutorialPage(type, title, image, indicatorIcon, instruction); 405 } 406 createHardwareTutorialPage(@onNull Context context)407 private static TutorialPage createHardwareTutorialPage(@NonNull Context context) { 408 final int type = UserShortcutType.HARDWARE; 409 final CharSequence title = 410 context.getText(R.string.accessibility_tutorial_dialog_title_volume); 411 final View image = 412 createIllustrationView(context, R.drawable.accessibility_shortcut_type_volume_keys); 413 final ImageView indicatorIcon = 414 createImageView(context, R.drawable.ic_accessibility_page_indicator); 415 final CharSequence instruction = 416 context.getText(R.string.accessibility_tutorial_dialog_message_volume); 417 indicatorIcon.setEnabled(false); 418 419 return new TutorialPage(type, title, image, indicatorIcon, instruction); 420 } 421 createTripleTapTutorialPage(@onNull Context context)422 private static TutorialPage createTripleTapTutorialPage(@NonNull Context context) { 423 final int type = UserShortcutType.TRIPLETAP; 424 final CharSequence title = 425 context.getText(R.string.accessibility_tutorial_dialog_title_triple); 426 final View image = 427 createIllustrationViewWithImageRawResource(context, 428 R.raw.accessibility_shortcut_type_tripletap); 429 final CharSequence instruction = context.getString( 430 R.string.accessibility_tutorial_dialog_tripletap_instruction, 3); 431 final ImageView indicatorIcon = 432 createImageView(context, R.drawable.ic_accessibility_page_indicator); 433 indicatorIcon.setEnabled(false); 434 435 return new TutorialPage(type, title, image, indicatorIcon, instruction); 436 } 437 createTwoFingerTripleTapTutorialPage(@onNull Context context)438 private static TutorialPage createTwoFingerTripleTapTutorialPage(@NonNull Context context) { 439 final int type = UserShortcutType.TWOFINGER_DOUBLETAP; 440 final int numFingers = 2; 441 final CharSequence title = context.getString( 442 R.string.accessibility_tutorial_dialog_title_two_finger_double, numFingers); 443 final View image = 444 createIllustrationViewWithImageRawResource(context, 445 R.raw.accessibility_shortcut_type_2finger_doubletap); 446 final CharSequence instruction = context.getString( 447 R.string.accessibility_tutorial_dialog_twofinger_doubletap_instruction, numFingers); 448 final ImageView indicatorIcon = 449 createImageView(context, R.drawable.ic_accessibility_page_indicator); 450 indicatorIcon.setEnabled(false); 451 452 return new TutorialPage(type, title, image, indicatorIcon, instruction); 453 } 454 createQuickSettingsTutorialPage( @onNull Context context, @NonNull CharSequence featureName, boolean inSetupWizard)455 private static TutorialPage createQuickSettingsTutorialPage( 456 @NonNull Context context, @NonNull CharSequence featureName, boolean inSetupWizard) { 457 final int type = UserShortcutType.QUICK_SETTINGS; 458 final CharSequence title = 459 context.getText(R.string.accessibility_tutorial_dialog_title_quick_setting); 460 final View image = 461 createIllustrationView(context, 462 R.drawable.accessibility_shortcut_type_quick_settings); 463 // Remove the unneeded background, since the main image already includes a background 464 image.findViewById(R.id.image_background).setVisibility(GONE); 465 final int numFingers = AccessibilityUtil.isTouchExploreEnabled(context) ? 2 : 1; 466 Map<String, Object> arguments = new ArrayMap<>(); 467 arguments.put("count", numFingers); 468 arguments.put("featureName", featureName); 469 final CharSequence instruction = StringUtil.getIcuPluralsString(context, 470 arguments, 471 R.string.accessibility_tutorial_dialog_message_quick_setting); 472 final SpannableStringBuilder tutorialText = new SpannableStringBuilder(); 473 if (inSetupWizard) { 474 tutorialText.append(context.getText( 475 R.string.accessibility_tutorial_dialog_shortcut_unavailable_in_suw)) 476 .append("\n\n"); 477 } 478 tutorialText.append(instruction); 479 final ImageView indicatorIcon = 480 createImageView(context, R.drawable.ic_accessibility_page_indicator); 481 indicatorIcon.setEnabled(false); 482 483 return new TutorialPage(type, title, image, indicatorIcon, tutorialText); 484 } 485 486 /** 487 * Create the tutorial pages for selected shortcut types in the same order as shown in the 488 * edit shortcut screen. 489 */ 490 @VisibleForTesting createShortcutTutorialPages( @onNull Context context, int shortcutTypes, @NonNull CharSequence featureName, boolean inSetupWizard)491 static List<TutorialPage> createShortcutTutorialPages( 492 @NonNull Context context, int shortcutTypes, @NonNull CharSequence featureName, 493 boolean inSetupWizard) { 494 // LINT.IfChange(shortcut_type_ui_order) 495 final List<TutorialPage> tutorialPages = new ArrayList<>(); 496 if (android.view.accessibility.Flags.a11yQsShortcut()) { 497 if ((shortcutTypes & UserShortcutType.QUICK_SETTINGS) 498 == UserShortcutType.QUICK_SETTINGS) { 499 tutorialPages.add( 500 createQuickSettingsTutorialPage(context, featureName, inSetupWizard)); 501 } 502 } 503 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 504 tutorialPages.add(createSoftwareTutorialPage(context)); 505 } 506 507 if ((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE) { 508 tutorialPages.add(createHardwareTutorialPage(context)); 509 } 510 511 if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) { 512 if ((shortcutTypes & UserShortcutType.TWOFINGER_DOUBLETAP) 513 == UserShortcutType.TWOFINGER_DOUBLETAP) { 514 tutorialPages.add(createTwoFingerTripleTapTutorialPage(context)); 515 } 516 } 517 518 if ((shortcutTypes & UserShortcutType.TRIPLETAP) == UserShortcutType.TRIPLETAP) { 519 tutorialPages.add(createTripleTapTutorialPage(context)); 520 } 521 // LINT.ThenChange(/res/xml/accessibility_edit_shortcuts.xml:shortcut_type_ui_order) 522 523 return tutorialPages; 524 } 525 createSoftwareImage(Context context)526 private static View createSoftwareImage(Context context) { 527 int resId; 528 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 529 return createIllustrationViewWithImageRawResource( 530 context, R.raw.accessibility_shortcut_type_fab); 531 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 532 resId = AccessibilityUtil.isTouchExploreEnabled(context) 533 ? R.drawable.accessibility_shortcut_type_gesture_touch_explore_on 534 : R.drawable.accessibility_shortcut_type_gesture; 535 } else { 536 resId = R.drawable.accessibility_shortcut_type_navbar; 537 } 538 return createIllustrationView(context, resId); 539 } 540 getSoftwareTitle(Context context)541 private static CharSequence getSoftwareTitle(Context context) { 542 int resId; 543 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 544 resId = R.string.accessibility_tutorial_dialog_title_button; 545 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 546 resId = R.string.accessibility_tutorial_dialog_title_gesture; 547 } else { 548 resId = R.string.accessibility_tutorial_dialog_title_button; 549 } 550 return context.getText(resId); 551 } 552 getSoftwareInstruction(Context context)553 private static CharSequence getSoftwareInstruction(Context context) { 554 final SpannableStringBuilder sb = new SpannableStringBuilder(); 555 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 556 final int resId = R.string.accessibility_tutorial_dialog_message_floating_button; 557 sb.append(context.getText(resId)); 558 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 559 final int numFingers = AccessibilityUtil.isTouchExploreEnabled(context) ? 3 : 2; 560 sb.append(StringUtil.getIcuPluralsString( 561 context, 562 numFingers, 563 R.string.accessibility_tutorial_dialog_gesture_shortcut_instruction)); 564 } else { 565 final int resId = R.string.accessibility_tutorial_dialog_message_button; 566 sb.append(getSoftwareInstructionWithIcon(context, context.getText(resId))); 567 } 568 return sb; 569 } 570 getSoftwareInstructionWithIcon(Context context, CharSequence text)571 private static CharSequence getSoftwareInstructionWithIcon(Context context, CharSequence text) { 572 final String message = text.toString(); 573 final SpannableString spannableInstruction = SpannableString.valueOf(message); 574 final int indexIconStart = message.indexOf("%s"); 575 final int indexIconEnd = indexIconStart + 2; 576 final ImageView iconView = new ImageView(context); 577 iconView.setImageDrawable(context.getDrawable(R.drawable.ic_accessibility_new)); 578 final Drawable icon = iconView.getDrawable().mutate(); 579 final ImageSpan imageSpan = new ImageSpan(icon); 580 imageSpan.setContentDescription(""); 581 icon.setBounds(/* left= */ 0, /* top= */ 0, 582 icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); 583 spannableInstruction.setSpan(imageSpan, indexIconStart, indexIconEnd, 584 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 585 586 return spannableInstruction; 587 } 588 589 private static class TutorialPage { 590 private final int mType; 591 private final CharSequence mTitle; 592 private final View mIllustrationView; 593 private final ImageView mIndicatorIcon; 594 private final CharSequence mInstruction; 595 TutorialPage(int type, CharSequence title, View illustrationView, ImageView indicatorIcon, CharSequence instruction)596 TutorialPage(int type, CharSequence title, View illustrationView, ImageView indicatorIcon, 597 CharSequence instruction) { 598 this.mType = type; 599 this.mTitle = title; 600 this.mIllustrationView = illustrationView; 601 this.mIndicatorIcon = indicatorIcon; 602 this.mInstruction = instruction; 603 604 setupIllustrationChildViewsGravity(); 605 } 606 getType()607 public int getType() { 608 return mType; 609 } 610 getTitle()611 public CharSequence getTitle() { 612 return mTitle; 613 } 614 getIllustrationView()615 public View getIllustrationView() { 616 return mIllustrationView; 617 } 618 getIndicatorIcon()619 public ImageView getIndicatorIcon() { 620 return mIndicatorIcon; 621 } 622 getInstruction()623 public CharSequence getInstruction() { 624 return mInstruction; 625 } 626 setupIllustrationChildViewsGravity()627 private void setupIllustrationChildViewsGravity() { 628 final View backgroundView = mIllustrationView.findViewById(R.id.image_background); 629 initViewGravity(backgroundView); 630 631 final View lottieView = mIllustrationView.findViewById(R.id.image); 632 initViewGravity(lottieView); 633 } 634 initViewGravity(@onNull View view)635 private void initViewGravity(@NonNull View view) { 636 final FrameLayout.LayoutParams layoutParams = 637 new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, 638 FrameLayout.LayoutParams.WRAP_CONTENT); 639 layoutParams.gravity = Gravity.CENTER; 640 641 view.setLayoutParams(layoutParams); 642 } 643 } 644 645 private static class TutorialPageChangeListener implements ViewPager.OnPageChangeListener { 646 private int mLastTutorialPagePosition = 0; 647 private final Context mContext; 648 private final TextSwitcher mTitle; 649 private final TextSwitcher mInstruction; 650 private final List<TutorialPage> mTutorialPages; 651 private final ViewPager mViewPager; 652 private OnPageSelectedCallback mOnPageSelectedCallback; 653 TutorialPageChangeListener(Context context, ViewPager viewPager, ViewGroup title, ViewGroup instruction, List<TutorialPage> tutorialPages)654 TutorialPageChangeListener(Context context, ViewPager viewPager, ViewGroup title, 655 ViewGroup instruction, List<TutorialPage> tutorialPages) { 656 this.mContext = context; 657 this.mViewPager = viewPager; 658 this.mTitle = (TextSwitcher) title; 659 this.mInstruction = (TextSwitcher) instruction; 660 this.mTutorialPages = tutorialPages; 661 this.mOnPageSelectedCallback = null; 662 663 this.mViewPager.addOnPageChangeListener(this); 664 } 665 setOnPageSelectedCallback( OnPageSelectedCallback callback)666 public void setOnPageSelectedCallback( 667 OnPageSelectedCallback callback) { 668 this.mOnPageSelectedCallback = callback; 669 } 670 671 @Override onPageScrolled(int position, float positionOffset, int positionOffsetPixels)672 public void onPageScrolled(int position, float positionOffset, 673 int positionOffsetPixels) { 674 // Do nothing. 675 } 676 677 @Override onPageSelected(int position)678 public void onPageSelected(int position) { 679 final boolean isPreviousPosition = 680 mLastTutorialPagePosition > position; 681 @AnimRes 682 final int inAnimationResId = isPreviousPosition 683 ? android.R.anim.slide_in_left 684 : com.android.internal.R.anim.slide_in_right; 685 686 @AnimRes 687 final int outAnimationResId = isPreviousPosition 688 ? android.R.anim.slide_out_right 689 : com.android.internal.R.anim.slide_out_left; 690 691 mTitle.setInAnimation(mContext, inAnimationResId); 692 mTitle.setOutAnimation(mContext, outAnimationResId); 693 mTitle.setText(mTutorialPages.get(position).getTitle()); 694 695 mInstruction.setInAnimation(mContext, inAnimationResId); 696 mInstruction.setOutAnimation(mContext, outAnimationResId); 697 mInstruction.setText(mTutorialPages.get(position).getInstruction()); 698 699 for (TutorialPage page : mTutorialPages) { 700 page.getIndicatorIcon().setEnabled(false); 701 } 702 mTutorialPages.get(position).getIndicatorIcon().setEnabled(true); 703 mLastTutorialPagePosition = position; 704 705 final int currentPageNumber = position + 1; 706 mViewPager.setContentDescription( 707 mContext.getString(R.string.accessibility_tutorial_pager, 708 currentPageNumber, mTutorialPages.size())); 709 710 if (mOnPageSelectedCallback != null) { 711 mOnPageSelectedCallback.onPageSelected(position); 712 } 713 } 714 715 @Override onPageScrollStateChanged(int state)716 public void onPageScrollStateChanged(int state) { 717 // Do nothing. 718 } 719 720 /** The interface that provides a callback method after tutorial page is selected. */ 721 private interface OnPageSelectedCallback { 722 723 /** The callback method after tutorial page is selected. */ onPageSelected(int index)724 void onPageSelected(int index); 725 } 726 } 727 } 728