1 /* 2 * Copyright (C) 2021 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.launcher3.taskbar; 17 18 import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; 19 20 import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION; 21 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_EDU_TOOLTIP; 22 import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES; 23 import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_RESUMED; 24 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; 25 26 import android.animation.Animator; 27 import android.animation.AnimatorSet; 28 import android.os.RemoteException; 29 import android.util.Log; 30 import android.view.TaskTransitionSpec; 31 import android.view.View; 32 import android.view.WindowManagerGlobal; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.launcher3.DeviceProfile; 38 import com.android.launcher3.LauncherState; 39 import com.android.launcher3.QuickstepTransitionManager; 40 import com.android.launcher3.R; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.anim.AnimatedFloat; 43 import com.android.launcher3.logging.InstanceId; 44 import com.android.launcher3.logging.InstanceIdSequence; 45 import com.android.launcher3.model.data.ItemInfo; 46 import com.android.launcher3.uioverrides.QuickstepLauncher; 47 import com.android.launcher3.util.DisplayController; 48 import com.android.launcher3.util.MultiPropertyFactory; 49 import com.android.launcher3.util.OnboardingPrefs; 50 import com.android.quickstep.RecentsAnimationCallbacks; 51 import com.android.quickstep.util.GroupTask; 52 import com.android.quickstep.views.RecentsView; 53 54 import java.io.PrintWriter; 55 import java.util.Set; 56 57 /** 58 * A data source which integrates with a Launcher instance 59 */ 60 public class LauncherTaskbarUIController extends TaskbarUIController { 61 62 private static final String TAG = "TaskbarUIController"; 63 64 public static final int MINUS_ONE_PAGE_PROGRESS_INDEX = 0; 65 public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1; 66 public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2; 67 public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3; 68 69 public static final int DISPLAY_PROGRESS_COUNT = 4; 70 71 private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat( 72 this::onInAppDisplayProgressChanged); 73 private final MultiPropertyFactory<AnimatedFloat> mTaskbarInAppDisplayProgressMultiProp = 74 new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress, 75 AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max); 76 77 private final QuickstepLauncher mLauncher; 78 79 private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = 80 dp -> { 81 onStashedInAppChanged(dp); 82 if (mControllers != null && mControllers.taskbarViewController != null) { 83 mControllers.taskbarViewController.onRotationChanged(dp); 84 } 85 }; 86 87 // Initialized in init. 88 private final TaskbarLauncherStateController 89 mTaskbarLauncherStateController = new TaskbarLauncherStateController(); 90 LauncherTaskbarUIController(QuickstepLauncher launcher)91 public LauncherTaskbarUIController(QuickstepLauncher launcher) { 92 mLauncher = launcher; 93 } 94 95 @Override init(TaskbarControllers taskbarControllers)96 protected void init(TaskbarControllers taskbarControllers) { 97 super.init(taskbarControllers); 98 99 mTaskbarLauncherStateController.init(mControllers, mLauncher); 100 101 mLauncher.setTaskbarUIController(this); 102 103 onLauncherResumedOrPaused(mLauncher.hasBeenResumed(), true /* fromInit */); 104 105 onStashedInAppChanged(mLauncher.getDeviceProfile()); 106 mTaskbarLauncherStateController.updateStateForSysuiFlags( 107 mControllers.getSharedState().sysuiStateFlags, true /* fromInit */); 108 mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); 109 110 // Restore the in-app display progress from before Taskbar was recreated. 111 float[] prevProgresses = mControllers.getSharedState().inAppDisplayProgressMultiPropValues; 112 for (int i = 0; i < prevProgresses.length; i++) { 113 mTaskbarInAppDisplayProgressMultiProp.get(i).setValue(prevProgresses[i]); 114 } 115 } 116 117 @Override onDestroy()118 protected void onDestroy() { 119 super.onDestroy(); 120 onLauncherResumedOrPaused(false); 121 mTaskbarLauncherStateController.onDestroy(); 122 123 mLauncher.setTaskbarUIController(null); 124 mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); 125 updateTaskTransitionSpec(true); 126 } 127 onInAppDisplayProgressChanged()128 private void onInAppDisplayProgressChanged() { 129 if (mControllers != null) { 130 // Update our shared state so we can restore it if taskbar gets recreated. 131 for (int i = 0; i < DISPLAY_PROGRESS_COUNT; i++) { 132 mControllers.getSharedState().inAppDisplayProgressMultiPropValues[i] = 133 mTaskbarInAppDisplayProgressMultiProp.get(i).getValue(); 134 } 135 // Ensure nav buttons react to our latest state if necessary. 136 mControllers.navbarButtonsViewController.updateNavButtonTranslationY(); 137 } 138 } 139 140 @Override isTaskbarTouchable()141 protected boolean isTaskbarTouchable() { 142 return !(mTaskbarLauncherStateController.isAnimatingToLauncher() 143 && mTaskbarLauncherStateController.isTaskbarAlignedWithHotseat()); 144 } 145 setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim)146 public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) { 147 mTaskbarLauncherStateController.setShouldDelayLauncherStateAnim( 148 shouldDelayLauncherStateAnim); 149 } 150 151 /** 152 * Adds the Launcher resume animator to the given animator set. 153 * 154 * This should be used to run a Launcher resume animation whose progress matches a 155 * swipe progress. 156 * 157 * @param placeholderDuration a placeholder duration to be used to ensure all full-length 158 * sub-animations are properly coordinated. This duration should not 159 * actually be used since this animation tracks a swipe progress. 160 */ addLauncherResumeAnimation(AnimatorSet animation, int placeholderDuration)161 protected void addLauncherResumeAnimation(AnimatorSet animation, int placeholderDuration) { 162 animation.play(onLauncherResumedOrPaused( 163 /* isResumed= */ true, 164 /* fromInit= */ false, 165 /* startAnimation= */ false, 166 placeholderDuration)); 167 } 168 169 /** 170 * Should be called from onResume() and onPause(), and animates the Taskbar accordingly. 171 */ onLauncherResumedOrPaused(boolean isResumed)172 public void onLauncherResumedOrPaused(boolean isResumed) { 173 onLauncherResumedOrPaused(isResumed, false /* fromInit */); 174 } 175 onLauncherResumedOrPaused(boolean isResumed, boolean fromInit)176 private void onLauncherResumedOrPaused(boolean isResumed, boolean fromInit) { 177 onLauncherResumedOrPaused( 178 isResumed, 179 fromInit, 180 /* startAnimation= */ true, 181 DisplayController.isTransientTaskbar(mLauncher) 182 ? TRANSIENT_TASKBAR_TRANSITION_DURATION 183 : (!isResumed 184 ? QuickstepTransitionManager.TASKBAR_TO_APP_DURATION 185 : QuickstepTransitionManager.TASKBAR_TO_HOME_DURATION)); 186 } 187 188 @Nullable onLauncherResumedOrPaused( boolean isResumed, boolean fromInit, boolean startAnimation, int duration)189 private Animator onLauncherResumedOrPaused( 190 boolean isResumed, boolean fromInit, boolean startAnimation, int duration) { 191 if (ENABLE_SHELL_TRANSITIONS && isResumed 192 && !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) { 193 // Launcher is resumed, but in a state where taskbar is still independent, so 194 // ignore the state change. 195 return null; 196 } 197 198 mTaskbarLauncherStateController.updateStateForFlag(FLAG_RESUMED, isResumed); 199 return mTaskbarLauncherStateController.applyState(fromInit ? 0 : duration, startAnimation); 200 } 201 202 /** 203 * Create Taskbar animation when going from an app to Launcher as part of recents transition. 204 * @param toState If known, the state we will end up in when reaching Launcher. 205 * @param callbacks callbacks to track the recents animation lifecycle. The state change is 206 * automatically reset once the recents animation finishes 207 */ createAnimToLauncher(@onNull LauncherState toState, @NonNull RecentsAnimationCallbacks callbacks, long duration)208 public Animator createAnimToLauncher(@NonNull LauncherState toState, 209 @NonNull RecentsAnimationCallbacks callbacks, long duration) { 210 return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration); 211 } 212 isDraggingItem()213 public boolean isDraggingItem() { 214 return mControllers.taskbarDragController.isDragging(); 215 } 216 217 @Override onStashedInAppChanged()218 protected void onStashedInAppChanged() { 219 onStashedInAppChanged(mLauncher.getDeviceProfile()); 220 } 221 onStashedInAppChanged(DeviceProfile deviceProfile)222 private void onStashedInAppChanged(DeviceProfile deviceProfile) { 223 boolean taskbarStashedInApps = mControllers.taskbarStashController.isStashedInApp(); 224 deviceProfile.isTaskbarPresentInApps = !taskbarStashedInApps; 225 updateTaskTransitionSpec(taskbarStashedInApps); 226 } 227 updateTaskTransitionSpec(boolean taskbarIsHidden)228 private void updateTaskTransitionSpec(boolean taskbarIsHidden) { 229 try { 230 if (taskbarIsHidden) { 231 // Clear custom task transition settings when the taskbar is stashed 232 WindowManagerGlobal.getWindowManagerService().clearTaskTransitionSpec(); 233 } else { 234 // Adjust task transition spec to account for taskbar being visible 235 WindowManagerGlobal.getWindowManagerService().setTaskTransitionSpec( 236 new TaskTransitionSpec( 237 mLauncher.getColor(R.color.taskbar_background), 238 Set.of(ITYPE_EXTRA_NAVIGATION_BAR))); 239 } 240 } catch (RemoteException e) { 241 // This shouldn't happen but if it does task animations won't look good until the 242 // taskbar stashing state is changed. 243 Log.e(TAG, "Failed to update task transition spec to account for new taskbar state", 244 e); 245 } 246 } 247 248 /** 249 * Starts a Taskbar EDU flow, if the user should see one upon launching an application. 250 */ showEduOnAppLaunch()251 public void showEduOnAppLaunch() { 252 if (!shouldShowEduOnAppLaunch()) { 253 return; 254 } 255 256 // Transient and persistent bottom sheet. 257 if (!ENABLE_TASKBAR_EDU_TOOLTIP.get()) { 258 mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.TASKBAR_EDU_SEEN); 259 mControllers.taskbarEduController.showEdu(); 260 return; 261 } 262 263 // Persistent features EDU tooltip. 264 if (!DisplayController.isTransientTaskbar(mLauncher)) { 265 mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); 266 return; 267 } 268 269 // Transient swipe EDU tooltip. 270 mControllers.taskbarEduTooltipController.maybeShowSwipeEdu(); 271 } 272 273 /** 274 * Returns {@code true} if a Taskbar education should be shown on application launch. 275 */ shouldShowEduOnAppLaunch()276 public boolean shouldShowEduOnAppLaunch() { 277 if (Utilities.isRunningInTestHarness()) { 278 return false; 279 } 280 281 // Transient and persistent bottom sheet. 282 if (!ENABLE_TASKBAR_EDU_TOOLTIP.get()) { 283 return !mLauncher.getOnboardingPrefs().getBoolean(OnboardingPrefs.TASKBAR_EDU_SEEN); 284 } 285 286 // Persistent features EDU tooltip. 287 if (!DisplayController.isTransientTaskbar(mLauncher)) { 288 return !mLauncher.getOnboardingPrefs().hasReachedMaxCount( 289 OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP); 290 } 291 292 // Transient swipe EDU tooltip. 293 return mControllers.taskbarEduTooltipController.getTooltipStep() < TOOLTIP_STEP_FEATURES; 294 } 295 296 @Override onTaskbarIconLaunched(ItemInfo item)297 public void onTaskbarIconLaunched(ItemInfo item) { 298 super.onTaskbarIconLaunched(item); 299 InstanceId instanceId = new InstanceIdSequence().newInstanceId(); 300 mLauncher.logAppLaunch(mControllers.taskbarActivityContext.getStatsLogManager(), item, 301 instanceId); 302 } 303 304 /** 305 * Animates Taskbar elements during a transition to a Launcher state that should use in-app 306 * layouts. 307 * 308 * @param progress [0, 1] 309 * 0 => use home layout 310 * 1 => use in-app layout 311 */ onTaskbarInAppDisplayProgressUpdate(float progress, int progressIndex)312 public void onTaskbarInAppDisplayProgressUpdate(float progress, int progressIndex) { 313 mTaskbarInAppDisplayProgressMultiProp.get(progressIndex).setValue(progress); 314 if (mControllers == null) { 315 // This method can be called before init() is called. 316 return; 317 } 318 if (mControllers.uiController.isIconAlignedWithHotseat() 319 && !mTaskbarLauncherStateController.isAnimatingToLauncher()) { 320 // Only animate the nav buttons while home and not animating home, otherwise let 321 // the TaskbarViewController handle it. 322 mControllers.navbarButtonsViewController 323 .getTaskbarNavButtonTranslationYForInAppDisplay() 324 .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY() 325 * mTaskbarInAppDisplayProgress.value); 326 } 327 } 328 329 /** Returns true iff any in-app display progress > 0. */ shouldUseInAppLayout()330 public boolean shouldUseInAppLayout() { 331 return mTaskbarInAppDisplayProgress.value > 0; 332 } 333 334 @Override onExpandPip()335 public void onExpandPip() { 336 super.onExpandPip(); 337 mTaskbarLauncherStateController.updateStateForFlag(FLAG_RESUMED, false); 338 mTaskbarLauncherStateController.applyState(); 339 } 340 341 @Override updateStateForSysuiFlags(int sysuiFlags, boolean skipAnim)342 public void updateStateForSysuiFlags(int sysuiFlags, boolean skipAnim) { 343 mTaskbarLauncherStateController.updateStateForSysuiFlags(sysuiFlags, skipAnim); 344 } 345 346 @Override isIconAlignedWithHotseat()347 public boolean isIconAlignedWithHotseat() { 348 return mTaskbarLauncherStateController.isIconAlignedWithHotseat(); 349 } 350 351 @Override isHotseatIconOnTopWhenAligned()352 public boolean isHotseatIconOnTopWhenAligned() { 353 return mTaskbarLauncherStateController.isInHotseatOnTopStates() 354 && mTaskbarInAppDisplayProgressMultiProp.get(MINUS_ONE_PAGE_PROGRESS_INDEX) 355 .getValue() == 0; 356 } 357 358 @Override isInOverview()359 protected boolean isInOverview() { 360 return mTaskbarLauncherStateController.isInOverview(); 361 } 362 363 @Override getRecentsView()364 public RecentsView getRecentsView() { 365 return mLauncher.getOverviewPanel(); 366 } 367 368 @Override launchSplitTasks(@onNull View taskView, @NonNull GroupTask groupTask)369 public void launchSplitTasks(@NonNull View taskView, @NonNull GroupTask groupTask) { 370 mLauncher.launchSplitTasks(taskView, groupTask); 371 } 372 373 @Override onIconLayoutBoundsChanged()374 protected void onIconLayoutBoundsChanged() { 375 mTaskbarLauncherStateController.resetIconAlignment(); 376 } 377 378 @Override dumpLogs(String prefix, PrintWriter pw)379 public void dumpLogs(String prefix, PrintWriter pw) { 380 super.dumpLogs(prefix, pw); 381 382 pw.println(String.format("%s\tTaskbar in-app display progress: %.2f", prefix, 383 mTaskbarInAppDisplayProgress.value)); 384 mTaskbarInAppDisplayProgressMultiProp.dump( 385 prefix + "\t\t", 386 pw, 387 "mTaskbarInAppDisplayProgressMultiProp", 388 "MINUS_ONE_PAGE_PROGRESS_INDEX", 389 "ALL_APPS_PAGE_PROGRESS_INDEX", 390 "WIDGETS_PAGE_PROGRESS_INDEX", 391 "SYSUI_SURFACE_PROGRESS_INDEX"); 392 393 mTaskbarLauncherStateController.dumpLogs(prefix + "\t", pw); 394 } 395 } 396