content: /*
   * Copyright (C) 2016 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.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
  import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
  import static android.util.TypedValue.COMPLEX_UNIT_DIP;
  
  import static com.android.server.wm.PinnedStackControllerProto.DEFAULT_BOUNDS;
  import static com.android.server.wm.PinnedStackControllerProto.MOVEMENT_BOUNDS;
  import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
  import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
  
  import android.annotation.NonNull;
  import android.app.RemoteAction;
  import android.content.pm.ParceledListSlice;
  import android.content.res.Resources;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.util.DisplayMetrics;
  import android.util.Log;
  import android.util.Size;
  import android.util.Slog;
  import android.util.TypedValue;
  import android.util.proto.ProtoOutputStream;
  import android.view.DisplayInfo;
  import android.view.Gravity;
  import android.view.IPinnedStackController;
  import android.view.IPinnedStackListener;
  
  import com.android.internal.policy.PipSnapAlgorithm;
  import com.android.internal.util.Preconditions;
  import com.android.server.UiThread;
  
  import java.io.PrintWriter;
  import java.lang.ref.WeakReference;
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * Holds the common state of the pinned stack between the system and SystemUI. If SystemUI ever
   * needs to be restarted, it will be notified with the last known state.
   *
   * Changes to the pinned stack also flow through this controller, and generally, the system only
   * changes the pinned stack bounds through this controller in two ways:
   *
   * 1) When first entering PiP: the controller returns the valid bounds given, taking aspect ratio
   *    and IME state into account.
   * 2) When rotating the device: the controller calculates the new bounds in the new orientation,
   *    taking the minimized and IME state into account. In this case, we currently ignore the
   *    SystemUI adjustments (ie. expanded for menu, interaction, etc).
   *
   * Other changes in the system, including adjustment of IME, configuration change, and more are
   * handled by SystemUI (similar to the docked stack divider).
   */
  class PinnedStackController {
  
      private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM;
  
      public static final float INVALID_SNAP_FRACTION = -1f;
      private final WindowManagerService mService;
      private final DisplayContent mDisplayContent;
      private final Handler mHandler = UiThread.getHandler();
  
      private IPinnedStackListener mPinnedStackListener;
      private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler =
              new PinnedStackListenerDeathHandler();
  
      private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback();
      private final PipSnapAlgorithm mSnapAlgorithm;
  
      // States that affect how the PIP can be manipulated
      private boolean mIsMinimized;
      private boolean mIsImeShowing;
      private int mImeHeight;
      private boolean mIsShelfShowing;
      private int mShelfHeight;
  
      // The set of actions and aspect-ratio for the that are currently allowed on the PiP activity
      private ArrayList<RemoteAction> mActions = new ArrayList<>();
      private float mAspectRatio = -1f;
  
      // Used to calculate stack bounds across rotations
      private final DisplayInfo mDisplayInfo = new DisplayInfo();
      private final Rect mStableInsets = new Rect();
  
      // The size and position information that describes where the pinned stack will go by default.
      private int mDefaultMinSize;
      private int mDefaultStackGravity;
      private float mDefaultAspectRatio;
      private Point mScreenEdgeInsets;
      private int mCurrentMinSize;
      private float mReentrySnapFraction = INVALID_SNAP_FRACTION;
      private WeakReference<AppWindowToken> mLastPipActivity = null;
  
      // The aspect ratio bounds of the PIP.
      private float mMinAspectRatio;
      private float mMaxAspectRatio;
  
      // Temp vars for calculation
      private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
      private final Rect mTmpInsets = new Rect();
      private final Rect mTmpRect = new Rect();
      private final Rect mTmpAnimatingBoundsRect = new Rect();
      private final Point mTmpDisplaySize = new Point();
  
  
      /**
       * The callback object passed to listeners for them to notify the controller of state changes.
       */
      private class PinnedStackControllerCallback extends IPinnedStackController.Stub {
  
          @Override
          public void setIsMinimized(final boolean isMinimized) {
              mHandler.post(() -> {
                  mIsMinimized = isMinimized;
                  mSnapAlgorithm.setMinimized(isMinimized);
              });
          }
  
          @Override
          public void setMinEdgeSize(int minEdgeSize) {
              mHandler.post(() -> {
                  mCurrentMinSize = Math.max(mDefaultMinSize, minEdgeSize);
              });
          }
  
          @Override
          public int getDisplayRotation() {
              synchronized (mService.mGlobalLock) {
                  return mDisplayInfo.rotation;
              }
          }
      }
  
      /**
       * Handler for the case where the listener dies.
       */
      private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient {
  
          @Override
          public void binderDied() {
              // Clean up the state if the listener dies
              if (mPinnedStackListener != null) {
                  mPinnedStackListener.asBinder().unlinkToDeath(mPinnedStackListenerDeathHandler, 0);
              }
              mPinnedStackListener = null;
          }
      }
  
      PinnedStackController(WindowManagerService service, DisplayContent displayContent) {
          mService = service;
          mDisplayContent = displayContent;
          mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
          mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
          reloadResources();
          // Initialize the aspect ratio to the default aspect ratio.  Don't do this in reload
          // resources as it would clobber mAspectRatio when entering PiP from fullscreen which
          // triggers a configuration change and the resources to be reloaded.
          mAspectRatio = mDefaultAspectRatio;
      }
  
      void onConfigurationChanged() {
          reloadResources();
      }
  
      /**
       * Reloads all the resources for the current configuration.
       */
      private void reloadResources() {
          final Resources res = mService.mContext.getResources();
          mDefaultMinSize = res.getDimensionPixelSize(
                  com.android.internal.R.dimen.default_minimal_size_pip_resizable_task);
          mCurrentMinSize = mDefaultMinSize;
          mDefaultAspectRatio = res.getFloat(
                  com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio);
          final String screenEdgeInsetsDpString = res.getString(
                  com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets);
          final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
                  ? Size.parseSize(screenEdgeInsetsDpString)
                  : null;
          mDefaultStackGravity = res.getInteger(
                  com.android.internal.R.integer.config_defaultPictureInPictureGravity);
          mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
          mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
                  : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
                          dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
          mMinAspectRatio = res.getFloat(
                  com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
          mMaxAspectRatio = res.getFloat(
                  com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
      }
  
      /**
       * Registers a pinned stack listener.
       */
      void registerPinnedStackListener(IPinnedStackListener listener) {
          try {
              listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
              listener.onListenerRegistered(mCallbacks);
              mPinnedStackListener = listener;
              notifyImeVisibilityChanged(mIsImeShowing, mImeHeight);
              notifyShelfVisibilityChanged(mIsShelfShowing, mShelfHeight);
              // The movement bounds notification needs to be sent before the minimized state, since
              // SystemUI may use the bounds to retore the minimized position
              notifyMovementBoundsChanged(false /* fromImeAdjustment */,
                      false /* fromShelfAdjustment */);
              notifyActionsChanged(mActions);
              notifyMinimizeChanged(mIsMinimized);
          } catch (RemoteException e) {
              Log.e(TAG, "Failed to register pinned stack listener", e);
          }
      }
  
      /**
       * @return whether the given {@param aspectRatio} is valid.
       */
      public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
          return Float.compare(mMinAspectRatio, aspectRatio) <= 0 &&
                  Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
      }
  
      /**
       * Returns the current bounds (or the default bounds if there are no current bounds) with the
       * specified aspect ratio.
       */
      Rect transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
              boolean useCurrentMinEdgeSize) {
          // Save the snap fraction, calculate the aspect ratio based on screen size
          final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
                  getMovementBounds(stackBounds));
  
          final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize;
          final Size size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize,
                  mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
          final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
          final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
          stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
          mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
          if (mIsMinimized) {
              applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds));
          }
          return stackBounds;
      }
  
      /**
       * Saves the current snap fraction for re-entry of the current activity into PiP.
       */
      void saveReentrySnapFraction(final AppWindowToken token, final Rect stackBounds) {
          mReentrySnapFraction = getSnapFraction(stackBounds);
          mLastPipActivity = new WeakReference<>(token);
      }
  
      /**
       * Resets the last saved snap fraction so that the default bounds will be returned.
       */
      void resetReentrySnapFraction(AppWindowToken token) {
          if (mLastPipActivity != null && mLastPipActivity.get() == token) {
              mReentrySnapFraction = INVALID_SNAP_FRACTION;
              mLastPipActivity = null;
          }
      }
  
      /**
       * @return the default bounds to show the PIP when there is no active PIP.
       */
      Rect getDefaultOrLastSavedBounds() {
          return getDefaultBounds(mReentrySnapFraction);
      }
  
      /**
       * @return the default bounds to show the PIP, if a {@param snapFraction} is provided, then it
       * will apply the default bounds to the provided snap fraction.
       */
      Rect getDefaultBounds(float snapFraction) {
          synchronized (mService.mGlobalLock) {
              final Rect insetBounds = new Rect();
              getInsetBounds(insetBounds);
  
              final Rect defaultBounds = new Rect();
              final Size size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio,
                      mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
              if (snapFraction != INVALID_SNAP_FRACTION) {
                  defaultBounds.set(0, 0, size.getWidth(), size.getHeight());
                  final Rect movementBounds = getMovementBounds(defaultBounds);
                  mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
              } else {
                  Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds,
                          0, Math.max(mIsImeShowing ? mImeHeight : 0,
                                  mIsShelfShowing ? mShelfHeight : 0),
                          defaultBounds);
              }
              return defaultBounds;
          }
      }
  
      /**
       * In the case where the display rotation is changed but there is no stack, we can't depend on
       * onTaskStackBoundsChanged() to be called.  But we still should update our known display info
       * with the new state so that we can update SystemUI.
       */
      synchronized void onDisplayInfoChanged(DisplayInfo displayInfo) {
          mDisplayInfo.copyFrom(displayInfo);
          notifyMovementBoundsChanged(false /* fromImeAdjustment */, false /* fromShelfAdjustment */);
      }
  
      /**
       * Updates the display info, calculating and returning the new stack and movement bounds in the
       * new orientation of the device if necessary.
       */
      boolean onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds) {
          synchronized (mService.mGlobalLock) {
              final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo();
              if (isSameDimensionAndRotation(mDisplayInfo, displayInfo)) {
                  // No dimension/rotation change, ignore
                  outBounds.setEmpty();
                  return false;
              } else if (targetBounds.isEmpty()) {
                  // The stack is null, we are just initializing the stack, so just store the display
                  // info and ignore
                  mDisplayInfo.copyFrom(displayInfo);
                  outBounds.setEmpty();
                  return false;
              }
  
              mTmpRect.set(targetBounds);
              final Rect postChangeStackBounds = mTmpRect;
  
              // Calculate the snap fraction of the current stack along the old movement bounds
              final float snapFraction = getSnapFraction(postChangeStackBounds);
              mDisplayInfo.copyFrom(displayInfo);
  
              // Calculate the stack bounds in the new orientation to the same same fraction along the
              // rotated movement bounds.
              final Rect postChangeMovementBounds = getMovementBounds(postChangeStackBounds,
                      false /* adjustForIme */, false /* adjustForShelf */);
              mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
                      snapFraction);
              if (mIsMinimized) {
                  applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds);
              }
  
              notifyMovementBoundsChanged(false /* fromImeAdjustment */,
                      false /* fromShelfAdjustment */);
  
              outBounds.set(postChangeStackBounds);
              return true;
          }
      }
  
      /**
       * Sets the Ime state and height.
       */
      void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
          // Due to the order of callbacks from the system, we may receive an ime height even when
          // {@param adjustedForIme} is false, and also a zero height when {@param adjustedForIme}
          // is true.  Instead, ensure that the ime state changes with the height and if the ime is
          // showing, then the height is non-zero.
          final boolean imeShowing = adjustedForIme && imeHeight > 0;
          imeHeight = imeShowing ? imeHeight : 0;
          if (imeShowing == mIsImeShowing && imeHeight == mImeHeight) {
              return;
          }
  
          mIsImeShowing = imeShowing;
          mImeHeight = imeHeight;
          notifyImeVisibilityChanged(imeShowing, imeHeight);
          notifyMovementBoundsChanged(true /* fromImeAdjustment */, false /* fromShelfAdjustment */);
      }
  
      /**
       * Sets the shelf state and height.
       */
      void setAdjustedForShelf(boolean adjustedForShelf, int shelfHeight) {
          final boolean shelfShowing = adjustedForShelf && shelfHeight > 0;
          if (shelfShowing == mIsShelfShowing && shelfHeight == mShelfHeight) {
              return;
          }
  
          mIsShelfShowing = shelfShowing;
          mShelfHeight = shelfHeight;
          notifyShelfVisibilityChanged(shelfShowing, shelfHeight);
          notifyMovementBoundsChanged(false /* fromImeAdjustment */, true /* fromShelfAdjustment */);
      }
  
      /**
       * Sets the current aspect ratio.
       */
      void setAspectRatio(float aspectRatio) {
          if (Float.compare(mAspectRatio, aspectRatio) != 0) {
              mAspectRatio = aspectRatio;
              notifyMovementBoundsChanged(false /* fromImeAdjustment */,
                      false /* fromShelfAdjustment */);
          }
      }
  
      /**
       * @return the current aspect ratio.
       */
      float getAspectRatio() {
          return mAspectRatio;
      }
  
      /**
       * Sets the current set of actions.
       */
      void setActions(List<RemoteAction> actions) {
          mActions.clear();
          if (actions != null) {
              mActions.addAll(actions);
          }
          notifyActionsChanged(mActions);
      }
  
      private boolean isSameDimensionAndRotation(@NonNull DisplayInfo display1,
              @NonNull DisplayInfo display2) {
          Preconditions.checkNotNull(display1);
          Preconditions.checkNotNull(display2);
          return ((display1.rotation == display2.rotation)
                  && (display1.logicalWidth == display2.logicalWidth)
                  && (display1.logicalHeight == display2.logicalHeight));
      }
  
      /**
       * Notifies listeners that the PIP needs to be adjusted for the IME.
       */
      private void notifyImeVisibilityChanged(boolean imeVisible, int imeHeight) {
          if (mPinnedStackListener != null) {
              try {
                  mPinnedStackListener.onImeVisibilityChanged(imeVisible, imeHeight);
              } catch (RemoteException e) {
                  Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
              }
          }
      }
  
      private void notifyShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
          if (mPinnedStackListener != null) {
              try {
                  mPinnedStackListener.onShelfVisibilityChanged(shelfVisible, shelfHeight);
              } catch (RemoteException e) {
                  Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
              }
          }
      }
  
      /**
       * Notifies listeners that the PIP minimized state has changed.
       */
      private void notifyMinimizeChanged(boolean isMinimized) {
          if (mPinnedStackListener != null) {
              try {
                  mPinnedStackListener.onMinimizedStateChanged(isMinimized);
              } catch (RemoteException e) {
                  Slog.e(TAG_WM, "Error delivering minimize changed event.", e);
              }
          }
      }
  
      /**
       * Notifies listeners that the PIP actions have changed.
       */
      private void notifyActionsChanged(List<RemoteAction> actions) {
          if (mPinnedStackListener != null) {
              try {
                  mPinnedStackListener.onActionsChanged(new ParceledListSlice(actions));
              } catch (RemoteException e) {
                  Slog.e(TAG_WM, "Error delivering actions changed event.", e);
              }
          }
      }
  
      /**
       * Notifies listeners that the PIP movement bounds have changed.
       */
      private void notifyMovementBoundsChanged(boolean fromImeAdjustment,
              boolean fromShelfAdjustment) {
          synchronized (mService.mGlobalLock) {
              if (mPinnedStackListener == null) {
                  return;
              }
              try {
                  final Rect insetBounds = new Rect();
                  getInsetBounds(insetBounds);
                  final Rect normalBounds = getDefaultBounds(INVALID_SNAP_FRACTION);
                  if (isValidPictureInPictureAspectRatio(mAspectRatio)) {
                      transformBoundsToAspectRatio(normalBounds, mAspectRatio,
                              false /* useCurrentMinEdgeSize */);
                  }
                  final Rect animatingBounds = mTmpAnimatingBoundsRect;
                  final TaskStack pinnedStack = mDisplayContent.getPinnedStack();
                  if (pinnedStack != null) {
                      pinnedStack.getAnimationOrCurrentBounds(animatingBounds);
                  } else {
                      animatingBounds.set(normalBounds);
                  }
                  mPinnedStackListener.onMovementBoundsChanged(insetBounds, normalBounds,
                          animatingBounds, fromImeAdjustment, fromShelfAdjustment,
                          mDisplayInfo.rotation);
              } catch (RemoteException e) {
                  Slog.e(TAG_WM, "Error delivering actions changed event.", e);
              }
          }
      }
  
      /**
       * @return the bounds on the screen that the PIP can be visible in.
       */
      private void getInsetBounds(Rect outRect) {
          synchronized (mService.mGlobalLock) {
              mDisplayContent.getDisplayPolicy().getStableInsetsLw(mDisplayInfo.rotation,
                      mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight,
                      mDisplayInfo.displayCutout, mTmpInsets);
              outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y,
                      mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x,
                      mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y);
          }
      }
  
      /**
       * @return the movement bounds for the given {@param stackBounds} and the current state of the
       *         controller.
       */
      private Rect getMovementBounds(Rect stackBounds) {
          synchronized (mService.mGlobalLock) {
              return getMovementBounds(stackBounds, true /* adjustForIme */,
                      true /* adjustForShelf */);
          }
      }
  
      /**
       * @return the movement bounds for the given {@param stackBounds} and the current state of the
       *         controller.
       */
      private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme, boolean adjustForShelf) {
          synchronized (mService.mGlobalLock) {
              final Rect movementBounds = new Rect();
              getInsetBounds(movementBounds);
  
              // Apply the movement bounds adjustments based on the current state
              mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds,
                      Math.max((adjustForIme && mIsImeShowing) ? mImeHeight : 0,
                              (adjustForShelf && mIsShelfShowing) ? mShelfHeight : 0));
              return movementBounds;
          }
      }
  
      /**
       * Applies the minimized offsets to the given stack bounds.
       */
      private void applyMinimizedOffset(Rect stackBounds, Rect movementBounds) {
          synchronized (mService.mGlobalLock) {
              mTmpDisplaySize.set(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
              mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mStableInsets);
              mSnapAlgorithm.applyMinimizedOffset(stackBounds, movementBounds, mTmpDisplaySize,
                      mStableInsets);
          }
      }
  
      /**
       * @return the default snap fraction to apply instead of the default gravity when calculating
       *         the default stack bounds when first entering PiP.
       */
      private float getSnapFraction(Rect stackBounds) {
          return mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds));
      }
  
      /**
       * @return the pixels for a given dp value.
       */
      private int dpToPx(float dpValue, DisplayMetrics dm) {
          return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
      }
  
      void dump(String prefix, PrintWriter pw) {
          pw.println(prefix + "PinnedStackController");
          pw.print(prefix + "  defaultBounds=");
          getDefaultBounds(INVALID_SNAP_FRACTION).printShortString(pw);
          pw.println();
          pw.println(prefix + "  mDefaultMinSize=" + mDefaultMinSize);
          pw.println(prefix + "  mDefaultStackGravity=" + mDefaultStackGravity);
          pw.println(prefix + "  mDefaultAspectRatio=" + mDefaultAspectRatio);
          mService.getStackBounds(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mTmpRect);
          pw.print(prefix + "  movementBounds="); getMovementBounds(mTmpRect).printShortString(pw);
          pw.println();
          pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
          pw.println(prefix + "  mImeHeight=" + mImeHeight);
          pw.println(prefix + "  mIsShelfShowing=" + mIsShelfShowing);
          pw.println(prefix + "  mShelfHeight=" + mShelfHeight);
          pw.println(prefix + "  mReentrySnapFraction=" + mReentrySnapFraction);
          pw.println(prefix + "  mIsMinimized=" + mIsMinimized);
          pw.println(prefix + "  mAspectRatio=" + mAspectRatio);
          pw.println(prefix + "  mMinAspectRatio=" + mMinAspectRatio);
          pw.println(prefix + "  mMaxAspectRatio=" + mMaxAspectRatio);
          if (mActions.isEmpty()) {
              pw.println(prefix + "  mActions=[]");
          } else {
              pw.println(prefix + "  mActions=[");
              for (int i = 0; i < mActions.size(); i++) {
                  RemoteAction action = mActions.get(i);
                  pw.print(prefix + "    Action[" + i + "]: ");
                  action.dump("", pw);
              }
              pw.println(prefix + "  ]");
          }
          pw.println(prefix + "  mDisplayInfo=" + mDisplayInfo);
      }
  
      void writeToProto(ProtoOutputStream proto, long fieldId) {
          final long token = proto.start(fieldId);
          getDefaultBounds(INVALID_SNAP_FRACTION).writeToProto(proto, DEFAULT_BOUNDS);
          mService.getStackBounds(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mTmpRect);
          getMovementBounds(mTmpRect).writeToProto(proto, MOVEMENT_BOUNDS);
          proto.end(token);
      }
  }
  