• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 package com.android.quickstep.interaction;
17 
18 import static android.view.View.GONE;
19 
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.graphics.drawable.Animatable2;
23 import android.graphics.drawable.AnimatedVectorDrawable;
24 import android.graphics.drawable.ColorDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.RippleDrawable;
27 import android.util.Log;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.accessibility.AccessibilityEvent;
31 import android.widget.Button;
32 import android.widget.ImageView;
33 import android.widget.RelativeLayout;
34 import android.widget.TextView;
35 
36 import androidx.annotation.CallSuper;
37 import androidx.annotation.DrawableRes;
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 import androidx.annotation.StringRes;
41 import androidx.appcompat.app.AlertDialog;
42 import androidx.appcompat.content.res.AppCompatResources;
43 
44 import com.android.launcher3.R;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.anim.AnimatorListeners;
47 import com.android.launcher3.views.ClipIconView;
48 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
49 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
50 
51 abstract class TutorialController implements BackGestureAttemptCallback,
52         NavBarGestureAttemptCallback {
53 
54     private static final String TAG = "TutorialController";
55 
56     private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips";
57     private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips";
58 
59     private static final int FEEDBACK_ANIMATION_MS = 250;
60     private static final int RIPPLE_VISIBLE_MS = 300;
61     private static final int GESTURE_ANIMATION_DELAY_MS = 1500;
62     private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 4000;
63 
64     final TutorialFragment mTutorialFragment;
65     TutorialType mTutorialType;
66     final Context mContext;
67 
68     final TextView mCloseButton;
69     final ViewGroup mFeedbackView;
70     final TextView mFeedbackTitleView;
71     final ImageView mFeedbackVideoView;
72     final ImageView mGestureVideoView;
73     final RelativeLayout mFakeLauncherView;
74     final ImageView mFakeHotseatView;
75     final ClipIconView mFakeIconView;
76     final View mFakeTaskView;
77     final View mFakePreviousTaskView;
78     final View mRippleView;
79     final RippleDrawable mRippleDrawable;
80     final Button mActionButton;
81     final TutorialStepIndicator mTutorialStepView;
82     private final AlertDialog mSkipTutorialDialog;
83 
84     protected boolean mGestureCompleted = false;
85 
86     // These runnables  should be used when posting callbacks to their views and cleared from their
87     // views before posting new callbacks.
88     private final Runnable mTitleViewCallback;
89     @Nullable private Runnable mFeedbackViewCallback;
90     @Nullable private Runnable mFeedbackVideoViewCallback;
91 
TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)92     TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
93         mTutorialFragment = tutorialFragment;
94         mTutorialType = tutorialType;
95         mContext = mTutorialFragment.getContext();
96 
97         RootSandboxLayout rootView = tutorialFragment.getRootView();
98         mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
99         mCloseButton.setOnClickListener(button -> showSkipTutorialDialog());
100         mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
101         mFeedbackTitleView = mFeedbackView.findViewById(
102                 R.id.gesture_tutorial_fragment_feedback_title);
103         mFeedbackVideoView = rootView.findViewById(R.id.gesture_tutorial_feedback_video);
104         mGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_gesture_video);
105         mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view);
106         mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view);
107         mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
108         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
109         mFakePreviousTaskView =
110                 rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view);
111         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
112         mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
113         mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
114         mTutorialStepView =
115                 rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step);
116         mSkipTutorialDialog = createSkipTutorialDialog();
117 
118         mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
119                 AccessibilityEvent.TYPE_VIEW_FOCUSED);
120     }
121 
showSkipTutorialDialog()122     private void showSkipTutorialDialog() {
123         if (mSkipTutorialDialog != null) {
124             mSkipTutorialDialog.show();
125         }
126     }
127 
setTutorialType(TutorialType tutorialType)128     void setTutorialType(TutorialType tutorialType) {
129         mTutorialType = tutorialType;
130     }
131 
132     @DrawableRes
getMockHotseatResId()133     protected int getMockHotseatResId() {
134         return R.drawable.default_sandbox_mock_launcher;
135     }
136 
137     @DrawableRes
getMockAppTaskThumbnailResId(boolean forDarkMode)138     protected int getMockAppTaskThumbnailResId(boolean forDarkMode) {
139         return R.drawable.default_sandbox_app_task_thumbnail;
140     }
141 
142     @DrawableRes
getMockPreviousAppTaskThumbnailResId()143     protected int getMockPreviousAppTaskThumbnailResId() {
144         return R.drawable.default_sandbox_app_previous_task_thumbnail;
145     }
146 
147     @DrawableRes
getMockAppIconResId()148     public int getMockAppIconResId() {
149         return R.drawable.default_sandbox_app_icon;
150     }
151 
152     @DrawableRes
getMockWallpaperResId()153     public int getMockWallpaperResId() {
154         return R.drawable.default_sandbox_wallpaper;
155     }
156 
fadeTaskViewAndRun(Runnable r)157     void fadeTaskViewAndRun(Runnable r) {
158         mFakeTaskView.animate().alpha(0).setListener(AnimatorListeners.forSuccessCallback(r));
159     }
160 
161     @StringRes
getIntroductionTitle()162     public Integer getIntroductionTitle() {
163         return null;
164     }
165 
166     @StringRes
getIntroductionSubtitle()167     public Integer getIntroductionSubtitle() {
168         return null;
169     }
170 
showFeedback()171     void showFeedback() {
172         if (mGestureCompleted) {
173             mFeedbackView.setTranslationY(0);
174             return;
175         }
176         AnimatedVectorDrawable tutorialAnimation = mTutorialFragment.getTutorialAnimation();
177         AnimatedVectorDrawable gestureAnimation = mTutorialFragment.getGestureAnimation();
178 
179         if (tutorialAnimation != null && gestureAnimation != null) {
180             TextView title = mFeedbackView.findViewById(
181                     R.id.gesture_tutorial_fragment_feedback_title);
182 
183             playFeedbackVideo(tutorialAnimation, gestureAnimation, () -> {
184                 mFeedbackView.setTranslationY(0);
185                 title.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
186             }, true);
187         }
188     }
189 
190     /**
191      * Show feedback reflecting a failed gesture attempt.
192      *
193      * @param subtitleResId Resource of the text to display.
194      **/
showFeedback(int subtitleResId)195     void showFeedback(int subtitleResId) {
196         showFeedback(subtitleResId, false);
197     }
198 
199     /**
200      * Show feedback reflecting the result of a gesture attempt.
201      *
202      * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown.
203      **/
showFeedback(int subtitleResId, boolean isGestureSuccessful)204     void showFeedback(int subtitleResId, boolean isGestureSuccessful) {
205         showFeedback(
206                 isGestureSuccessful
207                         ? R.string.gesture_tutorial_nice : R.string.gesture_tutorial_try_again,
208                 subtitleResId,
209                 isGestureSuccessful,
210                 false);
211     }
212 
showFeedback( int titleResId, int subtitleResId, boolean isGestureSuccessful, boolean useGestureAnimationDelay)213     void showFeedback(
214             int titleResId,
215             int subtitleResId,
216             boolean isGestureSuccessful,
217             boolean useGestureAnimationDelay) {
218         mFeedbackTitleView.setText(titleResId);
219         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
220         TextView subtitle =
221                 mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_subtitle);
222         subtitle.setText(subtitleResId);
223         if (isGestureSuccessful) {
224             hideCloseButton();
225             if (mTutorialFragment.isAtFinalStep()) {
226                 showActionButton();
227             }
228 
229             if (mFeedbackVideoViewCallback != null) {
230                 mFeedbackVideoView.removeCallbacks(mFeedbackVideoViewCallback);
231                 mFeedbackVideoViewCallback = null;
232             }
233         }
234         mGestureCompleted = isGestureSuccessful;
235 
236         AnimatedVectorDrawable tutorialAnimation = mTutorialFragment.getTutorialAnimation();
237         AnimatedVectorDrawable gestureAnimation = mTutorialFragment.getGestureAnimation();
238         if (tutorialAnimation != null && gestureAnimation != null) {
239             if (!isGestureSuccessful) {
240                 playFeedbackVideo(tutorialAnimation, gestureAnimation, () -> {
241                     mFeedbackView.setTranslationY(
242                             -mFeedbackView.getHeight() - mFeedbackView.getTop());
243                     mFeedbackView.setVisibility(View.VISIBLE);
244                     mFeedbackView.animate()
245                             .setDuration(FEEDBACK_ANIMATION_MS)
246                             .translationY(0)
247                             .start();
248                     mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS);
249                 }, useGestureAnimationDelay);
250                 return;
251             } else {
252                 mTutorialFragment.releaseFeedbackVideoView();
253             }
254         }
255         mFeedbackView.setTranslationY(-mFeedbackView.getHeight() - mFeedbackView.getTop());
256         mFeedbackView.setVisibility(View.VISIBLE);
257         mFeedbackView.animate()
258                 .setDuration(FEEDBACK_ANIMATION_MS)
259                 .translationY(0)
260                 .withEndAction(() -> {
261                     if (isGestureSuccessful && !mTutorialFragment.isAtFinalStep()) {
262                         if (mFeedbackViewCallback != null) {
263                             mFeedbackView.removeCallbacks(mFeedbackViewCallback);
264                         }
265                         mFeedbackViewCallback = mTutorialFragment::continueTutorial;
266                         mFeedbackView.postDelayed(mFeedbackViewCallback,
267                                 ADVANCE_TUTORIAL_TIMEOUT_MS);
268                     }
269                 })
270                 .start();
271         mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS);
272     }
273 
hideFeedback(boolean releaseFeedbackVideo)274     void hideFeedback(boolean releaseFeedbackVideo) {
275         mFeedbackView.clearAnimation();
276         mFeedbackView.setVisibility(View.INVISIBLE);
277         if (releaseFeedbackVideo) {
278             mTutorialFragment.releaseFeedbackVideoView();
279         }
280     }
281 
playFeedbackVideo( @onNull AnimatedVectorDrawable tutorialAnimation, @NonNull AnimatedVectorDrawable gestureAnimation, @NonNull Runnable onStartRunnable, boolean useGestureAnimationDelay)282     private void playFeedbackVideo(
283             @NonNull AnimatedVectorDrawable tutorialAnimation,
284             @NonNull AnimatedVectorDrawable gestureAnimation,
285             @NonNull Runnable onStartRunnable,
286             boolean useGestureAnimationDelay) {
287 
288         if (tutorialAnimation.isRunning()) {
289             tutorialAnimation.reset();
290         }
291         tutorialAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
292 
293             @Override
294             public void onAnimationStart(Drawable drawable) {
295                 super.onAnimationStart(drawable);
296 
297                 mGestureVideoView.setVisibility(GONE);
298                 if (gestureAnimation.isRunning()) {
299                     gestureAnimation.stop();
300                 }
301 
302                 if (!useGestureAnimationDelay) {
303                     onStartRunnable.run();
304                 }
305             }
306 
307             @Override
308             public void onAnimationEnd(Drawable drawable) {
309                 super.onAnimationEnd(drawable);
310 
311                 mGestureVideoView.setVisibility(View.VISIBLE);
312                 gestureAnimation.start();
313 
314                 tutorialAnimation.unregisterAnimationCallback(this);
315             }
316         });
317 
318         if (mFeedbackViewCallback != null) {
319             mFeedbackVideoView.removeCallbacks(mFeedbackViewCallback);
320             mFeedbackViewCallback = null;
321         }
322         if (mFeedbackVideoViewCallback != null) {
323             mFeedbackVideoView.removeCallbacks(mFeedbackVideoViewCallback);
324             mFeedbackVideoViewCallback = null;
325         }
326         if (useGestureAnimationDelay) {
327             mFeedbackViewCallback = onStartRunnable;
328             mFeedbackVideoViewCallback = () -> {
329                 mFeedbackVideoView.setVisibility(View.VISIBLE);
330                 tutorialAnimation.start();
331             };
332 
333             mFeedbackVideoView.setVisibility(View.GONE);
334             mFeedbackView.post(mFeedbackViewCallback);
335             mFeedbackVideoView.postDelayed(mFeedbackVideoViewCallback, GESTURE_ANIMATION_DELAY_MS);
336         } else {
337             mFeedbackVideoView.setVisibility(View.VISIBLE);
338             tutorialAnimation.start();
339         }
340     }
341 
setRippleHotspot(float x, float y)342     void setRippleHotspot(float x, float y) {
343         mRippleDrawable.setHotspot(x, y);
344     }
345 
showRippleEffect(@ullable Runnable onCompleteRunnable)346     void showRippleEffect(@Nullable Runnable onCompleteRunnable) {
347         mRippleDrawable.setState(
348                 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled});
349         mRippleView.postDelayed(() -> {
350             mRippleDrawable.setState(new int[] {});
351             if (onCompleteRunnable != null) {
352                 onCompleteRunnable.run();
353             }
354         }, RIPPLE_VISIBLE_MS);
355     }
356 
onActionButtonClicked(View button)357     void onActionButtonClicked(View button) {
358         mTutorialFragment.continueTutorial();
359     }
360 
361     @CallSuper
transitToController()362     void transitToController() {
363         hideFeedback(false);
364         hideActionButton();
365         updateSubtext();
366         updateDrawables();
367 
368         mGestureCompleted = false;
369         if (mFakeHotseatView != null) {
370             mFakeHotseatView.setVisibility(View.INVISIBLE);
371         }
372     }
373 
hideCloseButton()374     void hideCloseButton() {
375         mCloseButton.setVisibility(GONE);
376     }
377 
showCloseButton()378     void showCloseButton() {
379         mCloseButton.setVisibility(View.VISIBLE);
380         mCloseButton.setTextAppearance(Utilities.isDarkTheme(mContext)
381                 ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext
382                 : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark);
383     }
384 
hideActionButton()385     void hideActionButton() {
386         showCloseButton();
387         // Invisible to maintain the layout.
388         mActionButton.setVisibility(View.INVISIBLE);
389         mActionButton.setOnClickListener(null);
390     }
391 
showActionButton()392     void showActionButton() {
393         hideCloseButton();
394         mActionButton.setVisibility(View.VISIBLE);
395         mActionButton.setOnClickListener(this::onActionButtonClicked);
396     }
397 
updateSubtext()398     private void updateSubtext() {
399         mTutorialStepView.setTutorialProgress(
400                 mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps());
401     }
402 
updateDrawables()403     private void updateDrawables() {
404         if (mContext != null) {
405             mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
406                     mContext, getMockWallpaperResId()));
407             mTutorialFragment.updateFeedbackVideo();
408             mFakeLauncherView.setBackgroundColor(
409                     mContext.getColor(Utilities.isDarkTheme(mContext)
410                             ? R.color.fake_wallpaper_color_dark_mode
411                             : R.color.fake_wallpaper_color_light_mode));
412             mFakeHotseatView.setImageDrawable(AppCompatResources.getDrawable(
413                     mContext, getMockHotseatResId()));
414             mFakeTaskView.setBackground(AppCompatResources.getDrawable(
415                     mContext, getMockAppTaskThumbnailResId(Utilities.isDarkTheme(mContext))));
416             mFakeTaskView.animate().alpha(1).setListener(
417                     AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
418             mFakePreviousTaskView.setBackground(AppCompatResources.getDrawable(
419                     mContext, getMockPreviousAppTaskThumbnailResId()));
420             mFakeIconView.setBackground(AppCompatResources.getDrawable(
421                     mContext, getMockAppIconResId()));
422         }
423     }
424 
createSkipTutorialDialog()425     private AlertDialog createSkipTutorialDialog() {
426         if (mContext instanceof GestureSandboxActivity) {
427             GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext;
428             View contentView = View.inflate(
429                     sandboxActivity, R.layout.gesture_tutorial_dialog, null);
430             AlertDialog tutorialDialog = new AlertDialog
431                     .Builder(sandboxActivity, R.style.Theme_AppCompat_Dialog_Alert)
432                     .setView(contentView)
433                     .create();
434 
435             PackageManager packageManager = mContext.getPackageManager();
436             CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME;
437 
438             try {
439                 tipsAppName = packageManager.getApplicationLabel(
440                         packageManager.getApplicationInfo(
441                                 PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA));
442             } catch (PackageManager.NameNotFoundException e) {
443                 Log.e(TAG,
444                         "Could not find app label for package name: "
445                                 + PIXEL_TIPS_APP_PACKAGE_NAME
446                                 + ". Defaulting to 'Pixel Tips.'",
447                         e);
448             }
449 
450             TextView subtitleTextView = (TextView) contentView.findViewById(
451                     R.id.gesture_tutorial_dialog_subtitle);
452             if (subtitleTextView != null) {
453                 subtitleTextView.setText(
454                         mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName));
455             } else {
456                 Log.w(TAG, "No subtitle view in the skip tutorial dialog to update.");
457             }
458 
459             Button cancelButton = (Button) contentView.findViewById(
460                     R.id.gesture_tutorial_dialog_cancel_button);
461             if (cancelButton != null) {
462                 cancelButton.setOnClickListener(
463                         v -> tutorialDialog.dismiss());
464             } else {
465                 Log.w(TAG, "No cancel button in the skip tutorial dialog to update.");
466             }
467 
468             Button confirmButton = contentView.findViewById(
469                     R.id.gesture_tutorial_dialog_confirm_button);
470             if (confirmButton != null) {
471                 confirmButton.setOnClickListener(v -> {
472                     sandboxActivity.closeTutorial();
473                     tutorialDialog.dismiss();
474                 });
475             } else {
476                 Log.w(TAG, "No confirm button in the skip tutorial dialog to update.");
477             }
478 
479             tutorialDialog.getWindow().setBackgroundDrawable(
480                     new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent)));
481 
482             return tutorialDialog;
483         }
484 
485         return null;
486     }
487 
488     /** Denotes the type of the tutorial. */
489     enum TutorialType {
490         BACK_NAVIGATION,
491         BACK_NAVIGATION_COMPLETE,
492         HOME_NAVIGATION,
493         HOME_NAVIGATION_COMPLETE,
494         OVERVIEW_NAVIGATION,
495         OVERVIEW_NAVIGATION_COMPLETE,
496         ASSISTANT,
497         ASSISTANT_COMPLETE,
498         SANDBOX_MODE
499     }
500 }
501