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 android.content.SharedPreferences;
19 import android.content.pm.ActivityInfo;
20 import android.content.res.Configuration;
21 import android.graphics.Color;
22 import android.graphics.Insets;
23 import android.graphics.Rect;
24 import android.os.Bundle;
25 import android.text.TextUtils;
26 import android.util.DisplayMetrics;
27 import android.view.Display;
28 import android.view.View;
29 import android.view.Window;
30 import android.view.WindowInsets;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.fragment.app.FragmentActivity;
35 
36 import com.android.launcher3.DeviceProfile;
37 import com.android.launcher3.InvariantDeviceProfile;
38 import com.android.launcher3.LauncherPrefs;
39 import com.android.launcher3.R;
40 import com.android.launcher3.config.FeatureFlags;
41 import com.android.launcher3.logging.StatsLogManager;
42 import com.android.quickstep.TouchInteractionService.TISBinder;
43 import com.android.quickstep.interaction.TutorialController.TutorialType;
44 import com.android.quickstep.util.TISBindHelper;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 
49 /** Shows the gesture interactive sandbox in full screen mode. */
50 public class GestureSandboxActivity extends FragmentActivity {
51 
52     private static final String KEY_TUTORIAL_STEPS = "tutorial_steps";
53     private static final String KEY_CURRENT_STEP = "current_step";
54     static final String KEY_TUTORIAL_TYPE = "tutorial_type";
55     static final String KEY_GESTURE_COMPLETE = "gesture_complete";
56     static final String KEY_USE_TUTORIAL_MENU = "use_tutorial_menu";
57     public static final double SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95;
58     public static final double SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05;
59 
60     @Nullable private TutorialType[] mTutorialSteps;
61     private GestureSandboxFragment mCurrentFragment;
62     private GestureSandboxFragment mPendingFragment;
63 
64     private int mCurrentStep;
65     private int mNumSteps;
66 
67     private SharedPreferences mSharedPrefs;
68     private StatsLogManager mStatsLogManager;
69     private TISBindHelper mTISBindHelper;
70 
71     @Override
onCreate(Bundle savedInstanceState)72     protected void onCreate(Bundle savedInstanceState) {
73         super.onCreate(savedInstanceState);
74         requestWindowFeature(Window.FEATURE_NO_TITLE);
75         setContentView(R.layout.gesture_tutorial_activity);
76 
77         mSharedPrefs = LauncherPrefs.getPrefs(this);
78         mStatsLogManager = StatsLogManager.newInstance(getApplicationContext());
79 
80         Bundle args = savedInstanceState == null ? getIntent().getExtras() : savedInstanceState;
81 
82         boolean gestureComplete = args != null && args.getBoolean(KEY_GESTURE_COMPLETE, false);
83         if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
84                 && args != null
85                 && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) {
86             mTutorialSteps = null;
87             TutorialType tutorialTypeOverride = (TutorialType) args.get(KEY_TUTORIAL_TYPE);
88             mCurrentFragment = tutorialTypeOverride == null
89                     ? new MenuFragment()
90                     : makeTutorialFragment(
91                             tutorialTypeOverride,
92                             gestureComplete,
93                             /* fromMenu= */ true);
94         } else {
95             mTutorialSteps = getTutorialSteps(args);
96             mCurrentFragment = makeTutorialFragment(
97                     mTutorialSteps[mCurrentStep - 1],
98                     gestureComplete,
99                     /* fromMenu= */ false);
100         }
101         getSupportFragmentManager().beginTransaction()
102                 .add(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
103                 .commit();
104 
105         if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
106             correctUserOrientation();
107         }
108         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
109 
110         initWindowInsets();
111     }
112 
113 
114     @Override
onConfigurationChanged(Configuration newConfig)115     public void onConfigurationChanged(Configuration newConfig) {
116         super.onConfigurationChanged(newConfig);
117 
118         // Ensure the prompt to rotate the screen is updated
119         if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
120             correctUserOrientation();
121         }
122     }
123 
initWindowInsets()124     private void initWindowInsets() {
125         View root = findViewById(android.R.id.content);
126         root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
127             @Override
128             public void onLayoutChange(View v, int left, int top, int right, int bottom,
129                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
130                 updateExclusionRects(root);
131             }
132         });
133 
134         // Return CONSUMED if you don't want want the window insets to keep being
135         // passed down to descendant views.
136         root.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
137             @Override
138             public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
139                 return WindowInsets.CONSUMED;
140             }
141         });
142     }
143 
updateExclusionRects(View rootView)144     private void updateExclusionRects(View rootView) {
145         Insets gestureInsets = rootView.getRootWindowInsets()
146                 .getInsets(WindowInsets.Type.systemGestures());
147         ArrayList<Rect> exclusionRects = new ArrayList<>();
148         // Add rect for left
149         exclusionRects.add(new Rect(0, 0, gestureInsets.left, rootView.getHeight()));
150         // Add rect for right
151         exclusionRects.add(new Rect(
152                 rootView.getWidth() - gestureInsets.right,
153                 0,
154                 rootView.getWidth(),
155                 rootView.getHeight()
156         ));
157         rootView.setSystemGestureExclusionRects(exclusionRects);
158     }
159 
160     /**
161      * Gesture animations are only in landscape for large screens and portrait for mobile. This
162      * method enforces the following flows:
163      *     1) phone / two-panel closed -> lock to portrait
164      *     2) Large screen + portrait -> prompt the user to rotate the screen
165      *     3) Large screen + landscape -> hide potential rotating prompt
166      *     4) Square aspect ratio -> no action taken as the animations will fit both orientations
167      */
correctUserOrientation()168     private void correctUserOrientation() {
169         DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.get(
170                 getApplicationContext()).getDeviceProfile(this);
171         if (deviceProfile.isTablet) {
172             // The tutorial will work in either orientation if the height and width are similar
173             boolean isAspectRatioSquare =
174                     deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND
175                             && deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND;
176             boolean showRotationPrompt = !isAspectRatioSquare
177                     && getResources().getConfiguration().orientation
178                     == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
179 
180             GestureSandboxFragment fragment = showRotationPrompt
181                     ? new RotationPromptFragment()
182                     : mCurrentFragment.canRecreateFragment() || mPendingFragment == null
183                             ? mCurrentFragment.recreateFragment()
184                             : mPendingFragment.recreateFragment();
185             showFragment(fragment == null ? mCurrentFragment : fragment);
186 
187         } else {
188             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
189         }
190     }
191 
192     private void showFragment(@NonNull GestureSandboxFragment fragment) {
193         // Store the current fragment in mPendingFragment so that it can be recreated after the
194         // new fragment is shown.
195         if (mCurrentFragment.canRecreateFragment()) {
196             mPendingFragment = mCurrentFragment;
197         } else {
198             mPendingFragment = null;
199         }
200         mCurrentFragment = fragment;
201         getSupportFragmentManager().beginTransaction()
202                 .replace(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
203                 .runOnCommit(() -> mCurrentFragment.onAttachedToWindow())
204                 .commit();
205     }
206 
207     @Override
onAttachedToWindow()208     public void onAttachedToWindow() {
209         super.onAttachedToWindow();
210         if (mCurrentFragment.shouldDisableSystemGestures()) {
211             disableSystemGestures();
212         }
213         mCurrentFragment.onAttachedToWindow();
214     }
215 
216     @Override
onDetachedFromWindow()217     public void onDetachedFromWindow() {
218         super.onDetachedFromWindow();
219         mCurrentFragment.onDetachedFromWindow();
220     }
221 
222     @Override
onWindowFocusChanged(boolean hasFocus)223     public void onWindowFocusChanged(boolean hasFocus) {
224         super.onWindowFocusChanged(hasFocus);
225         if (hasFocus) {
226             hideSystemUI();
227         }
228     }
229 
230     @Override
onSaveInstanceState(@onNull Bundle savedInstanceState)231     protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
232         savedInstanceState.putStringArray(KEY_TUTORIAL_STEPS, getTutorialStepNames());
233         savedInstanceState.putInt(KEY_CURRENT_STEP, mCurrentStep);
234         mCurrentFragment.onSaveInstanceState(savedInstanceState);
235         super.onSaveInstanceState(savedInstanceState);
236     }
237 
getSharedPrefs()238     protected SharedPreferences getSharedPrefs() {
239         return mSharedPrefs;
240     }
241 
getStatsLogManager()242     protected StatsLogManager getStatsLogManager() {
243         return mStatsLogManager;
244     }
245 
246     /** Returns true iff there aren't anymore tutorial types to display to the user. */
isTutorialComplete()247     public boolean isTutorialComplete() {
248         return mCurrentStep >= mNumSteps;
249     }
250 
getCurrentStep()251     public int getCurrentStep() {
252         return mCurrentStep;
253     }
254 
getNumSteps()255     public int getNumSteps() {
256         return mNumSteps;
257     }
258 
259     /**
260      * Replaces the current TutorialFragment, continuing to the next tutorial step if there is one.
261      *
262      * If there is no following step, the tutorial is closed.
263      */
continueTutorial()264     public void continueTutorial() {
265         if (isTutorialComplete() || mTutorialSteps == null) {
266             mCurrentFragment.close();
267             return;
268         }
269         launchTutorialStep(mTutorialSteps[mCurrentStep], false);
270         mCurrentStep++;
271     }
272 
makeTutorialFragment( @onNull TutorialType tutorialType, boolean gestureComplete, boolean fromMenu)273     private TutorialFragment makeTutorialFragment(
274             @NonNull TutorialType tutorialType, boolean gestureComplete, boolean fromMenu) {
275         return TutorialFragment.newInstance(tutorialType, gestureComplete, fromMenu);
276     }
277 
278     /**
279      * Launches the given gesture nav tutorial step.
280      *
281      * If the step is being launched from the gesture nav tutorial menu, then that step will launch
282      * the menu when complete.
283      */
launchTutorialStep(@onNull TutorialType tutorialType, boolean fromMenu)284     public void launchTutorialStep(@NonNull TutorialType tutorialType, boolean fromMenu) {
285         showFragment(makeTutorialFragment(tutorialType, false, fromMenu));
286     }
287 
288     /** Launches the gesture nav tutorial menu page */
launchTutorialMenu()289     public void launchTutorialMenu() {
290         showFragment(new MenuFragment());
291     }
292 
getTutorialStepNames()293     private String[] getTutorialStepNames() {
294         if (mTutorialSteps == null) {
295             return new String[0];
296         }
297         String[] tutorialStepNames = new String[mTutorialSteps.length];
298 
299         int i = 0;
300         for (TutorialType tutorialStep : mTutorialSteps) {
301             tutorialStepNames[i++] = tutorialStep.name();
302         }
303 
304         return tutorialStepNames;
305     }
306 
getTutorialSteps(Bundle extras)307     private TutorialType[] getTutorialSteps(Bundle extras) {
308         TutorialType[] defaultSteps = new TutorialType[] {
309                 TutorialType.HOME_NAVIGATION,
310                 TutorialType.BACK_NAVIGATION,
311                 TutorialType.OVERVIEW_NAVIGATION};
312         mCurrentStep = 1;
313         mNumSteps = defaultSteps.length;
314 
315         if (extras == null || !extras.containsKey(KEY_TUTORIAL_STEPS)) {
316             return defaultSteps;
317         }
318 
319         String[] savedStepsNames;
320         Object savedSteps = extras.get(KEY_TUTORIAL_STEPS);
321         if (savedSteps instanceof String) {
322             savedStepsNames = TextUtils.isEmpty((String) savedSteps)
323                     ? null : ((String) savedSteps).split(",");
324         } else if (savedSteps instanceof String[]) {
325             savedStepsNames = (String[]) savedSteps;
326         } else {
327             return defaultSteps;
328         }
329 
330         if (savedStepsNames == null || savedStepsNames.length == 0) {
331             return defaultSteps;
332         }
333 
334         TutorialType[] tutorialSteps = new TutorialType[savedStepsNames.length];
335         for (int i = 0; i < savedStepsNames.length; i++) {
336             tutorialSteps[i] = TutorialType.valueOf(savedStepsNames[i]);
337         }
338 
339         mCurrentStep = Math.max(extras.getInt(KEY_CURRENT_STEP, -1), 1);
340         mNumSteps = tutorialSteps.length;
341 
342         return tutorialSteps;
343     }
344 
hideSystemUI()345     private void hideSystemUI() {
346         getWindow().getDecorView().setSystemUiVisibility(
347                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
348                         | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
349                         | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
350                         | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
351                         | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
352                         | View.SYSTEM_UI_FLAG_FULLSCREEN);
353         getWindow().setNavigationBarColor(Color.TRANSPARENT);
354     }
355 
disableSystemGestures()356     private void disableSystemGestures() {
357         Display display = getDisplay();
358         if (display != null) {
359             DisplayMetrics metrics = new DisplayMetrics();
360             display.getMetrics(metrics);
361             getWindow().setSystemGestureExclusionRects(
362                     Arrays.asList(new Rect(0, 0, metrics.widthPixels, metrics.heightPixels)));
363         }
364     }
365 
366     @Override
onResume()367     protected void onResume() {
368         super.onResume();
369         updateServiceState(true);
370     }
371 
onTISConnected(TISBinder binder)372     private void onTISConnected(TISBinder binder) {
373         updateServiceState(isResumed());
374     }
375 
376     @Override
onPause()377     protected void onPause() {
378         super.onPause();
379         updateServiceState(false);
380     }
381 
updateServiceState(boolean isEnabled)382     private void updateServiceState(boolean isEnabled) {
383         TISBinder binder = mTISBindHelper.getBinder();
384         if (binder != null) {
385             binder.setGestureBlockedTaskId(isEnabled ? getTaskId() : -1);
386         }
387     }
388 
389     @Override
onDestroy()390     protected void onDestroy() {
391         super.onDestroy();
392         mTISBindHelper.onDestroy();
393         updateServiceState(false);
394     }
395 }
396