blob: 451afa08040cc70e69eddeec5cf00dfb14d7ed69 [file] [log] [blame]
/*
* 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<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = new ArrayMap();
private OneHandedAnimationController mAnimationController;
private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
private OneHandedTutorialHandler mTutorialHandler;
private List<OneHandedTransitionCallback> 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<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) {
final List<DisplayAreaAppearedInfo> 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<WindowContainerToken, SurfaceControl> 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<WindowContainerToken, SurfaceControl> 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);
}
}
}