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