| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wm; |
| |
| import static android.app.ActivityTaskManager.INVALID_TASK_ID; |
| import static android.view.RemoteAnimationTarget.MODE_CLOSING; |
| import static android.view.RemoteAnimationTarget.MODE_OPENING; |
| import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; |
| import static android.view.WindowManager.TRANSIT_CLOSE; |
| import static android.view.WindowManager.TRANSIT_OLD_NONE; |
| import static android.view.WindowManager.TRANSIT_TO_BACK; |
| |
| import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW; |
| import static com.android.server.wm.BackNavigationProto.ANIMATION_IN_PROGRESS; |
| import static com.android.server.wm.BackNavigationProto.ANIMATION_RUNNING; |
| import static com.android.server.wm.BackNavigationProto.LAST_BACK_TYPE; |
| import static com.android.server.wm.BackNavigationProto.MAIN_OPEN_ACTIVITY; |
| import static com.android.server.wm.BackNavigationProto.SHOW_WALLPAPER; |
| import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.content.res.ResourceId; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.RemoteCallback; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| import android.util.proto.ProtoOutputStream; |
| import android.view.RemoteAnimationTarget; |
| import android.view.SurfaceControl; |
| import android.view.WindowInsets; |
| import android.window.BackAnimationAdapter; |
| import android.window.BackNavigationInfo; |
| import android.window.IBackAnimationFinishedCallback; |
| import android.window.IWindowlessStartingSurfaceCallback; |
| import android.window.OnBackInvokedCallbackInfo; |
| import android.window.TaskSnapshot; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.policy.TransitionAnimation; |
| import com.android.internal.protolog.common.ProtoLog; |
| import com.android.server.wm.utils.InsetUtils; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Objects; |
| |
| /** |
| * Controller to handle actions related to the back gesture on the server side. |
| */ |
| class BackNavigationController { |
| private static final String TAG = "CoreBackPreview"; |
| private WindowManagerService mWindowManagerService; |
| private boolean mBackAnimationInProgress; |
| private @BackNavigationInfo.BackTargetType int mLastBackType; |
| private boolean mShowWallpaper; |
| private Runnable mPendingAnimation; |
| private final NavigationMonitor mNavigationMonitor = new NavigationMonitor(); |
| |
| private AnimationHandler mAnimationHandler; |
| |
| /** |
| * The transition who match the back navigation targets, |
| * release animation after this transition finish. |
| */ |
| private Transition mWaitTransitionFinish; |
| private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>(); |
| private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>(); |
| |
| // This will be set if the back navigation is in progress and the current transition is still |
| // running. The pending animation builder will do the animation stuff includes creating leashes, |
| // re-parenting leashes and set launch behind, etc. Will be handled when transition finished. |
| private AnimationHandler.ScheduleAnimationBuilder mPendingAnimationBuilder; |
| |
| private static int sDefaultAnimationResId; |
| |
| /** |
| * true if the back predictability feature is enabled |
| */ |
| static final boolean sPredictBackEnable = |
| SystemProperties.getBoolean("persist.wm.debug.predictive_back", true); |
| |
| static boolean isScreenshotEnabled() { |
| return SystemProperties.getInt("persist.wm.debug.predictive_back_screenshot", 0) != 0; |
| } |
| |
| // Notify focus window changed |
| void onFocusChanged(WindowState newFocus) { |
| mNavigationMonitor.onFocusWindowChanged(newFocus); |
| } |
| |
| /** |
| * Set up the necessary leashes and build a {@link BackNavigationInfo} instance for an upcoming |
| * back gesture animation. |
| * |
| * @return a {@link BackNavigationInfo} instance containing the required leashes and metadata |
| * for the animation, or null if we don't know how to animate the current window and need to |
| * fallback on dispatching the key event. |
| */ |
| @VisibleForTesting |
| @Nullable |
| BackNavigationInfo startBackNavigation(@NonNull RemoteCallback navigationObserver, |
| BackAnimationAdapter adapter) { |
| if (!sPredictBackEnable) { |
| return null; |
| } |
| final WindowManagerService wmService = mWindowManagerService; |
| |
| int backType = BackNavigationInfo.TYPE_UNDEFINED; |
| |
| // The currently visible activity (if any). |
| ActivityRecord currentActivity = null; |
| |
| // The currently visible task (if any). |
| Task currentTask = null; |
| |
| // The previous task we're going back to. Can be the same as currentTask, if there are |
| // multiple Activities in the Stack. |
| Task prevTask = null; |
| |
| WindowContainer<?> removedWindowContainer = null; |
| WindowState window; |
| |
| BackNavigationInfo.Builder infoBuilder = new BackNavigationInfo.Builder(); |
| synchronized (wmService.mGlobalLock) { |
| if (isMonitoringTransition()) { |
| Slog.w(TAG, "Previous animation hasn't finish, status: " + mAnimationHandler); |
| // Don't start any animation for it. |
| return null; |
| } |
| |
| window = wmService.getFocusedWindowLocked(); |
| |
| if (window == null) { |
| // We don't have any focused window, fallback ont the top currentTask of the focused |
| // display. |
| ProtoLog.w(WM_DEBUG_BACK_PREVIEW, |
| "No focused window, defaulting to top current task's window"); |
| currentTask = wmService.mAtmService.getTopDisplayFocusedRootTask(); |
| window = currentTask != null |
| ? currentTask.getWindow(WindowState::isFocused) : null; |
| } |
| |
| if (window == null) { |
| Slog.e(TAG, "Window is null, returning null."); |
| return null; |
| } |
| |
| // This is needed to bridge the old and new back behavior with recents. While in |
| // Overview with live tile enabled, the previous app is technically focused but we |
| // add an input consumer to capture all input that would otherwise go to the apps |
| // being controlled by the animation. This means that the window resolved is not |
| // the right window to consume back while in overview, so we need to route it to |
| // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing |
| // compat callback in VRI only works when the window is focused. |
| // This symptom also happen while shell transition enabled, we can check that by |
| // isTransientLaunch to know whether the focus window is point to live tile. |
| final RecentsAnimationController recentsAnimationController = |
| wmService.getRecentsAnimationController(); |
| final ActivityRecord tmpAR = window.mActivityRecord; |
| if ((tmpAR != null && tmpAR.isActivityTypeHomeOrRecents() |
| && tmpAR.mTransitionController.isTransientLaunch(tmpAR)) |
| || (recentsAnimationController != null |
| && recentsAnimationController.shouldApplyInputConsumer(tmpAR))) { |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by " |
| + "recents. Overriding back callback to recents controller callback."); |
| return null; |
| } |
| |
| if (!window.isDrawn()) { |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, |
| "Focused window didn't have a valid surface drawn."); |
| return null; |
| } |
| |
| currentActivity = window.mActivityRecord; |
| currentTask = window.getTask(); |
| if ((currentTask != null && !currentTask.isVisibleRequested()) |
| || (currentActivity != null && !currentActivity.isVisibleRequested())) { |
| // Closing transition is happening on focus window and should be update soon, |
| // don't drive back navigation with it. |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focus window is closing."); |
| return null; |
| } |
| // Now let's find if this window has a callback from the client side. |
| final OnBackInvokedCallbackInfo callbackInfo = window.getOnBackInvokedCallbackInfo(); |
| if (callbackInfo == null) { |
| Slog.e(TAG, "No callback registered, returning null."); |
| return null; |
| } |
| if (!callbackInfo.isSystemCallback()) { |
| backType = BackNavigationInfo.TYPE_CALLBACK; |
| } |
| infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback()); |
| infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback()); |
| mNavigationMonitor.startMonitor(window, navigationObserver); |
| |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, " |
| + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s", |
| currentTask, currentActivity, callbackInfo, window); |
| |
| // If we don't need to set up the animation, we return early. This is the case when |
| // - We have an application callback. |
| // - We don't have any ActivityRecord or Task to animate. |
| // - The IME is opened, and we just need to close it. |
| // - The home activity is the focused activity & it's not TYPE_BASE_APPLICATION |
| // - The current activity will do shared element transition when exiting. |
| if (backType == BackNavigationInfo.TYPE_CALLBACK |
| || currentActivity == null |
| || currentTask == null |
| || (currentActivity.isActivityTypeHome() |
| && window.mAttrs.type == TYPE_BASE_APPLICATION) |
| || currentActivity.mHasSceneTransition) { |
| infoBuilder.setType(BackNavigationInfo.TYPE_CALLBACK); |
| infoBuilder.setOnBackNavigationDone(new RemoteCallback(result -> |
| onBackNavigationDone(result, BackNavigationInfo.TYPE_CALLBACK))); |
| mLastBackType = BackNavigationInfo.TYPE_CALLBACK; |
| return infoBuilder.build(); |
| } |
| |
| // The previous activity we're going back to. This can be either a child of currentTask |
| // if there are more than one Activity in currentTask, or a child of prevTask, if |
| // currentActivity is the last child of currentTask. |
| // We don't have an application callback, let's find the destination of the back gesture |
| // The search logic should align with ActivityClientController#finishActivity |
| final ArrayList<ActivityRecord> prevActivities = new ArrayList<>(); |
| final boolean canAnimate = getAnimatablePrevActivities(currentTask, currentActivity, |
| prevActivities); |
| final boolean isOccluded = isKeyguardOccluded(window); |
| if (!canAnimate) { |
| backType = BackNavigationInfo.TYPE_CALLBACK; |
| } else if ((window.getParent().getChildCount() > 1 |
| && window.getParent().getChildAt(0) != window)) { |
| // TODO Dialog window does not need to attach on activity, check |
| // window.mAttrs.type != TYPE_BASE_APPLICATION |
| // Are we the top window of our parent? If not, we are a window on top of the |
| // activity, we won't close the activity. |
| backType = BackNavigationInfo.TYPE_DIALOG_CLOSE; |
| removedWindowContainer = window; |
| } else if (hasTranslucentActivity(currentActivity, prevActivities)) { |
| // skip if one of participant activity is translucent |
| backType = BackNavigationInfo.TYPE_CALLBACK; |
| } else if (prevActivities.size() > 0) { |
| if (!isOccluded || isAllActivitiesCanShowWhenLocked(prevActivities)) { |
| // We have another Activity in the same currentTask to go to |
| final WindowContainer parent = currentActivity.getParent(); |
| final boolean canCustomize = parent != null |
| && (parent.asTask() != null |
| || (parent.asTaskFragment() != null |
| && parent.canCustomizeAppTransition())); |
| if (canCustomize) { |
| if (isCustomizeExitAnimation(window)) { |
| infoBuilder.setWindowAnimations( |
| window.mAttrs.packageName, window.mAttrs.windowAnimations); |
| } |
| final ActivityRecord.CustomAppTransition customAppTransition = |
| currentActivity.getCustomAnimation(false/* open */); |
| if (customAppTransition != null) { |
| infoBuilder.setCustomAnimation(currentActivity.packageName, |
| customAppTransition.mEnterAnim, |
| customAppTransition.mExitAnim, |
| customAppTransition.mBackgroundColor); |
| } |
| } |
| removedWindowContainer = currentActivity; |
| prevTask = prevActivities.get(0).getTask(); |
| backType = BackNavigationInfo.TYPE_CROSS_ACTIVITY; |
| } else { |
| // keyguard locked and activities are unable to show when locked. |
| backType = BackNavigationInfo.TYPE_CALLBACK; |
| } |
| } else { |
| // TODO(208789724): Create single source of truth for this, maybe in |
| // RootWindowContainer |
| prevTask = currentTask.mRootWindowContainer.getTask(t -> { |
| if (t.showToCurrentUser() && !t.mChildren.isEmpty()) { |
| final ActivityRecord ar = t.getTopNonFinishingActivity(); |
| return ar != null && ar.showToCurrentUser(); |
| } |
| return false; |
| }, currentTask, false /*includeBoundary*/, true /*traverseTopToBottom*/); |
| final ActivityRecord tmpPre = prevTask != null |
| ? prevTask.getTopNonFinishingActivity() : null; |
| if (tmpPre != null) { |
| prevActivities.add(tmpPre); |
| findAdjacentActivityIfExist(tmpPre, prevActivities); |
| } |
| if (prevTask == null || prevActivities.isEmpty() |
| || (isOccluded && !isAllActivitiesCanShowWhenLocked(prevActivities))) { |
| backType = BackNavigationInfo.TYPE_CALLBACK; |
| } else if (prevTask.isActivityTypeHome()) { |
| removedWindowContainer = currentTask; |
| backType = BackNavigationInfo.TYPE_RETURN_TO_HOME; |
| mShowWallpaper = true; |
| } else { |
| // If it reaches the top activity, we will check the below task from parent. |
| // If it's null or multi-window and has different parent task, fallback the type |
| // to TYPE_CALLBACK. Or set the type to proper value when it's return to home or |
| // another task. |
| final Task prevParent = prevTask.getParent().asTask(); |
| final Task currParent = currentTask.getParent().asTask(); |
| if (prevTask.inMultiWindowMode() && prevParent != currParent) { |
| backType = BackNavigationInfo.TYPE_CALLBACK; |
| } else { |
| removedWindowContainer = prevTask; |
| backType = BackNavigationInfo.TYPE_CROSS_TASK; |
| } |
| } |
| } |
| infoBuilder.setType(backType); |
| |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Previous Destination is Activity:%s Task:%s " |
| + "removedContainer:%s, backType=%s", |
| prevActivities.size() > 0 ? TextUtils.join(";", prevActivities.stream() |
| .map(r -> r.mActivityComponent).toArray()) : null, |
| prevTask != null ? prevTask.getName() : null, |
| removedWindowContainer, |
| BackNavigationInfo.typeToString(backType)); |
| |
| // For now, we only animate when going home, cross task or cross-activity. |
| boolean prepareAnimation = |
| (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME |
| || backType == BackNavigationInfo.TYPE_CROSS_TASK |
| || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY |
| || backType == BackNavigationInfo.TYPE_DIALOG_CLOSE) |
| && adapter != null; |
| |
| if (prepareAnimation) { |
| final AnimationHandler.ScheduleAnimationBuilder builder = |
| mAnimationHandler.prepareAnimation( |
| backType, |
| adapter, |
| currentTask, |
| prevTask, |
| currentActivity, |
| prevActivities, |
| removedWindowContainer); |
| mBackAnimationInProgress = builder != null; |
| if (mBackAnimationInProgress) { |
| if (removedWindowContainer.hasCommittedReparentToAnimationLeash() |
| || removedWindowContainer.mTransitionController.inTransition() |
| || mWindowManagerService.mSyncEngine.hasPendingSyncSets()) { |
| ProtoLog.w(WM_DEBUG_BACK_PREVIEW, |
| "Pending back animation due to another animation is running"); |
| mPendingAnimationBuilder = builder; |
| // Current transition is still running, we have to defer the hiding to the |
| // client process to prevent the unexpected relayout when handling the back |
| // animation. |
| for (int i = prevActivities.size() - 1; i >= 0; --i) { |
| prevActivities.get(i).setDeferHidingClient(true); |
| } |
| } else { |
| scheduleAnimation(builder); |
| } |
| } |
| } |
| infoBuilder.setPrepareRemoteAnimation(prepareAnimation); |
| |
| if (removedWindowContainer != null) { |
| final int finalBackType = backType; |
| final RemoteCallback onBackNavigationDone = new RemoteCallback(result -> |
| onBackNavigationDone(result, finalBackType)); |
| infoBuilder.setOnBackNavigationDone(onBackNavigationDone); |
| } else { |
| mNavigationMonitor.stopMonitorForRemote(); |
| } |
| mLastBackType = backType; |
| return infoBuilder.build(); |
| } |
| } |
| |
| /** |
| * Gets previous activities from currentActivity. |
| * |
| * @return false if unable to predict what will happen |
| */ |
| @VisibleForTesting |
| static boolean getAnimatablePrevActivities(@NonNull Task currentTask, |
| @NonNull ActivityRecord currentActivity, |
| @NonNull ArrayList<ActivityRecord> outPrevActivities) { |
| if (currentActivity.mAtmService |
| .mTaskOrganizerController.shouldInterceptBackPressedOnRootTask( |
| currentTask.getRootTask())) { |
| // The task organizer will handle back pressed, don't play animation. |
| return false; |
| } |
| final ActivityRecord root = currentTask.getRootActivity(false /*ignoreRelinquishIdentity*/, |
| true /*setToBottomIfNone*/); |
| if (root != null && ActivityClientController.shouldMoveTaskToBack(currentActivity, root)) { |
| return true; |
| } |
| |
| // Searching previous |
| final ActivityRecord prevActivity = currentTask.getActivity((below) -> !below.finishing, |
| currentActivity, false /*includeBoundary*/, true /*traverseTopToBottom*/); |
| |
| final TaskFragment currTF = currentActivity.getTaskFragment(); |
| if (currTF != null && currTF.asTask() == null) { |
| // The currentActivity is embedded, search for the candidate previous activities. |
| if (prevActivity != null && currTF.hasChild(prevActivity)) { |
| // PrevActivity is under the same task fragment, that's it. |
| outPrevActivities.add(prevActivity); |
| return true; |
| } |
| if (currTF.getAdjacentTaskFragment() != null) { |
| // The two TFs are adjacent (visually displayed side-by-side), search if any |
| // activity below the lowest one |
| // If companion, those two TF will be closed together. |
| if (currTF.getCompanionTaskFragment() != null) { |
| final WindowContainer commonParent = currTF.getParent(); |
| final TaskFragment adjacentTF = currTF.getAdjacentTaskFragment(); |
| final TaskFragment lowerTF = commonParent.mChildren.indexOf(currTF) |
| < commonParent.mChildren.indexOf(adjacentTF) |
| ? currTF : adjacentTF; |
| final ActivityRecord lowerActivity = lowerTF.getTopNonFinishingActivity(); |
| // TODO (b/274997067) close currTF + companionTF, open next activities if any. |
| // Allow to predict next task if no more activity in task. Or return previous |
| // activities for cross-activity animation. |
| return currentTask.getActivity((below) -> !below.finishing, lowerActivity, |
| false /*includeBoundary*/, true /*traverseTopToBottom*/) == null; |
| } |
| // Unable to predict if no companion, it can only close current activity and make |
| // prev Activity full screened. |
| return false; |
| } else if (currTF.getCompanionTaskFragment() != null) { |
| // TF is isStacked, search bottom activity from companion TF. |
| // |
| // Sample hierarchy: search for underPrevious if any. |
| // Current TF |
| // Companion TF (bottomActivityInCompanion) |
| // Bottom Activity not inside companion TF (underPrevious) |
| final TaskFragment companionTF = currTF.getCompanionTaskFragment(); |
| // find bottom activity in Companion TF. |
| final ActivityRecord bottomActivityInCompanion = companionTF.getActivity( |
| (below) -> !below.finishing, false /* traverseTopToBottom */); |
| final ActivityRecord underPrevious = currentTask.getActivity( |
| (below) -> !below.finishing, bottomActivityInCompanion, |
| false /*includeBoundary*/, true /*traverseTopToBottom*/); |
| if (underPrevious != null) { |
| outPrevActivities.add(underPrevious); |
| addPreviousAdjacentActivityIfExist(underPrevious, outPrevActivities); |
| } |
| return true; |
| } |
| } |
| |
| if (prevActivity == null) { |
| // No previous activity in this Task nor TaskFragment, it can still predict if previous |
| // task exists. |
| return true; |
| } |
| // Add possible adjacent activity if prevActivity is embedded |
| addPreviousAdjacentActivityIfExist(prevActivity, outPrevActivities); |
| outPrevActivities.add(prevActivity); |
| return true; |
| } |
| |
| private static void addPreviousAdjacentActivityIfExist(@NonNull ActivityRecord prevActivity, |
| @NonNull ArrayList<ActivityRecord> outPrevActivities) { |
| final TaskFragment prevTF = prevActivity.getTaskFragment(); |
| if (prevTF == null || prevTF.asTask() != null) { |
| return; |
| } |
| |
| final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment(); |
| if (prevTFAdjacent == null || prevTFAdjacent.asTask() != null) { |
| return; |
| } |
| final ActivityRecord prevActivityAdjacent = |
| prevTFAdjacent.getTopNonFinishingActivity(); |
| if (prevActivityAdjacent != null) { |
| outPrevActivities.add(prevActivityAdjacent); |
| } |
| } |
| |
| private static void findAdjacentActivityIfExist(@NonNull ActivityRecord mainActivity, |
| @NonNull ArrayList<ActivityRecord> outList) { |
| final TaskFragment mainTF = mainActivity.getTaskFragment(); |
| if (mainTF == null || mainTF.getAdjacentTaskFragment() == null) { |
| return; |
| } |
| final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment(); |
| final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); |
| if (topActivity == null) { |
| return; |
| } |
| outList.add(topActivity); |
| } |
| |
| private static boolean hasTranslucentActivity(@NonNull ActivityRecord currentActivity, |
| @NonNull ArrayList<ActivityRecord> prevActivities) { |
| if (!currentActivity.occludesParent() || currentActivity.showWallpaper()) { |
| return true; |
| } |
| for (int i = prevActivities.size() - 1; i >= 0; --i) { |
| final ActivityRecord test = prevActivities.get(i); |
| if (!test.occludesParent() || test.showWallpaper()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isAllActivitiesCanShowWhenLocked( |
| @NonNull ArrayList<ActivityRecord> prevActivities) { |
| for (int i = prevActivities.size() - 1; i >= 0; --i) { |
| if (!prevActivities.get(i).canShowWhenLocked()) { |
| return false; |
| } |
| } |
| return !prevActivities.isEmpty(); |
| } |
| |
| boolean isMonitoringTransition() { |
| return mAnimationHandler.mComposed || mNavigationMonitor.isMonitorForRemote(); |
| } |
| |
| private void scheduleAnimation(@NonNull AnimationHandler.ScheduleAnimationBuilder builder) { |
| mPendingAnimation = builder.build(); |
| mWindowManagerService.mWindowPlacerLocked.requestTraversal(); |
| if (mShowWallpaper) { |
| mWindowManagerService.getDefaultDisplayContentLocked().mWallpaperController |
| .adjustWallpaperWindows(); |
| } |
| } |
| |
| private boolean isWaitBackTransition() { |
| return mAnimationHandler.mComposed && mAnimationHandler.mWaitTransition; |
| } |
| |
| boolean isKeyguardOccluded(WindowState focusWindow) { |
| final KeyguardController kc = mWindowManagerService.mAtmService.mKeyguardController; |
| final int displayId = focusWindow.getDisplayId(); |
| return kc.isKeyguardOccluded(displayId); |
| } |
| |
| /** |
| * There are two ways to customize activity exit animation, one is to provide the |
| * windowAnimationStyle by Activity#setTheme, another one is to set resId by |
| * Window#setWindowAnimations. |
| * Not all run-time customization methods can be checked from here, such as |
| * overridePendingTransition, which the animation resource will be set just before the |
| * transition is about to happen. |
| */ |
| private static boolean isCustomizeExitAnimation(WindowState window) { |
| // The default animation ResId is loaded from system package, so the result must match. |
| if (Objects.equals(window.mAttrs.packageName, "android")) { |
| return false; |
| } |
| if (window.mAttrs.windowAnimations != 0) { |
| final TransitionAnimation transitionAnimation = window.getDisplayContent() |
| .mAppTransition.mTransitionAnimation; |
| final int attr = com.android.internal.R.styleable |
| .WindowAnimation_activityCloseExitAnimation; |
| final int appResId = transitionAnimation.getAnimationResId( |
| window.mAttrs, attr, TRANSIT_OLD_NONE); |
| if (ResourceId.isValid(appResId)) { |
| if (sDefaultAnimationResId == 0) { |
| sDefaultAnimationResId = transitionAnimation.getDefaultAnimationResId(attr, |
| TRANSIT_OLD_NONE); |
| } |
| return sDefaultAnimationResId != appResId; |
| } |
| } |
| return false; |
| } |
| |
| // For legacy transition. |
| /** |
| * Once we find the transition targets match back animation targets, remove the target from |
| * list, so that transition won't count them in since the close animation was finished. |
| * |
| * @return {@code true} if the participants of this transition was animated by back gesture |
| * animations, and shouldn't join next transition. |
| */ |
| boolean removeIfContainsBackAnimationTargets(ArraySet<ActivityRecord> openApps, |
| ArraySet<ActivityRecord> closeApps) { |
| if (!isMonitoringTransition()) { |
| return false; |
| } |
| mTmpCloseApps.addAll(closeApps); |
| final boolean matchAnimationTargets = removeIfWaitForBackTransition(openApps, closeApps); |
| if (!matchAnimationTargets) { |
| mNavigationMonitor.onTransitionReadyWhileNavigate(mTmpOpenApps, mTmpCloseApps); |
| } |
| mTmpCloseApps.clear(); |
| return matchAnimationTargets; |
| } |
| |
| boolean removeIfWaitForBackTransition(ArraySet<ActivityRecord> openApps, |
| ArraySet<ActivityRecord> closeApps) { |
| if (!isWaitBackTransition()) { |
| return false; |
| } |
| // Note: TmpOpenApps is empty. Unlike shell transition, the open apps will be removed from |
| // mOpeningApps if there is no visibility change. |
| if (mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps)) { |
| // remove close target from close list, open target from open list; |
| // but the open target can be in close list. |
| for (int i = openApps.size() - 1; i >= 0; --i) { |
| final ActivityRecord ar = openApps.valueAt(i); |
| if (mAnimationHandler.isTarget(ar, true /* open */)) { |
| openApps.removeAt(i); |
| mAnimationHandler.markStartingSurfaceMatch(); |
| } |
| } |
| for (int i = closeApps.size() - 1; i >= 0; --i) { |
| final ActivityRecord ar = closeApps.valueAt(i); |
| if (mAnimationHandler.isTarget(ar, false /* open */)) { |
| closeApps.removeAt(i); |
| } |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private class NavigationMonitor { |
| // The window which triggering the back navigation. |
| private WindowState mNavigatingWindow; |
| private RemoteCallback mObserver; |
| |
| void startMonitor(@NonNull WindowState window, @NonNull RemoteCallback observer) { |
| mNavigatingWindow = window; |
| mObserver = observer; |
| } |
| |
| void stopMonitorForRemote() { |
| mObserver = null; |
| } |
| |
| void stopMonitorTransition() { |
| mNavigatingWindow = null; |
| } |
| |
| boolean isMonitorForRemote() { |
| return mNavigatingWindow != null && mObserver != null; |
| } |
| |
| boolean isMonitorAnimationOrTransition() { |
| return mNavigatingWindow != null |
| && (mAnimationHandler.mComposed || mAnimationHandler.mWaitTransition); |
| } |
| |
| /** |
| * Notify focus window changed during back navigation. This will cancel the gesture for |
| * scenarios like: a system window popup, or when an activity add a new window. |
| * |
| * This method should only be used to check window-level change, otherwise it may cause |
| * misjudgment in multi-window mode. For example: in split-screen, when user is |
| * navigating on the top task, bottom task can start a new task, which will gain focus for |
| * a short time, but we should not cancel the navigation. |
| */ |
| private void onFocusWindowChanged(WindowState newFocus) { |
| if (!atSameDisplay(newFocus) |
| || !(isMonitorForRemote() || isMonitorAnimationOrTransition())) { |
| return; |
| } |
| // Keep navigating if either new focus == navigating window or null. |
| if (newFocus != null && newFocus != mNavigatingWindow |
| && (newFocus.mActivityRecord == null |
| || (newFocus.mActivityRecord == mNavigatingWindow.mActivityRecord))) { |
| cancelBackNavigating("focusWindowChanged"); |
| } |
| } |
| |
| /** |
| * Notify an unexpected transition has happened during back navigation. |
| */ |
| private void onTransitionReadyWhileNavigate(ArrayList<WindowContainer> opening, |
| ArrayList<WindowContainer> closing) { |
| if (!isMonitorForRemote() && !isMonitorAnimationOrTransition()) { |
| return; |
| } |
| final ArrayList<WindowContainer> all = new ArrayList<>(opening); |
| all.addAll(closing); |
| for (int i = all.size() - 1; i >= 0; --i) { |
| if (all.get(i).hasChild(mNavigatingWindow)) { |
| cancelBackNavigating("transitionHappens"); |
| break; |
| } |
| } |
| } |
| |
| private boolean atSameDisplay(WindowState newFocus) { |
| if (mNavigatingWindow == null) { |
| return false; |
| } |
| final int navigatingDisplayId = mNavigatingWindow.getDisplayId(); |
| return newFocus == null || newFocus.getDisplayId() == navigatingDisplayId; |
| } |
| |
| private void cancelBackNavigating(String reason) { |
| EventLogTags.writeWmBackNaviCanceled(reason); |
| if (isMonitorForRemote()) { |
| mObserver.sendResult(null /* result */); |
| } |
| if (isMonitorAnimationOrTransition()) { |
| clearBackAnimations(); |
| } |
| cancelPendingAnimation(); |
| } |
| } |
| |
| // For shell transition |
| /** |
| * Check whether the transition targets was animated by back gesture animation. |
| * Because the opening target could request to do other stuff at onResume, so it could become |
| * close target for a transition. So the condition here is |
| * The closing target should only exist in close list, but the opening target can be either in |
| * open or close list. |
| */ |
| void onTransactionReady(Transition transition, ArrayList<Transition.ChangeInfo> targets, |
| SurfaceControl.Transaction startTransaction) { |
| if (!isMonitoringTransition() || targets.isEmpty()) { |
| return; |
| } |
| for (int i = targets.size() - 1; i >= 0; --i) { |
| final WindowContainer wc = targets.get(i).mContainer; |
| if (wc.asActivityRecord() == null && wc.asTask() == null |
| && wc.asTaskFragment() == null) { |
| continue; |
| } |
| // WC can be visible due to setLaunchBehind |
| if (wc.isVisibleRequested()) { |
| mTmpOpenApps.add(wc); |
| } else { |
| mTmpCloseApps.add(wc); |
| } |
| } |
| final boolean matchAnimationTargets = isWaitBackTransition() |
| && (transition.mType == TRANSIT_CLOSE || transition.mType == TRANSIT_TO_BACK) |
| && mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, |
| "onTransactionReady, opening: %s, closing: %s, animating: %s, match: %b", |
| mTmpOpenApps, mTmpCloseApps, mAnimationHandler, matchAnimationTargets); |
| if (!matchAnimationTargets) { |
| mNavigationMonitor.onTransitionReadyWhileNavigate(mTmpOpenApps, mTmpCloseApps); |
| } else { |
| if (mWaitTransitionFinish != null) { |
| Slog.e(TAG, "Gesture animation is applied on another transition?"); |
| } |
| mWaitTransitionFinish = transition; |
| // Flag target matches to defer remove the splash screen. |
| for (int i = mTmpOpenApps.size() - 1; i >= 0; --i) { |
| final WindowContainer wc = mTmpOpenApps.get(i); |
| if (mAnimationHandler.isTarget(wc, true /* open */)) { |
| mAnimationHandler.markStartingSurfaceMatch(); |
| break; |
| } |
| } |
| // Because the target will reparent to transition root, so it cannot be controlled by |
| // animation leash. Hide the close target when transition starts. |
| startTransaction.hide(mAnimationHandler.mCloseAdaptor.mTarget.getSurfaceControl()); |
| } |
| mTmpOpenApps.clear(); |
| mTmpCloseApps.clear(); |
| } |
| |
| boolean isMonitorTransitionTarget(WindowContainer wc) { |
| if (!isWaitBackTransition() || mWaitTransitionFinish == null) { |
| return false; |
| } |
| return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */); |
| } |
| |
| /** |
| * Cleanup animation, this can either happen when legacy transition ready, or when the Shell |
| * transition finish. |
| */ |
| void clearBackAnimations() { |
| mAnimationHandler.clearBackAnimateTarget(); |
| mNavigationMonitor.stopMonitorTransition(); |
| mWaitTransitionFinish = null; |
| } |
| |
| /** |
| * Called when a transition finished. |
| * Handle the pending animation when the running transition finished. |
| * @param targets The final animation targets derived in transition. |
| * @param finishedTransition The finished transition target. |
| */ |
| boolean onTransitionFinish(ArrayList<Transition.ChangeInfo> targets, |
| @NonNull Transition finishedTransition) { |
| if (finishedTransition == mWaitTransitionFinish) { |
| clearBackAnimations(); |
| } |
| |
| if (!mBackAnimationInProgress || mPendingAnimationBuilder == null) { |
| return false; |
| } |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, |
| "Handling the deferred animation after transition finished"); |
| |
| // Find the participated container collected by transition when : |
| // Open transition -> the open target in back navigation, the close target in transition. |
| // Close transition -> the close target in back navigation, the open target in transition. |
| boolean hasTarget = false; |
| for (int i = 0; i < finishedTransition.mParticipants.size(); i++) { |
| final WindowContainer wc = finishedTransition.mParticipants.valueAt(i); |
| if (wc.asActivityRecord() == null && wc.asTask() == null |
| && wc.asTaskFragment() == null) { |
| continue; |
| } |
| |
| if (mPendingAnimationBuilder.containTarget(wc)) { |
| hasTarget = true; |
| break; |
| } |
| } |
| |
| if (!hasTarget) { |
| // Skip if no target participated in current finished transition. |
| Slog.w(TAG, "Finished transition didn't include the targets" |
| + " open: " + Arrays.toString(mPendingAnimationBuilder.mOpenTargets) |
| + " close: " + mPendingAnimationBuilder.mCloseTarget); |
| cancelPendingAnimation(); |
| return false; |
| } |
| |
| // Ensure the final animation targets which hidden by transition could be visible. |
| for (int i = 0; i < targets.size(); i++) { |
| final WindowContainer wc = targets.get(i).mContainer; |
| wc.prepareSurfaces(); |
| } |
| |
| scheduleAnimation(mPendingAnimationBuilder); |
| mPendingAnimationBuilder = null; |
| return true; |
| } |
| |
| private void cancelPendingAnimation() { |
| if (mPendingAnimationBuilder == null) { |
| return; |
| } |
| try { |
| mPendingAnimationBuilder.mBackAnimationAdapter.getRunner().onAnimationCancelled(); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Remote animation gone", e); |
| } |
| mPendingAnimationBuilder = null; |
| } |
| |
| /** |
| * Create and handling animations status for an open/close animation targets. |
| */ |
| static class AnimationHandler { |
| private final boolean mShowWindowlessSurface; |
| private final WindowManagerService mWindowManagerService; |
| private BackWindowAnimationAdaptor mCloseAdaptor; |
| private BackWindowAnimationAdaptorWrapper mOpenAnimAdaptor; |
| private boolean mComposed; |
| private boolean mWaitTransition; |
| private int mSwitchType = UNKNOWN; |
| |
| // This will be set before transition happen, to know whether the real opening target |
| // exactly match animating target. When target match, reparent the starting surface to |
| // the opening target like starting window do. |
| private boolean mStartingSurfaceTargetMatch; |
| private ActivityRecord[] mOpenActivities; |
| |
| AnimationHandler(WindowManagerService wms) { |
| mWindowManagerService = wms; |
| final Context context = wms.mContext; |
| mShowWindowlessSurface = context.getResources().getBoolean( |
| com.android.internal.R.bool.config_predictShowStartingSurface); |
| } |
| private static final int UNKNOWN = 0; |
| private static final int TASK_SWITCH = 1; |
| private static final int ACTIVITY_SWITCH = 2; |
| private static final int DIALOG_CLOSE = 3; |
| |
| private static boolean isActivitySwitch(@NonNull WindowContainer close, |
| @NonNull WindowContainer[] open) { |
| if (open == null || open.length == 0 || close.asActivityRecord() == null) { |
| return false; |
| } |
| final Task closeTask = close.asActivityRecord().getTask(); |
| for (int i = open.length - 1; i >= 0; --i) { |
| if (open[i].asActivityRecord() == null |
| || (closeTask != open[i].asActivityRecord().getTask())) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private static boolean isTaskSwitch(@NonNull WindowContainer close, |
| @NonNull WindowContainer[] open) { |
| if (open == null || open.length != 1 || close.asTask() == null) { |
| return false; |
| } |
| return open[0].asTask() != null && (close.asTask() != open[0].asTask()); |
| } |
| |
| private static boolean isDialogClose(WindowContainer close) { |
| return close.asWindowState() != null; |
| } |
| |
| private void initiate(@NonNull WindowContainer close, @NonNull WindowContainer[] open, |
| @NonNull ActivityRecord[] openingActivities) { |
| if (isActivitySwitch(close, open)) { |
| mSwitchType = ACTIVITY_SWITCH; |
| } else if (isTaskSwitch(close, open)) { |
| mSwitchType = TASK_SWITCH; |
| } else if (isDialogClose(close)) { |
| mSwitchType = DIALOG_CLOSE; |
| } else { |
| mSwitchType = UNKNOWN; |
| return; |
| } |
| |
| mCloseAdaptor = createAdaptor(close, false, mSwitchType); |
| if (mCloseAdaptor.mAnimationTarget == null) { |
| Slog.w(TAG, "composeNewAnimations fail, skip"); |
| clearBackAnimateTarget(); |
| return; |
| } |
| |
| mOpenAnimAdaptor = new BackWindowAnimationAdaptorWrapper(true, mSwitchType, open); |
| if (!mOpenAnimAdaptor.isValid()) { |
| Slog.w(TAG, "compose animations fail, skip"); |
| clearBackAnimateTarget(); |
| return; |
| } |
| mOpenActivities = openingActivities; |
| } |
| |
| private boolean composeAnimations(@NonNull WindowContainer close, |
| @NonNull WindowContainer[] open, @NonNull ActivityRecord[] openingActivities) { |
| if (mComposed || mWaitTransition) { |
| Slog.e(TAG, "Previous animation is running " + this); |
| return false; |
| } |
| clearBackAnimateTarget(); |
| if (close == null || open == null || open.length == 0 || open.length > 2) { |
| Slog.e(TAG, "reset animation with null target close: " |
| + close + " open: " + Arrays.toString(open)); |
| return false; |
| } |
| initiate(close, open, openingActivities); |
| if (mSwitchType == UNKNOWN) { |
| return false; |
| } |
| mComposed = true; |
| mWaitTransition = false; |
| return true; |
| } |
| |
| @Nullable RemoteAnimationTarget[] getAnimationTargets() { |
| if (!mComposed) { |
| return null; |
| } |
| final RemoteAnimationTarget[] targets = new RemoteAnimationTarget[2]; |
| targets[0] = mCloseAdaptor.mAnimationTarget; |
| targets[1] = mOpenAnimAdaptor.getOrCreateAnimationTarget(); |
| return targets; |
| } |
| |
| boolean isSupportWindowlessSurface() { |
| return mWindowManagerService.mAtmService.mTaskOrganizerController |
| .isSupportWindowlessStartingSurface(); |
| } |
| |
| boolean containTarget(@NonNull ArrayList<WindowContainer> wcs, boolean open) { |
| for (int i = wcs.size() - 1; i >= 0; --i) { |
| if (isTarget(wcs.get(i), open)) { |
| return true; |
| } |
| } |
| return wcs.isEmpty(); |
| } |
| |
| boolean isTarget(@NonNull WindowContainer wc, boolean open) { |
| if (!mComposed) { |
| return false; |
| } |
| if (open) { |
| for (int i = mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) { |
| if (isAnimateTarget(wc, mOpenAnimAdaptor.mAdaptors[i].mTarget, mSwitchType)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| return isAnimateTarget(wc, mCloseAdaptor.mTarget, mSwitchType); |
| } |
| |
| private static boolean isAnimateTarget(@NonNull WindowContainer window, |
| @NonNull WindowContainer animationTarget, int switchType) { |
| if (switchType == TASK_SWITCH) { |
| // simplify home search for multiple hierarchy |
| if (window.isActivityTypeHome() && animationTarget.isActivityTypeHome()) { |
| return true; |
| } |
| return window == animationTarget |
| || (animationTarget.asTask() != null && animationTarget.hasChild(window)) |
| || (animationTarget.asActivityRecord() != null |
| && window.hasChild(animationTarget)); |
| } else if (switchType == ACTIVITY_SWITCH) { |
| return window == animationTarget |
| || (window.asTaskFragment() != null && window.hasChild(animationTarget)); |
| } |
| return false; |
| } |
| |
| void finishPresentAnimations() { |
| if (!mComposed) { |
| return; |
| } |
| |
| if (mCloseAdaptor != null) { |
| mCloseAdaptor.mTarget.cancelAnimation(); |
| mCloseAdaptor = null; |
| } |
| if (mOpenAnimAdaptor != null) { |
| mOpenAnimAdaptor.cleanUp(mStartingSurfaceTargetMatch); |
| mOpenAnimAdaptor = null; |
| } |
| |
| if (mOpenActivities != null) { |
| for (int i = mOpenActivities.length - 1; i >= 0; --i) { |
| if (mOpenActivities[i].mLaunchTaskBehind) { |
| restoreLaunchBehind(mOpenActivities[i]); |
| } |
| } |
| } |
| } |
| |
| void markStartingSurfaceMatch() { |
| mStartingSurfaceTargetMatch = true; |
| for (int i = mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) { |
| mOpenAnimAdaptor.mAdaptors[i].reparentWindowlessSurfaceToTarget(); |
| } |
| } |
| |
| void clearBackAnimateTarget() { |
| finishPresentAnimations(); |
| mComposed = false; |
| mWaitTransition = false; |
| mStartingSurfaceTargetMatch = false; |
| mSwitchType = UNKNOWN; |
| mOpenActivities = null; |
| } |
| |
| // The close target must in close list |
| // The open target can either in close or open list |
| boolean containsBackAnimationTargets(@NonNull ArrayList<WindowContainer> openApps, |
| @NonNull ArrayList<WindowContainer> closeApps) { |
| return containTarget(closeApps, false /* open */) |
| && (containTarget(openApps, true /* open */) |
| || containTarget(openApps, false /* open */)); |
| } |
| |
| @Override |
| public String toString() { |
| return "AnimationTargets{" |
| + " openTarget= " |
| + (mOpenAnimAdaptor != null ? dumpOpenAnimTargetsToString() : null) |
| + " closeTarget= " |
| + (mCloseAdaptor != null ? mCloseAdaptor.mTarget : null) |
| + " mSwitchType= " |
| + mSwitchType |
| + " mComposed= " |
| + mComposed |
| + " mWaitTransition= " |
| + mWaitTransition |
| + '}'; |
| } |
| |
| private String dumpOpenAnimTargetsToString() { |
| final StringBuilder sb = new StringBuilder(); |
| sb.append("{"); |
| for (int i = 0; i < mOpenAnimAdaptor.mAdaptors.length; i++) { |
| if (i > 0) { |
| sb.append(','); |
| } |
| sb.append(mOpenAnimAdaptor.mAdaptors[i].mTarget); |
| } |
| sb.append("}"); |
| return sb.toString(); |
| } |
| |
| @NonNull private static BackWindowAnimationAdaptor createAdaptor( |
| @NonNull WindowContainer target, boolean isOpen, int switchType) { |
| final BackWindowAnimationAdaptor adaptor = |
| new BackWindowAnimationAdaptor(target, isOpen, switchType); |
| final SurfaceControl.Transaction pt = target.getPendingTransaction(); |
| target.startAnimation(pt, adaptor, false /* hidden */, ANIMATION_TYPE_PREDICT_BACK); |
| // Workaround to show TaskFragment which can be hide in Transitions and won't show |
| // during isAnimating. |
| if (isOpen && target.asActivityRecord() != null) { |
| final TaskFragment fragment = target.asActivityRecord().getTaskFragment(); |
| if (fragment != null) { |
| pt.show(fragment.mSurfaceControl); |
| } |
| } |
| return adaptor; |
| } |
| |
| private static class BackWindowAnimationAdaptorWrapper { |
| final BackWindowAnimationAdaptor[] mAdaptors; |
| SurfaceControl.Transaction mCloseTransaction; |
| |
| BackWindowAnimationAdaptorWrapper(boolean isOpen, int switchType, |
| @NonNull WindowContainer... targets) { |
| mAdaptors = new BackWindowAnimationAdaptor[targets.length]; |
| for (int i = targets.length - 1; i >= 0; --i) { |
| mAdaptors[i] = createAdaptor(targets[i], isOpen, switchType); |
| } |
| } |
| |
| boolean isValid() { |
| for (int i = mAdaptors.length - 1; i >= 0; --i) { |
| if (mAdaptors[i].mAnimationTarget == null) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void cleanUp(boolean startingSurfaceMatch) { |
| for (int i = mAdaptors.length - 1; i >= 0; --i) { |
| mAdaptors[i].cleanUpWindowlessSurface(startingSurfaceMatch); |
| mAdaptors[i].mTarget.cancelAnimation(); |
| } |
| if (mCloseTransaction != null) { |
| mCloseTransaction.apply(); |
| mCloseTransaction = null; |
| } |
| } |
| |
| void onAnimationFinish() { |
| final SurfaceControl.Transaction pt = mAdaptors[0].mTarget.getPendingTransaction(); |
| if (mCloseTransaction != null) { |
| pt.merge(mCloseTransaction); |
| mCloseTransaction = null; |
| } |
| if (mAdaptors.length > 1) { |
| for (int i = mAdaptors.length - 1; i >= 0; --i) { |
| final WindowContainer wc = mAdaptors[i].mTarget; |
| final WindowContainer parent = wc.getParent(); |
| if (parent != null) { |
| pt.reparent(wc.getSurfaceControl(), |
| parent.getSurfaceControl()); |
| } |
| } |
| } |
| } |
| |
| @NonNull RemoteAnimationTarget getOrCreateAnimationTarget() { |
| // Special handle for opening two activities together. |
| // If we animate both activities separately, the animation area and rounded corner |
| // would also being handled separately. To make them seem like "open" together, wrap |
| // their leash with another animation leash. |
| if (mAdaptors.length > 1 && mCloseTransaction == null) { |
| final Rect unionBounds = new Rect(); |
| for (int i = mAdaptors.length - 1; i >= 0; --i) { |
| unionBounds.union(mAdaptors[i].mAnimationTarget.localBounds); |
| } |
| final WindowContainer wc = mAdaptors[0].mTarget; |
| final Task task = wc.asActivityRecord() != null |
| ? wc.asActivityRecord().getTask() : wc.asTask(); |
| final RemoteAnimationTarget represent = mAdaptors[0].mAnimationTarget; |
| final SurfaceControl leashSurface = new SurfaceControl.Builder() |
| .setName("cross-animation-leash") |
| .setContainerLayer() |
| .setHidden(false) |
| .setParent(task.getSurfaceControl()) |
| .build(); |
| final SurfaceControl.Transaction pt = wc.getPendingTransaction(); |
| pt.setLayer(leashSurface, wc.getParent().getLastLayer()); |
| mCloseTransaction = new SurfaceControl.Transaction(); |
| mCloseTransaction.reparent(leashSurface, null); |
| for (int i = mAdaptors.length - 1; i >= 0; --i) { |
| BackWindowAnimationAdaptor adaptor = mAdaptors[i]; |
| pt.reparent(adaptor.mAnimationTarget.leash, leashSurface); |
| pt.setPosition(adaptor.mAnimationTarget.leash, |
| adaptor.mAnimationTarget.localBounds.left, |
| adaptor.mAnimationTarget.localBounds.top); |
| } |
| return new RemoteAnimationTarget(represent.taskId, represent.mode, leashSurface, |
| represent.isTranslucent, represent.clipRect, represent.contentInsets, |
| represent.prefixOrderIndex, |
| new Point(unionBounds.left, unionBounds.top), |
| unionBounds, unionBounds, represent.windowConfiguration, |
| true /* isNotInRecents */, null, null, represent.taskInfo, |
| represent.allowEnterPip); |
| } else { |
| return mAdaptors[0].mAnimationTarget; |
| } |
| } |
| } |
| |
| private static class BackWindowAnimationAdaptor implements AnimationAdapter { |
| SurfaceControl mCapturedLeash; |
| private final Rect mBounds = new Rect(); |
| private final WindowContainer mTarget; |
| private final boolean mIsOpen; |
| private RemoteAnimationTarget mAnimationTarget; |
| private final int mSwitchType; |
| |
| // The starting surface task Id. Used to clear the starting surface if the animation has |
| // requested one during animating. |
| private int mRequestedStartingSurfaceId = INVALID_TASK_ID; |
| private SurfaceControl mStartingSurface; |
| |
| BackWindowAnimationAdaptor(@NonNull WindowContainer target, boolean isOpen, |
| int switchType) { |
| mBounds.set(target.getBounds()); |
| mTarget = target; |
| mIsOpen = isOpen; |
| mSwitchType = switchType; |
| } |
| |
| @Override |
| public boolean getShowWallpaper() { |
| return false; |
| } |
| |
| @Override |
| public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t, |
| int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) { |
| mCapturedLeash = animationLeash; |
| createRemoteAnimationTarget(); |
| final WindowState win = mTarget.asWindowState(); |
| if (win != null && mSwitchType == DIALOG_CLOSE) { |
| final Rect frame = win.getFrame(); |
| final Point position = new Point(); |
| win.transformFrameToSurfacePosition(frame.left, frame.top, position); |
| t.setPosition(mCapturedLeash, position.x, position.y); |
| } |
| } |
| |
| @Override |
| public void onAnimationCancelled(SurfaceControl animationLeash) { |
| if (mCapturedLeash == animationLeash) { |
| mCapturedLeash = null; |
| mRequestedStartingSurfaceId = INVALID_TASK_ID; |
| mStartingSurface = null; |
| } |
| } |
| |
| @Override |
| public long getDurationHint() { |
| return 0; |
| } |
| |
| @Override |
| public long getStatusBarTransitionsStartTime() { |
| return 0; |
| } |
| |
| @Override |
| public void dump(PrintWriter pw, String prefix) { |
| pw.print(prefix + "BackWindowAnimationAdaptor mCapturedLeash="); |
| pw.print(mCapturedLeash); |
| pw.println(); |
| } |
| |
| @Override |
| public void dumpDebug(ProtoOutputStream proto) { |
| |
| } |
| |
| RemoteAnimationTarget createRemoteAnimationTarget() { |
| if (mAnimationTarget != null) { |
| return mAnimationTarget; |
| } |
| |
| WindowState w = mTarget.asWindowState(); |
| ActivityRecord r = w != null ? w.getActivityRecord() : null; |
| Task t = r != null ? r.getTask() : mTarget.asTask(); |
| if (t == null && mTarget.asTaskFragment() != null) { |
| t = mTarget.asTaskFragment().getTask(); |
| r = mTarget.asTaskFragment().getTopNonFinishingActivity(); |
| } |
| if (r == null) { |
| r = t != null ? t.getTopNonFinishingActivity() |
| : mTarget.asActivityRecord(); |
| } |
| if (t == null && r != null) { |
| t = r.getTask(); |
| } |
| if (t == null || r == null) { |
| Slog.e(TAG, "createRemoteAnimationTarget fail " + mTarget); |
| return null; |
| } |
| final WindowState mainWindow = r.findMainWindow(); |
| Rect insets; |
| if (mainWindow != null) { |
| insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets( |
| mBounds, WindowInsets.Type.systemBars(), |
| false /* ignoreVisibility */).toRect(); |
| InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets()); |
| } else { |
| insets = new Rect(); |
| } |
| final int mode = mIsOpen ? MODE_OPENING : MODE_CLOSING; |
| mAnimationTarget = new RemoteAnimationTarget(t.mTaskId, mode, mCapturedLeash, |
| !r.fillsParent(), new Rect(), |
| insets, r.getPrefixOrderIndex(), new Point(mBounds.left, mBounds.top), |
| mBounds, mBounds, t.getWindowConfiguration(), |
| true /* isNotInRecents */, null, null, t.getTaskInfo(), |
| r.checkEnterPictureInPictureAppOpsState()); |
| return mAnimationTarget; |
| } |
| |
| void createStartingSurface(@NonNull WindowContainer closeWindow) { |
| if (!mIsOpen) { |
| return; |
| } |
| if (mSwitchType == DIALOG_CLOSE) { |
| return; |
| } |
| final Task openTask = mSwitchType == TASK_SWITCH |
| ? mTarget.asTask() : mSwitchType == ACTIVITY_SWITCH |
| ? mTarget.asActivityRecord().getTask() : null; |
| if (openTask == null) { |
| return; |
| } |
| final ActivityRecord mainActivity = mSwitchType == ACTIVITY_SWITCH |
| ? mTarget.asActivityRecord() |
| : openTask.getTopNonFinishingActivity(); |
| if (mainActivity == null) { |
| return; |
| } |
| final TaskSnapshot snapshot = getSnapshot(mTarget); |
| mRequestedStartingSurfaceId = openTask.mAtmService.mTaskOrganizerController |
| .addWindowlessStartingSurface(openTask, mainActivity, |
| // Choose configuration from closeWindow, because the configuration |
| // of opening target may not update before resume, so the starting |
| // surface should occlude it entirely. |
| mAnimationTarget.leash, snapshot, closeWindow.getConfiguration(), |
| new IWindowlessStartingSurfaceCallback.Stub() { |
| // Once the starting surface has been created in shell, it will call |
| // onSurfaceAdded to pass the created surface to core, so if a |
| // transition is triggered by the back gesture, there doesn't need to |
| // create another starting surface for the opening target, just reparent |
| // the starting surface to the opening target. |
| // Note if cleanUpWindowlessSurface happen prior than onSurfaceAdded |
| // called, there won't be able to reparent the starting surface on |
| // opening target. But if that happens and transition target is matched, |
| // the app window should already draw. |
| @Override |
| public void onSurfaceAdded(SurfaceControl sc) { |
| synchronized (mTarget.mWmService.mGlobalLock) { |
| if (mRequestedStartingSurfaceId != INVALID_TASK_ID) { |
| mStartingSurface = sc; |
| } |
| } |
| } |
| }); |
| } |
| |
| // When back gesture has triggered and transition target matches navigation target, |
| // reparent the starting surface to the opening target as it's starting window. |
| void reparentWindowlessSurfaceToTarget() { |
| if (mRequestedStartingSurfaceId == INVALID_TASK_ID) { |
| return; |
| } |
| // If open target matches, reparent to open activity or task |
| if (mStartingSurface != null && mStartingSurface.isValid()) { |
| mTarget.getPendingTransaction() |
| .reparent(mStartingSurface, mTarget.getSurfaceControl()); |
| // remove starting surface. |
| mStartingSurface = null; |
| } |
| } |
| |
| /** |
| * Ask shell to clear the starting surface. |
| * @param openTransitionMatch if true, shell will play the remove starting window |
| * animation, otherwise remove it directly. |
| */ |
| void cleanUpWindowlessSurface(boolean openTransitionMatch) { |
| if (mRequestedStartingSurfaceId == INVALID_TASK_ID) { |
| return; |
| } |
| mTarget.mWmService.mAtmService.mTaskOrganizerController |
| .removeWindowlessStartingSurface(mRequestedStartingSurfaceId, |
| !openTransitionMatch); |
| mRequestedStartingSurfaceId = INVALID_TASK_ID; |
| } |
| } |
| |
| ScheduleAnimationBuilder prepareAnimation( |
| int backType, |
| BackAnimationAdapter adapter, |
| Task currentTask, |
| Task previousTask, |
| ActivityRecord currentActivity, |
| ArrayList<ActivityRecord> previousActivity, |
| WindowContainer removedWindowContainer) { |
| switch (backType) { |
| case BackNavigationInfo.TYPE_RETURN_TO_HOME: |
| return new ScheduleAnimationBuilder(backType, adapter) |
| .setIsLaunchBehind(true) |
| .setComposeTarget(currentTask, previousTask); |
| case BackNavigationInfo.TYPE_CROSS_ACTIVITY: |
| ActivityRecord[] prevActs = new ActivityRecord[previousActivity.size()]; |
| prevActs = previousActivity.toArray(prevActs); |
| return new ScheduleAnimationBuilder(backType, adapter) |
| .setComposeTarget(currentActivity, prevActs) |
| .setIsLaunchBehind(false); |
| case BackNavigationInfo.TYPE_CROSS_TASK: |
| return new ScheduleAnimationBuilder(backType, adapter) |
| .setComposeTarget(currentTask, previousTask) |
| .setIsLaunchBehind(false); |
| case BackNavigationInfo.TYPE_DIALOG_CLOSE: |
| return new ScheduleAnimationBuilder(backType, adapter) |
| .setComposeTarget(removedWindowContainer, currentActivity) |
| .setIsLaunchBehind(false); |
| } |
| return null; |
| } |
| |
| class ScheduleAnimationBuilder { |
| final int mType; |
| final BackAnimationAdapter mBackAnimationAdapter; |
| WindowContainer mCloseTarget; |
| WindowContainer[] mOpenTargets; |
| boolean mIsLaunchBehind; |
| |
| ScheduleAnimationBuilder(int type, BackAnimationAdapter backAnimationAdapter) { |
| mType = type; |
| mBackAnimationAdapter = backAnimationAdapter; |
| } |
| |
| ScheduleAnimationBuilder setComposeTarget(@NonNull WindowContainer close, |
| @NonNull WindowContainer... open) { |
| mCloseTarget = close; |
| mOpenTargets = open; |
| return this; |
| } |
| |
| ScheduleAnimationBuilder setIsLaunchBehind(boolean launchBehind) { |
| mIsLaunchBehind = launchBehind; |
| return this; |
| } |
| |
| boolean containTarget(@NonNull WindowContainer wc) { |
| if (mOpenTargets != null) { |
| for (int i = mOpenTargets.length - 1; i >= 0; --i) { |
| if (wc == mOpenTargets[i] || mOpenTargets[i].hasChild(wc)) { |
| return true; |
| } |
| } |
| } |
| return wc == mCloseTarget || mCloseTarget.hasChild(wc); |
| } |
| |
| /** |
| * Apply preview strategy on the opening target |
| * @param closeWindow The close window, where it's configuration should cover all |
| * open target(s). |
| * @param openAnimationAdaptor The animator who can create starting surface. |
| * @param visibleOpenActivities The visible activities in opening targets. |
| */ |
| private void applyPreviewStrategy(@NonNull WindowContainer closeWindow, |
| @NonNull BackWindowAnimationAdaptor[] openAnimationAdaptor, |
| @NonNull ActivityRecord[] visibleOpenActivities) { |
| if (isSupportWindowlessSurface() && mShowWindowlessSurface && !mIsLaunchBehind |
| // TODO (b/274997067) Draw two snapshot in a single starting surface. |
| // We are using TaskId as the key of |
| // StartingSurfaceDrawer#StartingWindowRecordManager, so we cannot create |
| // two activity snapshot with WindowlessStartingWindow. |
| // Try to draw two snapshot within a WindowlessStartingWindow, or find |
| // another key for StartingWindowRecordManager. |
| && openAnimationAdaptor.length == 1) { |
| openAnimationAdaptor[0].createStartingSurface(closeWindow); |
| } else { |
| for (int i = visibleOpenActivities.length - 1; i >= 0; --i) { |
| setLaunchBehind(visibleOpenActivities[i]); |
| } |
| } |
| } |
| |
| @Nullable Runnable build() { |
| if (mOpenTargets == null || mCloseTarget == null || mOpenTargets.length == 0) { |
| return null; |
| } |
| final boolean shouldLaunchBehind = mIsLaunchBehind || !isSupportWindowlessSurface(); |
| final ActivityRecord[] openingActivities = getTopOpenActivities(mOpenTargets); |
| |
| if (shouldLaunchBehind && openingActivities == null) { |
| Slog.e(TAG, "No opening activity"); |
| return null; |
| } |
| |
| if (!composeAnimations(mCloseTarget, mOpenTargets, openingActivities)) { |
| return null; |
| } |
| mCloseTarget.mTransitionController.mSnapshotController |
| .mActivitySnapshotController.clearOnBackPressedActivities(); |
| applyPreviewStrategy(mCloseTarget, mOpenAnimAdaptor.mAdaptors, openingActivities); |
| |
| final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback(); |
| final RemoteAnimationTarget[] targets = getAnimationTargets(); |
| |
| return () -> { |
| try { |
| mBackAnimationAdapter.getRunner().onAnimationStart( |
| targets, null, null, callback); |
| } catch (RemoteException e) { |
| e.printStackTrace(); |
| } |
| }; |
| } |
| |
| private IBackAnimationFinishedCallback makeAnimationFinishedCallback() { |
| return new IBackAnimationFinishedCallback.Stub() { |
| @Override |
| public void onAnimationFinished(boolean triggerBack) { |
| synchronized (mWindowManagerService.mGlobalLock) { |
| if (!mComposed) { |
| // animation was canceled |
| return; |
| } |
| mOpenAnimAdaptor.onAnimationFinish(); |
| if (!triggerBack) { |
| clearBackAnimateTarget(); |
| } else { |
| mWaitTransition = true; |
| } |
| } |
| // TODO Add timeout monitor if transition didn't happen |
| } |
| }; |
| } |
| } |
| } |
| |
| /** |
| * Finds next opening activity(ies) based on open targets, which could be: |
| * 1. If the open window is Task, then the open activity can either be an activity, or |
| * two activities inside two TaskFragments |
| * 2. If the open window is Activity, then the open window can be an activity, or two |
| * adjacent TaskFragments below it. |
| */ |
| @Nullable |
| private static ActivityRecord[] getTopOpenActivities( |
| @NonNull WindowContainer[] openWindows) { |
| ActivityRecord[] openActivities = null; |
| final WindowContainer mainTarget = openWindows[0]; |
| if (mainTarget.asTask() != null) { |
| final ArrayList<ActivityRecord> inTaskActivities = new ArrayList<>(); |
| final Task task = mainTarget.asTask(); |
| final ActivityRecord tmpPreActivity = task.getTopNonFinishingActivity(); |
| if (tmpPreActivity != null) { |
| inTaskActivities.add(tmpPreActivity); |
| findAdjacentActivityIfExist(tmpPreActivity, inTaskActivities); |
| } |
| |
| openActivities = new ActivityRecord[inTaskActivities.size()]; |
| for (int i = inTaskActivities.size() - 1; i >= 0; --i) { |
| openActivities[i] = inTaskActivities.get(i); |
| } |
| } else if (mainTarget.asActivityRecord() != null) { |
| final int size = openWindows.length; |
| openActivities = new ActivityRecord[size]; |
| for (int i = size - 1; i >= 0; --i) { |
| openActivities[i] = openWindows[i].asActivityRecord(); |
| } |
| } |
| return openActivities; |
| } |
| |
| private static void setLaunchBehind(@NonNull ActivityRecord activity) { |
| if (!activity.isVisibleRequested()) { |
| activity.setVisibility(true); |
| // The transition could commit the visibility and in the finishing state, that could |
| // skip commitVisibility call in setVisibility cause the activity won't visible here. |
| // Call it again to make sure the activity could be visible while handling the pending |
| // animation. |
| activity.commitVisibility(true, true); |
| activity.mTransitionController.mSnapshotController |
| .mActivitySnapshotController.addOnBackPressedActivity(activity); |
| } |
| activity.mLaunchTaskBehind = true; |
| |
| // Handle fixed rotation launching app. |
| final DisplayContent dc = activity.mDisplayContent; |
| dc.rotateInDifferentOrientationIfNeeded(activity); |
| if (activity.hasFixedRotationTransform()) { |
| // Set the record so we can recognize it to continue to update display |
| // orientation if the previous activity becomes the top later. |
| dc.setFixedRotationLaunchingApp(activity, |
| activity.getWindowConfiguration().getRotation()); |
| } |
| |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, |
| "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity); |
| activity.mTaskSupervisor.mStoppingActivities.remove(activity); |
| activity.getDisplayContent().ensureActivitiesVisible(null /* starting */, |
| 0 /* configChanges */, false /* preserveWindows */, true); |
| } |
| |
| private static void restoreLaunchBehind(@NonNull ActivityRecord activity) { |
| activity.mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp(); |
| |
| // Restore the launch-behind state. |
| activity.mTaskSupervisor.scheduleLaunchTaskBehindComplete(activity.token); |
| activity.mLaunchTaskBehind = false; |
| // Ignore all change |
| activity.mTransitionController.mSnapshotController |
| .mActivitySnapshotController.clearOnBackPressedActivities(); |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, |
| "Setting Activity.mLauncherTaskBehind to false. Activity=%s", |
| activity); |
| } |
| |
| void checkAnimationReady(WallpaperController wallpaperController) { |
| if (!mBackAnimationInProgress) { |
| return; |
| } |
| |
| final boolean wallpaperReady = !mShowWallpaper |
| || (wallpaperController.getWallpaperTarget() != null |
| && wallpaperController.wallpaperTransitionReady()); |
| if (wallpaperReady && mPendingAnimation != null) { |
| startAnimation(); |
| } |
| } |
| |
| void startAnimation() { |
| if (!mBackAnimationInProgress) { |
| // gesture is already finished, do not start animation |
| if (mPendingAnimation != null) { |
| clearBackAnimations(); |
| mPendingAnimation = null; |
| } |
| return; |
| } |
| if (mPendingAnimation != null) { |
| mPendingAnimation.run(); |
| mPendingAnimation = null; |
| } |
| } |
| |
| private void onBackNavigationDone(Bundle result, int backType) { |
| boolean triggerBack = result != null && result.getBoolean( |
| BackNavigationInfo.KEY_TRIGGER_BACK); |
| ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, " |
| + "triggerBack=%b", backType, triggerBack); |
| |
| mNavigationMonitor.stopMonitorForRemote(); |
| mBackAnimationInProgress = false; |
| mShowWallpaper = false; |
| mPendingAnimationBuilder = null; |
| } |
| |
| static TaskSnapshot getSnapshot(@NonNull WindowContainer w) { |
| if (w.asTask() != null) { |
| final Task task = w.asTask(); |
| return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( |
| task.mTaskId, task.mUserId, false /* restoreFromDisk */, |
| false /* isLowResolution */); |
| } |
| |
| if (w.asActivityRecord() != null) { |
| final ActivityRecord ar = w.asActivityRecord(); |
| return ar.mWmService.mSnapshotController.mActivitySnapshotController.getSnapshot(ar); |
| } |
| return null; |
| } |
| |
| void setWindowManager(WindowManagerService wm) { |
| mWindowManagerService = wm; |
| mAnimationHandler = new AnimationHandler(wm); |
| } |
| |
| boolean isWallpaperVisible(WindowState w) { |
| return mAnimationHandler.mComposed && mShowWallpaper |
| && w.mAttrs.type == TYPE_BASE_APPLICATION && w.mActivityRecord != null |
| && mAnimationHandler.isTarget(w.mActivityRecord, true /* open */); |
| } |
| |
| // Called from WindowManagerService to write to a protocol buffer output stream. |
| void dumpDebug(ProtoOutputStream proto, long fieldId) { |
| final long token = proto.start(fieldId); |
| proto.write(ANIMATION_IN_PROGRESS, mBackAnimationInProgress); |
| proto.write(LAST_BACK_TYPE, mLastBackType); |
| proto.write(SHOW_WALLPAPER, mShowWallpaper); |
| if (mAnimationHandler.mOpenAnimAdaptor != null |
| && mAnimationHandler.mOpenAnimAdaptor.mAdaptors.length > 0) { |
| mAnimationHandler.mOpenActivities[0].writeNameToProto( |
| proto, MAIN_OPEN_ACTIVITY); |
| } else { |
| proto.write(MAIN_OPEN_ACTIVITY, ""); |
| } |
| // TODO (b/268563842) Only meaningful after new test added |
| proto.write(ANIMATION_RUNNING, mAnimationHandler.mComposed |
| || mAnimationHandler.mWaitTransition); |
| proto.end(token); |
| } |
| } |