/* * Copyright (C) 2020 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.wm.shell.onehanded; import static com.android.internal.jank.InteractionJankMonitor.CUJ_ONE_HANDED_ENTER_TRANSITION; import static com.android.internal.jank.InteractionJankMonitor.CUJ_ONE_HANDED_EXIT_TRANSITION; import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT; import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER; import android.content.Context; import android.graphics.Rect; import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArrayMap; import android.view.SurfaceControl; import android.window.DisplayAreaAppearedInfo; import android.window.DisplayAreaInfo; import android.window.DisplayAreaOrganizer; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Manages OneHanded display areas such as offset. * * This class listens on {@link DisplayAreaOrganizer} callbacks for windowing mode change * both to and from OneHanded and issues corresponding animation if applicable. * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running * and files a final {@link WindowContainerTransaction} at the end of the transition. * * This class is also responsible for translating one handed operations within SysUI component */ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { private static final String TAG = "OneHandedDisplayAreaOrganizer"; private static final String ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION = "persist.debug.one_handed_translate_animation_duration"; private DisplayLayout mDisplayLayout = new DisplayLayout(); private final Rect mLastVisualDisplayBounds = new Rect(); private final Rect mDefaultDisplayBounds = new Rect(); private final OneHandedSettingsUtil mOneHandedSettingsUtil; private final InteractionJankMonitor mJankMonitor; private final Context mContext; private boolean mIsReady; private float mLastVisualOffset = 0; private int mEnterExitAnimationDurationMs; private ArrayMap mDisplayAreaTokenMap = new ArrayMap(); private OneHandedAnimationController mAnimationController; private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; private OneHandedTutorialHandler mTutorialHandler; private List mTransitionCallbacks = new ArrayList<>(); @VisibleForTesting OneHandedAnimationCallback mOneHandedAnimationCallback = new OneHandedAnimationCallback() { @Override public void onOneHandedAnimationStart( OneHandedAnimationController.OneHandedTransitionAnimator animator) { final boolean isEntering = animator.getTransitionDirection() == TRANSITION_DIRECTION_TRIGGER; if (!mTransitionCallbacks.isEmpty()) { for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) { final OneHandedTransitionCallback cb = mTransitionCallbacks.get(i); cb.onStartTransition(isEntering); } } } @Override public void onOneHandedAnimationEnd(SurfaceControl.Transaction tx, OneHandedAnimationController.OneHandedTransitionAnimator animator) { mAnimationController.removeAnimator(animator.getToken()); final boolean isEntering = animator.getTransitionDirection() == TRANSITION_DIRECTION_TRIGGER; if (mAnimationController.isAnimatorsConsumed()) { endCUJTracing(isEntering ? CUJ_ONE_HANDED_ENTER_TRANSITION : CUJ_ONE_HANDED_EXIT_TRANSITION); finishOffset((int) animator.getDestinationOffset(), animator.getTransitionDirection()); } } @Override public void onOneHandedAnimationCancel( OneHandedAnimationController.OneHandedTransitionAnimator animator) { mAnimationController.removeAnimator(animator.getToken()); final boolean isEntering = animator.getTransitionDirection() == TRANSITION_DIRECTION_TRIGGER; if (mAnimationController.isAnimatorsConsumed()) { cancelCUJTracing(isEntering ? CUJ_ONE_HANDED_ENTER_TRANSITION : CUJ_ONE_HANDED_EXIT_TRANSITION); finishOffset((int) animator.getDestinationOffset(), animator.getTransitionDirection()); } } }; /** * Constructor of OneHandedDisplayAreaOrganizer */ public OneHandedDisplayAreaOrganizer(Context context, DisplayLayout displayLayout, OneHandedSettingsUtil oneHandedSettingsUtil, OneHandedAnimationController animationController, OneHandedTutorialHandler tutorialHandler, InteractionJankMonitor jankMonitor, ShellExecutor mainExecutor) { super(mainExecutor); mContext = context; setDisplayLayout(displayLayout); mOneHandedSettingsUtil = oneHandedSettingsUtil; mAnimationController = animationController; mJankMonitor = jankMonitor; final int animationDurationConfig = context.getResources().getInteger( R.integer.config_one_handed_translate_animation_duration); mEnterExitAnimationDurationMs = SystemProperties.getInt(ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION, animationDurationConfig); mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; mTutorialHandler = tutorialHandler; } @Override public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, @NonNull SurfaceControl leash) { mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); } @Override public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); if (leash != null) { leash.release(); } mDisplayAreaTokenMap.remove(displayAreaInfo.token); } @Override public List registerOrganizer(int displayAreaFeature) { final List displayAreaInfos = super.registerOrganizer(displayAreaFeature); for (int i = 0; i < displayAreaInfos.size(); i++) { final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); } mIsReady = true; updateDisplayBounds(); return displayAreaInfos; } @Override public void unregisterOrganizer() { super.unregisterOrganizer(); mIsReady = false; resetWindowsOffset(); } boolean isReady() { return mIsReady; } /** * Handler for display rotation changes by {@link DisplayLayout} * * @param context Any context * @param toRotation target rotation of the display (after rotating). * @param wct A task transaction {@link WindowContainerTransaction} from * {@link DisplayChangeController} to populate. */ public void onRotateDisplay(Context context, int toRotation, WindowContainerTransaction wct) { if (mDisplayLayout.rotation() == toRotation) { return; } mDisplayLayout.rotateTo(context.getResources(), toRotation); updateDisplayBounds(); finishOffset(0, TRANSITION_DIRECTION_EXIT); } /** * Offset the windows by a given offset on Y-axis, triggered also from screen rotation. * Directly perform manipulation/offset on the leash. */ public void scheduleOffset(int xOffset, int yOffset) { final float fromPos = mLastVisualOffset; final int direction = yOffset > 0 ? TRANSITION_DIRECTION_TRIGGER : TRANSITION_DIRECTION_EXIT; if (direction == TRANSITION_DIRECTION_TRIGGER) { beginCUJTracing(CUJ_ONE_HANDED_ENTER_TRANSITION, "enterOneHanded"); } else { beginCUJTracing(CUJ_ONE_HANDED_EXIT_TRANSITION, "stopOneHanded"); } mDisplayAreaTokenMap.forEach( (token, leash) -> { animateWindows(token, leash, fromPos, yOffset, direction, mEnterExitAnimationDurationMs); }); mLastVisualOffset = yOffset; } @VisibleForTesting void resetWindowsOffset() { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mDisplayAreaTokenMap.forEach( (token, leash) -> { final OneHandedAnimationController.OneHandedTransitionAnimator animator = mAnimationController.getAnimatorMap().remove(token); if (animator != null && animator.isRunning()) { animator.cancel(); } tx.setPosition(leash, 0, 0) .setWindowCrop(leash, -1, -1) .setCornerRadius(leash, -1); }); tx.apply(); mLastVisualOffset = 0; mLastVisualDisplayBounds.offsetTo(0, 0); } private void animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos, float toPos, @OneHandedAnimationController.TransitionDirection int direction, int durationMs) { final OneHandedAnimationController.OneHandedTransitionAnimator animator = mAnimationController.getAnimator(token, leash, fromPos, toPos, mLastVisualDisplayBounds); if (animator != null) { animator.setTransitionDirection(direction) .addOneHandedAnimationCallback(mOneHandedAnimationCallback) .addOneHandedAnimationCallback(mTutorialHandler) .setDuration(durationMs) .start(); } } @VisibleForTesting void finishOffset(int offset, @OneHandedAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_EXIT) { // We must do this to ensure reset property for leash when exit one handed mode resetWindowsOffset(); } mLastVisualOffset = direction == TRANSITION_DIRECTION_TRIGGER ? offset : 0; mLastVisualDisplayBounds.offsetTo(0, Math.round(mLastVisualOffset)); for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) { final OneHandedTransitionCallback cb = mTransitionCallbacks.get(i); if (direction == TRANSITION_DIRECTION_TRIGGER) { cb.onStartFinished(getLastVisualDisplayBounds()); } else { cb.onStopFinished(getLastVisualDisplayBounds()); } } } /** * The latest visual bounds of displayArea translated * * @return Rect latest finish_offset */ private Rect getLastVisualDisplayBounds() { return mLastVisualDisplayBounds; } @VisibleForTesting @Nullable Rect getLastDisplayBounds() { return mLastVisualDisplayBounds; } public DisplayLayout getDisplayLayout() { return mDisplayLayout; } @VisibleForTesting void setDisplayLayout(@NonNull DisplayLayout displayLayout) { mDisplayLayout.set(displayLayout); updateDisplayBounds(); } @VisibleForTesting ArrayMap getDisplayAreaTokenMap() { return mDisplayAreaTokenMap; } @VisibleForTesting void updateDisplayBounds() { mDefaultDisplayBounds.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); mLastVisualDisplayBounds.set(mDefaultDisplayBounds); } /** * Register transition callback */ public void registerTransitionCallback(OneHandedTransitionCallback callback) { mTransitionCallbacks.add(callback); } void beginCUJTracing(@InteractionJankMonitor.CujType int cujType, @Nullable String tag) { final Map.Entry firstEntry = getDisplayAreaTokenMap().entrySet().iterator().next(); final InteractionJankMonitor.Configuration.Builder builder = InteractionJankMonitor.Configuration.Builder.withSurface( cujType, mContext, firstEntry.getValue()); if (!TextUtils.isEmpty(tag)) { builder.setTag(tag); } mJankMonitor.begin(builder); } void endCUJTracing(@InteractionJankMonitor.CujType int cujType) { mJankMonitor.end(cujType); } void cancelCUJTracing(@InteractionJankMonitor.CujType int cujType) { mJankMonitor.cancel(cujType); } void dump(@NonNull PrintWriter pw) { final String innerPrefix = " "; pw.println(TAG); pw.print(innerPrefix + "mDisplayLayout.rotation()="); pw.println(mDisplayLayout.rotation()); pw.print(innerPrefix + "mDisplayAreaTokenMap="); pw.println(mDisplayAreaTokenMap); pw.print(innerPrefix + "mDefaultDisplayBounds="); pw.println(mDefaultDisplayBounds); pw.print(innerPrefix + "mIsReady="); pw.println(mIsReady); pw.print(innerPrefix + "mLastVisualDisplayBounds="); pw.println(mLastVisualDisplayBounds); pw.print(innerPrefix + "mLastVisualOffset="); pw.println(mLastVisualOffset); if (mAnimationController != null) { mAnimationController.dump(pw); } } }