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