| /* |
| * Copyright (C) 2022 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.systemui.animation |
| |
| import android.annotation.SuppressLint |
| import android.app.WindowConfiguration |
| import android.graphics.Point |
| import android.graphics.Rect |
| import android.os.IBinder |
| import android.os.RemoteException |
| import android.util.ArrayMap |
| import android.util.Log |
| import android.util.RotationUtils |
| import android.view.IRemoteAnimationFinishedCallback |
| import android.view.IRemoteAnimationRunner |
| import android.view.RemoteAnimationAdapter |
| import android.view.RemoteAnimationTarget |
| import android.view.SurfaceControl |
| import android.view.WindowManager |
| import android.window.IRemoteTransition |
| import android.window.IRemoteTransitionFinishedCallback |
| import android.window.RemoteTransition |
| import android.window.TransitionInfo |
| |
| class RemoteTransitionAdapter { |
| companion object { |
| /** |
| * Almost a copy of Transitions#setupStartState. |
| * |
| * TODO: remove when there is proper cross-process transaction sync. |
| */ |
| @SuppressLint("NewApi") |
| private fun setupLeash( |
| leash: SurfaceControl, |
| change: TransitionInfo.Change, |
| layer: Int, |
| info: TransitionInfo, |
| t: SurfaceControl.Transaction |
| ) { |
| val isOpening = |
| info.type == WindowManager.TRANSIT_OPEN || |
| info.type == WindowManager.TRANSIT_TO_FRONT |
| // Put animating stuff above this line and put static stuff below it. |
| val zSplitLine = info.changes.size |
| // changes should be ordered top-to-bottom in z |
| val mode = change.mode |
| |
| // Launcher animates leaf tasks directly, so always reparent all task leashes to root. |
| t.reparent(leash, info.rootLeash) |
| t.setPosition( |
| leash, |
| (change.startAbsBounds.left - info.rootOffset.x).toFloat(), |
| (change.startAbsBounds.top - info.rootOffset.y).toFloat() |
| ) |
| t.show(leash) |
| // Put all the OPEN/SHOW on top |
| if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) { |
| if (isOpening) { |
| t.setLayer(leash, zSplitLine + info.changes.size - layer) |
| if ( |
| change.flags and TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT == 0 |
| ) { |
| // if transferred, it should be left visible. |
| t.setAlpha(leash, 0f) |
| } |
| } else { |
| // put on bottom and leave it visible |
| t.setLayer(leash, zSplitLine - layer) |
| } |
| } else if ( |
| mode == WindowManager.TRANSIT_CLOSE || mode == WindowManager.TRANSIT_TO_BACK |
| ) { |
| if (isOpening) { |
| // put on bottom and leave visible |
| t.setLayer(leash, zSplitLine - layer) |
| } else { |
| // put on top |
| t.setLayer(leash, zSplitLine + info.changes.size - layer) |
| } |
| } else { // CHANGE |
| t.setLayer(leash, zSplitLine + info.changes.size - layer) |
| } |
| } |
| |
| @SuppressLint("NewApi") |
| private fun createLeash( |
| info: TransitionInfo, |
| change: TransitionInfo.Change, |
| order: Int, |
| t: SurfaceControl.Transaction |
| ): SurfaceControl { |
| // TODO: once we can properly sync transactions across process, then get rid of this. |
| if (change.parent != null && change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { |
| // Special case for wallpaper atm. Normally these are left alone; but, a quirk of |
| // making leashes means we have to handle them specially. |
| return change.leash |
| } |
| val leashSurface = |
| SurfaceControl.Builder() |
| .setName(change.leash.toString() + "_transition-leash") |
| .setContainerLayer() |
| .setParent( |
| if (change.parent == null) info.rootLeash |
| else info.getChange(change.parent!!)!!.leash |
| ) |
| .build() |
| // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) |
| setupLeash(leashSurface, change, info.changes.size - order, info, t) |
| t.reparent(change.leash, leashSurface) |
| t.setAlpha(change.leash, 1.0f) |
| t.show(change.leash) |
| t.setPosition(change.leash, 0f, 0f) |
| t.setLayer(change.leash, 0) |
| return leashSurface |
| } |
| |
| private fun newModeToLegacyMode(newMode: Int): Int { |
| return when (newMode) { |
| WindowManager.TRANSIT_OPEN, |
| WindowManager.TRANSIT_TO_FRONT -> RemoteAnimationTarget.MODE_OPENING |
| WindowManager.TRANSIT_CLOSE, |
| WindowManager.TRANSIT_TO_BACK -> RemoteAnimationTarget.MODE_CLOSING |
| else -> RemoteAnimationTarget.MODE_CHANGING |
| } |
| } |
| |
| private fun rectOffsetTo(rect: Rect, offset: Point): Rect { |
| val out = Rect(rect) |
| out.offsetTo(offset.x, offset.y) |
| return out |
| } |
| |
| fun createTarget( |
| change: TransitionInfo.Change, |
| order: Int, |
| info: TransitionInfo, |
| t: SurfaceControl.Transaction |
| ): RemoteAnimationTarget { |
| val target = |
| RemoteAnimationTarget( |
| /* taskId */ if (change.taskInfo != null) change.taskInfo!!.taskId else -1, |
| /* mode */ newModeToLegacyMode(change.mode), |
| /* leash */ createLeash(info, change, order, t), |
| /* isTranslucent */ (change.flags and TransitionInfo.FLAG_TRANSLUCENT != 0 || |
| change.flags and TransitionInfo.FLAG_SHOW_WALLPAPER != 0), |
| /* clipRect */ null, |
| /* contentInsets */ Rect(0, 0, 0, 0), |
| /* prefixOrderIndex */ order, |
| /* position */ null, |
| /* localBounds */ rectOffsetTo(change.endAbsBounds, change.endRelOffset), |
| /* screenSpaceBounds */ Rect(change.endAbsBounds), |
| /* windowConfig */ if (change.taskInfo != null) |
| change.taskInfo!!.configuration.windowConfiguration |
| else WindowConfiguration(), |
| /* isNotInRecents */ if (change.taskInfo != null) !change.taskInfo!!.isRunning |
| else true, |
| /* startLeash */ null, |
| /* startBounds */ Rect(change.startAbsBounds), |
| /* taskInfo */ change.taskInfo, |
| /* allowEnterPip */ change.allowEnterPip, |
| /* windowType */ WindowManager.LayoutParams.INVALID_WINDOW_TYPE |
| ) |
| target.backgroundColor = change.backgroundColor |
| return target |
| } |
| |
| /** |
| * Represents a TransitionInfo object as an array of old-style targets |
| * |
| * @param wallpapers If true, this will return wallpaper targets; otherwise it returns |
| * non-wallpaper targets. |
| * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should |
| * be populated by this function. If null, it is ignored. |
| */ |
| fun wrapTargets( |
| info: TransitionInfo, |
| wallpapers: Boolean, |
| t: SurfaceControl.Transaction, |
| leashMap: ArrayMap<SurfaceControl, SurfaceControl>? |
| ): Array<RemoteAnimationTarget> { |
| val out = ArrayList<RemoteAnimationTarget>() |
| for (i in info.changes.indices) { |
| val change = info.changes[i] |
| if (change.hasFlags(TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { |
| // For embedded container, when the parent Task is also in the transition, we |
| // should only animate the parent Task. |
| if (change.parent != null) continue |
| // For embedded container without parent, we should only animate if it fills |
| // the Task. Otherwise we may animate only partial of the Task. |
| if (!change.hasFlags(TransitionInfo.FLAG_FILLS_TASK)) continue |
| } |
| // Check if it is wallpaper |
| if (wallpapers != change.hasFlags(TransitionInfo.FLAG_IS_WALLPAPER)) continue |
| out.add(createTarget(change, info.changes.size - i, info, t)) |
| if (leashMap != null) { |
| leashMap[change.leash] = out[out.size - 1].leash |
| } |
| } |
| return out.toTypedArray() |
| } |
| |
| @JvmStatic |
| fun adaptRemoteRunner(runner: IRemoteAnimationRunner): IRemoteTransition.Stub { |
| return object : IRemoteTransition.Stub() { |
| override fun startAnimation( |
| token: IBinder, |
| info: TransitionInfo, |
| t: SurfaceControl.Transaction, |
| finishCallback: IRemoteTransitionFinishedCallback |
| ) { |
| val leashMap = ArrayMap<SurfaceControl, SurfaceControl>() |
| val appsCompat = wrapTargets(info, false /* wallpapers */, t, leashMap) |
| val wallpapersCompat = wrapTargets(info, true /* wallpapers */, t, leashMap) |
| // TODO(bc-unlock): Build wrapped object for non-apps target. |
| val nonAppsCompat = arrayOfNulls<RemoteAnimationTarget>(0) |
| |
| // TODO(b/177438007): Move this set-up logic into launcher's animation impl. |
| var isReturnToHome = false |
| var launcherTask: TransitionInfo.Change? = null |
| var wallpaper: TransitionInfo.Change? = null |
| var launcherLayer = 0 |
| var rotateDelta = 0 |
| var displayW = 0f |
| var displayH = 0f |
| for (i in info.changes.indices.reversed()) { |
| val change = info.changes[i] |
| if ( |
| change.taskInfo != null && |
| change.taskInfo!!.activityType == |
| WindowConfiguration.ACTIVITY_TYPE_HOME |
| ) { |
| isReturnToHome = |
| (change.mode == WindowManager.TRANSIT_OPEN || |
| change.mode == WindowManager.TRANSIT_TO_FRONT) |
| launcherTask = change |
| launcherLayer = info.changes.size - i |
| } else if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { |
| wallpaper = change |
| } |
| if ( |
| change.parent == null && |
| change.endRotation >= 0 && |
| change.endRotation != change.startRotation |
| ) { |
| rotateDelta = change.endRotation - change.startRotation |
| displayW = change.endAbsBounds.width().toFloat() |
| displayH = change.endAbsBounds.height().toFloat() |
| } |
| } |
| |
| // Prepare for rotation if there is one |
| val counterLauncher = CounterRotator() |
| val counterWallpaper = CounterRotator() |
| if (launcherTask != null && rotateDelta != 0 && launcherTask.parent != null) { |
| counterLauncher.setup( |
| t, |
| info.getChange(launcherTask.parent!!)!!.leash, |
| rotateDelta, |
| displayW, |
| displayH |
| ) |
| if (counterLauncher.surface != null) { |
| t.setLayer(counterLauncher.surface!!, launcherLayer) |
| } |
| } |
| if (isReturnToHome) { |
| if (counterLauncher.surface != null) { |
| t.setLayer(counterLauncher.surface!!, info.changes.size * 3) |
| } |
| // Need to "boost" the closing things since that's what launcher expects. |
| for (i in info.changes.indices.reversed()) { |
| val change = info.changes[i] |
| val leash = leashMap[change.leash] |
| val mode = info.changes[i].mode |
| // Only deal with independent layers |
| if (!TransitionInfo.isIndependent(change, info)) continue |
| if ( |
| mode == WindowManager.TRANSIT_CLOSE || |
| mode == WindowManager.TRANSIT_TO_BACK |
| ) { |
| t.setLayer(leash!!, info.changes.size * 3 - i) |
| counterLauncher.addChild(t, leash) |
| } |
| } |
| // Make wallpaper visible immediately since sysui apparently won't do this. |
| for (i in wallpapersCompat.indices.reversed()) { |
| t.show(wallpapersCompat[i].leash) |
| t.setAlpha(wallpapersCompat[i].leash, 1f) |
| } |
| } else { |
| if (launcherTask != null) { |
| counterLauncher.addChild(t, leashMap[launcherTask.leash]) |
| } |
| if (wallpaper != null && rotateDelta != 0 && wallpaper.parent != null) { |
| counterWallpaper.setup( |
| t, |
| info.getChange(wallpaper.parent!!)!!.leash, |
| rotateDelta, |
| displayW, |
| displayH |
| ) |
| if (counterWallpaper.surface != null) { |
| t.setLayer(counterWallpaper.surface!!, -1) |
| counterWallpaper.addChild(t, leashMap[wallpaper.leash]) |
| } |
| } |
| } |
| t.apply() |
| val animationFinishedCallback = |
| object : IRemoteAnimationFinishedCallback { |
| override fun onAnimationFinished() { |
| val finishTransaction = SurfaceControl.Transaction() |
| counterLauncher.cleanUp(finishTransaction) |
| counterWallpaper.cleanUp(finishTransaction) |
| // Release surface references now. This is apparently to free GPU |
| // memory while doing quick operations (eg. during CTS). |
| info.releaseAllSurfaces() |
| for (i in leashMap.size - 1 downTo 0) { |
| leashMap.valueAt(i).release() |
| } |
| try { |
| finishCallback.onTransitionFinished( |
| null /* wct */, |
| finishTransaction |
| ) |
| finishTransaction.close() |
| } catch (e: RemoteException) { |
| Log.e( |
| "ActivityOptionsCompat", |
| "Failed to call app controlled" + |
| " animation finished callback", |
| e |
| ) |
| } |
| } |
| |
| override fun asBinder(): IBinder? { |
| return null |
| } |
| } |
| // TODO(bc-unlcok): Pass correct transit type. |
| runner.onAnimationStart( |
| WindowManager.TRANSIT_OLD_NONE, |
| appsCompat, |
| wallpapersCompat, |
| nonAppsCompat, |
| animationFinishedCallback |
| ) |
| } |
| |
| override fun mergeAnimation( |
| token: IBinder, |
| info: TransitionInfo, |
| t: SurfaceControl.Transaction, |
| mergeTarget: IBinder, |
| finishCallback: IRemoteTransitionFinishedCallback |
| ) { |
| // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, |
| // ignore any incoming merges. |
| // Clean up stuff though cuz GC takes too long for benchmark tests. |
| t.close() |
| info.releaseAllSurfaces() |
| } |
| } |
| } |
| |
| @JvmStatic |
| fun adaptRemoteAnimation(adapter: RemoteAnimationAdapter): RemoteTransition { |
| return RemoteTransition(adaptRemoteRunner(adapter.runner), adapter.callingApplication) |
| } |
| } |
| |
| /** Utility class that takes care of counter-rotating surfaces during a transition animation. */ |
| class CounterRotator { |
| /** Gets the surface with the counter-rotation. */ |
| var surface: SurfaceControl? = null |
| private set |
| |
| /** |
| * Sets up this rotator. |
| * |
| * @param rotateDelta is the forward rotation change (the rotation the display is making). |
| * @param parentW (and H) Is the size of the rotating parent. |
| */ |
| fun setup( |
| t: SurfaceControl.Transaction, |
| parent: SurfaceControl, |
| rotateDelta: Int, |
| parentW: Float, |
| parentH: Float |
| ) { |
| if (rotateDelta == 0) return |
| val surface = |
| SurfaceControl.Builder() |
| .setName("Transition Unrotate") |
| .setContainerLayer() |
| .setParent(parent) |
| .build() |
| // Rotate forward to match the new rotation (rotateDelta is the forward rotation the |
| // parent already took). Child surfaces will be in the old rotation relative to the new |
| // parent rotation, so we need to forward-rotate the child surfaces to match. |
| RotationUtils.rotateSurface(t, surface, rotateDelta) |
| val tmpPt = Point(0, 0) |
| // parentW/H are the size in the END rotation, the rotation utilities expect the |
| // starting size. So swap them if necessary |
| val flipped = rotateDelta % 2 != 0 |
| val pw = if (flipped) parentH else parentW |
| val ph = if (flipped) parentW else parentH |
| RotationUtils.rotatePoint(tmpPt, rotateDelta, pw.toInt(), ph.toInt()) |
| t.setPosition(surface, tmpPt.x.toFloat(), tmpPt.y.toFloat()) |
| t.show(surface) |
| } |
| |
| /** Adds a surface that needs to be counter-rotate. */ |
| fun addChild(t: SurfaceControl.Transaction, child: SurfaceControl?) { |
| if (surface == null) return |
| t.reparent(child!!, surface) |
| } |
| |
| /** |
| * Clean-up. Since finishTransaction should reset all change leashes, we only need to remove |
| * the counter rotation surface. |
| */ |
| fun cleanUp(finishTransaction: SurfaceControl.Transaction) { |
| if (surface == null) return |
| finishTransaction.remove(surface!!) |
| } |
| } |
| } |