content: /*
   * Copyright (C) 2018 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 com.android.server.wm.AnimationAdapterProto.REMOTE;
  import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
  import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
  import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_REMOTE_ANIMATIONS;
  import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
  import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
  
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.os.Binder;
  import android.os.Handler;
  import android.os.IBinder.DeathRecipient;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.util.Slog;
  import android.util.proto.ProtoOutputStream;
  import android.view.IRemoteAnimationFinishedCallback;
  import android.view.RemoteAnimationAdapter;
  import android.view.RemoteAnimationTarget;
  import android.view.SurfaceControl;
  import android.view.SurfaceControl.Transaction;
  
  import com.android.internal.util.FastPrintWriter;
  import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
  import com.android.server.wm.utils.InsetUtils;
  
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.util.ArrayList;
  
  /**
   * Helper class to run app animations in a remote process.
   */
  class RemoteAnimationController implements DeathRecipient {
      private static final String TAG = TAG_WITH_CLASS_NAME
              || (DEBUG_REMOTE_ANIMATIONS && !DEBUG_APP_TRANSITIONS)
                      ? "RemoteAnimationController" : TAG_WM;
      private static final long TIMEOUT_MS = 2000;
  
      private final WindowManagerService mService;
      private final RemoteAnimationAdapter mRemoteAnimationAdapter;
      private final ArrayList<RemoteAnimationRecord> mPendingAnimations = new ArrayList<>();
      private final Rect mTmpRect = new Rect();
      private final Handler mHandler;
      private final Runnable mTimeoutRunnable = () -> cancelAnimation("timeoutRunnable");
  
      private FinishedCallback mFinishedCallback;
      private boolean mCanceled;
      private boolean mLinkedToDeathOfRunner;
  
      RemoteAnimationController(WindowManagerService service,
              RemoteAnimationAdapter remoteAnimationAdapter, Handler handler) {
          mService = service;
          mRemoteAnimationAdapter = remoteAnimationAdapter;
          mHandler = handler;
      }
  
      /**
       * Creates an animation record for each individual {@link AppWindowToken}.
       *
       * @param appWindowToken The app to animate.
       * @param position The position app bounds, in screen coordinates.
       * @param stackBounds The stack bounds of the app relative to position.
       * @param startBounds The stack bounds before the transition, in screen coordinates
       * @return The record representing animation(s) to run on the app.
       */
      RemoteAnimationRecord createRemoteAnimationRecord(AppWindowToken appWindowToken,
              Point position, Rect stackBounds, Rect startBounds) {
          if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "createAnimationAdapter(): token="
                  + appWindowToken);
          final RemoteAnimationRecord adapters =
                  new RemoteAnimationRecord(appWindowToken, position, stackBounds, startBounds);
          mPendingAnimations.add(adapters);
          return adapters;
      }
  
      /**
       * Called when the transition is ready to be started, and all leashes have been set up.
       */
      void goodToGo() {
          if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo()");
          if (mPendingAnimations.isEmpty() || mCanceled) {
              if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo(): Animation finished already,"
                      + " canceled=" + mCanceled
                      + " mPendingAnimations=" + mPendingAnimations.size());
              onAnimationFinished();
              return;
          }
  
          // Scale the timeout with the animator scale the controlling app is using.
          mHandler.postDelayed(mTimeoutRunnable,
                  (long) (TIMEOUT_MS * mService.getCurrentAnimatorScale()));
          mFinishedCallback = new FinishedCallback(this);
  
          final RemoteAnimationTarget[] animations = createAnimations();
          if (animations.length == 0) {
              if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo(): No apps to animate");
              onAnimationFinished();
              return;
          }
          mService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
              try {
                  linkToDeathOfRunner();
                  mRemoteAnimationAdapter.getRunner().onAnimationStart(animations, mFinishedCallback);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Failed to start remote animation", e);
                  onAnimationFinished();
              }
              if (DEBUG_REMOTE_ANIMATIONS) {
                  Slog.d(TAG, "startAnimation(): Notify animation start:");
                  writeStartDebugStatement();
              }
          });
          setRunningRemoteAnimation(true);
      }
  
      void cancelAnimation(String reason) {
          if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "cancelAnimation(): reason=" + reason);
          synchronized (mService.getWindowManagerLock()) {
              if (mCanceled) {
                  return;
              }
              mCanceled = true;
          }
          onAnimationFinished();
          invokeAnimationCancelled();
      }
  
      private void writeStartDebugStatement() {
          Slog.i(TAG, "Starting remote animation");
          final StringWriter sw = new StringWriter();
          final FastPrintWriter pw = new FastPrintWriter(sw);
          for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
              mPendingAnimations.get(i).mAdapter.dump(pw, "");
          }
          pw.close();
          Slog.i(TAG, sw.toString());
      }
  
      private RemoteAnimationTarget[] createAnimations() {
          if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "createAnimations()");
          final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
          for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
              final RemoteAnimationRecord wrappers = mPendingAnimations.get(i);
              final RemoteAnimationTarget target = wrappers.createRemoteAnimationTarget();
              if (target != null) {
                  if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\tAdd token=" + wrappers.mAppWindowToken);
                  targets.add(target);
              } else {
                  if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\tRemove token="
                          + wrappers.mAppWindowToken);
  
                  // We can't really start an animation but we still need to make sure to finish the
                  // pending animation that was started by SurfaceAnimator
                  if (wrappers.mAdapter != null
                          && wrappers.mAdapter.mCapturedFinishCallback != null) {
                      wrappers.mAdapter.mCapturedFinishCallback
                              .onAnimationFinished(wrappers.mAdapter);
                  }
                  if (wrappers.mThumbnailAdapter != null
                          && wrappers.mThumbnailAdapter.mCapturedFinishCallback != null) {
                      wrappers.mThumbnailAdapter.mCapturedFinishCallback
                              .onAnimationFinished(wrappers.mThumbnailAdapter);
                  }
                  mPendingAnimations.remove(i);
              }
          }
          return targets.toArray(new RemoteAnimationTarget[targets.size()]);
      }
  
      private void onAnimationFinished() {
          if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "onAnimationFinished(): mPendingAnimations="
                  + mPendingAnimations.size());
          mHandler.removeCallbacks(mTimeoutRunnable);
          synchronized (mService.mGlobalLock) {
              unlinkToDeathOfRunner();
              releaseFinishedCallback();
              mService.openSurfaceTransaction();
              try {
                  if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG,
                          "onAnimationFinished(): Notify animation finished:");
                  for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
                      final RemoteAnimationRecord adapters = mPendingAnimations.get(i);
                      if (adapters.mAdapter != null) {
                          adapters.mAdapter.mCapturedFinishCallback
                                  .onAnimationFinished(adapters.mAdapter);
                      }
                      if (adapters.mThumbnailAdapter != null) {
                          adapters.mThumbnailAdapter.mCapturedFinishCallback
                                  .onAnimationFinished(adapters.mThumbnailAdapter);
                      }
                      if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\t" + adapters.mAppWindowToken);
                  }
              } catch (Exception e) {
                  Slog.e(TAG, "Failed to finish remote animation", e);
                  throw e;
              } finally {
                  mService.closeSurfaceTransaction("RemoteAnimationController#finished");
              }
          }
          setRunningRemoteAnimation(false);
          if (DEBUG_REMOTE_ANIMATIONS) Slog.i(TAG, "Finishing remote animation");
      }
  
      private void invokeAnimationCancelled() {
          try {
              mRemoteAnimationAdapter.getRunner().onAnimationCancelled();
          } catch (RemoteException e) {
              Slog.e(TAG, "Failed to notify cancel", e);
          }
      }
  
      private void releaseFinishedCallback() {
          if (mFinishedCallback != null) {
              mFinishedCallback.release();
              mFinishedCallback = null;
          }
      }
  
      private void setRunningRemoteAnimation(boolean running) {
          final int pid = mRemoteAnimationAdapter.getCallingPid();
          final int uid = mRemoteAnimationAdapter.getCallingUid();
          if (pid == 0) {
              throw new RuntimeException("Calling pid of remote animation was null");
          }
          final WindowProcessController wpc = mService.mAtmService.getProcessController(pid, uid);
          if (wpc == null) {
              Slog.w(TAG, "Unable to find process with pid=" + pid + " uid=" + uid);
              return;
          }
          wpc.setRunningRemoteAnimation(running);
      }
  
      private void linkToDeathOfRunner() throws RemoteException {
          if (!mLinkedToDeathOfRunner) {
              mRemoteAnimationAdapter.getRunner().asBinder().linkToDeath(this, 0);
              mLinkedToDeathOfRunner = true;
          }
      }
  
      private void unlinkToDeathOfRunner() {
          if (mLinkedToDeathOfRunner) {
              mRemoteAnimationAdapter.getRunner().asBinder().unlinkToDeath(this, 0);
              mLinkedToDeathOfRunner = false;
          }
      }
  
      @Override
      public void binderDied() {
          cancelAnimation("binderDied");
      }
  
      private static final class FinishedCallback extends IRemoteAnimationFinishedCallback.Stub {
  
          RemoteAnimationController mOuter;
  
          FinishedCallback(RemoteAnimationController outer) {
              mOuter = outer;
          }
  
          @Override
          public void onAnimationFinished() throws RemoteException {
              if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "app-onAnimationFinished(): mOuter=" + mOuter);
              final long token = Binder.clearCallingIdentity();
              try {
                  if (mOuter != null) {
                      mOuter.onAnimationFinished();
  
                      // In case the client holds on to the finish callback, make sure we don't leak
                      // RemoteAnimationController which in turn would leak the runner on the client.
                      mOuter = null;
                  }
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
          }
  
          /**
           * Marks this callback as not be used anymore by releasing the reference to the outer class
           * to prevent memory leak.
           */
          void release() {
              if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "app-release(): mOuter=" + mOuter);
              mOuter = null;
          }
      };
  
      /**
       * Contains information about a remote-animation for one AppWindowToken. This keeps track of,
       * potentially, multiple animating surfaces (AdapterWrappers) associated with one
       * Window/Transition. For example, a change transition has an adapter controller for the
       * main window and an adapter controlling the start-state snapshot.
       * <p>
       * This can be thought of as a bridge between the information that the remote animator sees (via
       * {@link RemoteAnimationTarget}) and what the server sees (the
       * {@link RemoteAnimationAdapterWrapper}(s) interfacing with the moving surfaces).
       */
      public class RemoteAnimationRecord {
          RemoteAnimationAdapterWrapper mAdapter;
          RemoteAnimationAdapterWrapper mThumbnailAdapter = null;
          RemoteAnimationTarget mTarget;
          final AppWindowToken mAppWindowToken;
          final Rect mStartBounds;
  
          RemoteAnimationRecord(AppWindowToken appWindowToken, Point endPos, Rect endBounds,
                  Rect startBounds) {
              mAppWindowToken = appWindowToken;
              mAdapter = new RemoteAnimationAdapterWrapper(this, endPos, endBounds);
              if (startBounds != null) {
                  mStartBounds = new Rect(startBounds);
                  mTmpRect.set(startBounds);
                  mTmpRect.offsetTo(0, 0);
                  if (mRemoteAnimationAdapter.getChangeNeedsSnapshot()) {
                      mThumbnailAdapter =
                              new RemoteAnimationAdapterWrapper(this, new Point(0, 0), mTmpRect);
                  }
              } else {
                  mStartBounds = null;
              }
          }
  
          RemoteAnimationTarget createRemoteAnimationTarget() {
              final Task task = mAppWindowToken.getTask();
              final WindowState mainWindow = mAppWindowToken.findMainWindow();
              if (task == null || mainWindow == null || mAdapter == null
                      || mAdapter.mCapturedFinishCallback == null
                      || mAdapter.mCapturedLeash == null) {
                  return null;
              }
              final Rect insets = new Rect();
              mainWindow.getContentInsets(insets);
              InsetUtils.addInsets(insets, mAppWindowToken.getLetterboxInsets());
              mTarget = new RemoteAnimationTarget(task.mTaskId, getMode(),
                      mAdapter.mCapturedLeash, !mAppWindowToken.fillsParent(),
                      mainWindow.mWinAnimator.mLastClipRect, insets,
                      mAppWindowToken.getPrefixOrderIndex(), mAdapter.mPosition,
                      mAdapter.mStackBounds, task.getWindowConfiguration(), false /*isNotInRecents*/,
                      mThumbnailAdapter != null ? mThumbnailAdapter.mCapturedLeash : null,
                      mStartBounds);
              return mTarget;
          }
  
          private int getMode() {
              final DisplayContent dc = mAppWindowToken.getDisplayContent();
              if (dc.mOpeningApps.contains(mAppWindowToken)) {
                  return RemoteAnimationTarget.MODE_OPENING;
              } else if (dc.mChangingApps.contains(mAppWindowToken)) {
                  return RemoteAnimationTarget.MODE_CHANGING;
              } else {
                  return RemoteAnimationTarget.MODE_CLOSING;
              }
          }
      }
  
      private class RemoteAnimationAdapterWrapper implements AnimationAdapter {
          private final RemoteAnimationRecord mRecord;
          SurfaceControl mCapturedLeash;
          private OnAnimationFinishedCallback mCapturedFinishCallback;
          private final Point mPosition = new Point();
          private final Rect mStackBounds = new Rect();
  
          RemoteAnimationAdapterWrapper(RemoteAnimationRecord record, Point position,
                  Rect stackBounds) {
              mRecord = record;
              mPosition.set(position.x, position.y);
              mStackBounds.set(stackBounds);
          }
  
          @Override
          public boolean getShowWallpaper() {
              return false;
          }
  
          @Override
          public int getBackgroundColor() {
              return 0;
          }
  
          @Override
          public void startAnimation(SurfaceControl animationLeash, Transaction t,
                  OnAnimationFinishedCallback finishCallback) {
              if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "startAnimation");
  
              // Restore z-layering, position and stack crop until client has a chance to modify it.
              t.setLayer(animationLeash, mRecord.mAppWindowToken.getPrefixOrderIndex());
              t.setPosition(animationLeash, mPosition.x, mPosition.y);
              mTmpRect.set(mStackBounds);
              mTmpRect.offsetTo(0, 0);
              t.setWindowCrop(animationLeash, mTmpRect);
              mCapturedLeash = animationLeash;
              mCapturedFinishCallback = finishCallback;
          }
  
          @Override
          public void onAnimationCancelled(SurfaceControl animationLeash) {
              if (mRecord.mAdapter == this) {
                  mRecord.mAdapter = null;
              } else {
                  mRecord.mThumbnailAdapter = null;
              }
              if (mRecord.mAdapter == null && mRecord.mThumbnailAdapter == null) {
                  mPendingAnimations.remove(mRecord);
              }
              if (mPendingAnimations.isEmpty()) {
                  mHandler.removeCallbacks(mTimeoutRunnable);
                  releaseFinishedCallback();
                  invokeAnimationCancelled();
                  setRunningRemoteAnimation(false);
              }
          }
  
          @Override
          public long getDurationHint() {
              return mRemoteAnimationAdapter.getDuration();
          }
  
          @Override
          public long getStatusBarTransitionsStartTime() {
              return SystemClock.uptimeMillis()
                      + mRemoteAnimationAdapter.getStatusBarTransitionDelay();
          }
  
          @Override
          public void dump(PrintWriter pw, String prefix) {
              pw.print(prefix); pw.print("token="); pw.println(mRecord.mAppWindowToken);
              if (mRecord.mTarget != null) {
                  pw.print(prefix); pw.println("Target:");
                  mRecord.mTarget.dump(pw, prefix + "  ");
              } else {
                  pw.print(prefix); pw.println("Target: null");
              }
          }
  
          @Override
          public void writeToProto(ProtoOutputStream proto) {
              final long token = proto.start(REMOTE);
              if (mRecord.mTarget != null) {
                  mRecord.mTarget.writeToProto(proto, TARGET);
              }
              proto.end(token);
          }
      }
  }
  