diff options
Diffstat (limited to 'libs')
169 files changed, 6146 insertions, 2077 deletions
diff --git a/libs/WindowManager/OWNERS b/libs/WindowManager/OWNERS index 063d4594f2fa..2c61df96eb03 100644 --- a/libs/WindowManager/OWNERS +++ b/libs/WindowManager/OWNERS @@ -1,3 +1,3 @@ set noparent -include ../../services/core/java/com/android/server/wm/OWNERS
\ No newline at end of file +include /services/core/java/com/android/server/wm/OWNERS diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 39e32c694d2e..96e0559b0df6 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -122,12 +122,14 @@ android_library { "kotlinx-coroutines-android", "kotlinx-coroutines-core", "iconloader_base", + "jsr330", "protolog-lib", "SettingsLib", "WindowManager-Shell-proto", + "jsr330" ], kotlincflags: ["-Xjvm-default=enable"], manifest: "AndroidManifest.xml", min_sdk_version: "26", -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml index b86f36a14d17..341fe617b2d0 100644 --- a/libs/WindowManager/Shell/res/layout/split_divider.xml +++ b/libs/WindowManager/Shell/res/layout/split_divider.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.wm.shell.apppairs.DividerView +<com.android.wm.shell.common.split.DividerView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="match_parent" android:layout_width="match_parent"> @@ -24,4 +24,4 @@ android:id="@+id/docked_divider_background" android:background="@color/docked_divider_background"/> -</com.android.wm.shell.apppairs.DividerView> +</com.android.wm.shell.common.split.DividerView> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java index ada7e1a9e3ab..45948dd9e800 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java @@ -19,6 +19,7 @@ package com.android.wm.shell; import android.view.Gravity; import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.letterbox.LetterboxConfigController; import com.android.wm.shell.onehanded.OneHanded; @@ -61,6 +62,7 @@ public final class ShellCommandHandler { } /** Dumps WM Shell internal state. */ + @ExternalThread public void dump(PrintWriter pw) { mShellTaskOrganizer.dump(pw, ""); pw.println(); @@ -76,6 +78,7 @@ public final class ShellCommandHandler { /** Returns {@code true} if command was found and executed. */ + @ExternalThread public boolean handleCommand(String[] args, PrintWriter pw) { if (args.length < 2) { // Argument at position 0 is "WMShell". diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java index 118f189866eb..6c08079e586e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java @@ -21,6 +21,7 @@ import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_LETTERB import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.letterbox.LetterboxTaskListener; import com.android.wm.shell.splitscreen.SplitScreen; @@ -39,6 +40,7 @@ public class ShellInit { private final Optional<AppPairs> mAppPairsOptional; private final LetterboxTaskListener mLetterboxTaskListener; private final FullscreenTaskListener mFullscreenTaskListener; + private final Transitions mTransitions; public ShellInit(DisplayImeController displayImeController, DragAndDropController dragAndDropController, @@ -46,7 +48,8 @@ public class ShellInit { Optional<SplitScreen> splitScreenOptional, Optional<AppPairs> appPairsOptional, LetterboxTaskListener letterboxTaskListener, - FullscreenTaskListener fullscreenTaskListener) { + FullscreenTaskListener fullscreenTaskListener, + Transitions transitions) { mDisplayImeController = displayImeController; mDragAndDropController = dragAndDropController; mShellTaskOrganizer = shellTaskOrganizer; @@ -54,8 +57,10 @@ public class ShellInit { mAppPairsOptional = appPairsOptional; mLetterboxTaskListener = letterboxTaskListener; mFullscreenTaskListener = fullscreenTaskListener; + mTransitions = transitions; } + @ExternalThread public void init() { // Start listening for display changes mDisplayImeController.startMonitorDisplays(); @@ -70,5 +75,9 @@ public class ShellInit { mAppPairsOptional.ifPresent(AppPairs::onOrganizerRegistered); // Bind the splitscreen impl to the drag drop controller mDragAndDropController.setSplitScreenController(mSplitScreenOptional); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.register(mShellTaskOrganizer); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index beb9690b5cbc..62d265aab38f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -43,8 +43,6 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; import java.io.PrintWriter; @@ -102,24 +100,17 @@ public class ShellTaskOrganizer extends TaskOrganizer { /** @see #setPendingLaunchCookieListener */ private final ArrayMap<IBinder, TaskListener> mLaunchCookieToListener = new ArrayMap<>(); - // TODO(shell-transitions): move to a more "global" Shell location as this isn't only for Tasks - private final Transitions mTransitions; - private final Object mLock = new Object(); private final StartingSurfaceDrawer mStartingSurfaceDrawer; - public ShellTaskOrganizer(SyncTransactionQueue syncQueue, TransactionPool transactionPool, - ShellExecutor mainExecutor, ShellExecutor animExecutor, Context context) { - this(null, syncQueue, transactionPool, mainExecutor, animExecutor, context); + public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) { + this(null, mainExecutor, context); } @VisibleForTesting - ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, - SyncTransactionQueue syncQueue, TransactionPool transactionPool, - ShellExecutor mainExecutor, ShellExecutor animExecutor, Context context) { + ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor, + Context context) { super(taskOrganizerController, mainExecutor); - mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor); - if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions); // TODO(b/131727939) temporarily live here, the starting surface drawer should be controlled // by a controller, that class should be create while porting // ActivityRecord#addStartingWindow to WMShell. @@ -272,6 +263,12 @@ public class ShellTaskOrganizer extends TaskOrganizer { synchronized (mLock) { ProtoLog.v(WM_SHELL_TASK_ORG, "Task info changed taskId=%d", taskInfo.taskId); final TaskAppearedInfo data = mTasks.get(taskInfo.taskId); + if (data == null) { + // TODO(b/171749427): It means onTaskInfoChanged send before onTaskAppeared or + // after onTaskVanished, it should be fixed in controller side. + return; + } + final TaskListener oldListener = getTaskListener(data.getTaskInfo()); final TaskListener newListener = getTaskListener(taskInfo); mTasks.put(taskInfo.taskId, new TaskAppearedInfo(taskInfo, data.getLeash())); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java index 388eb28223dc..a779531f8b91 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java @@ -16,16 +16,16 @@ package com.android.wm.shell; -import static android.window.TransitionInfo.TRANSIT_CLOSE; -import static android.window.TransitionInfo.TRANSIT_HIDE; -import static android.window.TransitionInfo.TRANSIT_OPEN; -import static android.window.TransitionInfo.TRANSIT_SHOW; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import android.animation.Animator; import android.animation.ValueAnimator; -import android.annotation.MainThread; import android.annotation.NonNull; import android.os.IBinder; +import android.os.RemoteException; import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Slog; @@ -35,6 +35,8 @@ import android.window.ITransitionPlayer; import android.window.TransitionInfo; import android.window.WindowOrganizer; +import androidx.annotation.BinderThread; + import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; @@ -43,7 +45,7 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; /** Plays transition animations */ -public class Transitions extends ITransitionPlayer.Stub { +public class Transitions { private static final String TAG = "ShellTransitions"; /** Set to {@code true} to enable shell transitions. */ @@ -54,16 +56,22 @@ public class Transitions extends ITransitionPlayer.Stub { private final TransactionPool mTransactionPool; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; + private final TransitionPlayerImpl mPlayerImpl; /** Keeps track of currently tracked transitions and all the animations associated with each */ private final ArrayMap<IBinder, ArrayList<Animator>> mActiveTransitions = new ArrayMap<>(); - Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, + public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; mTransactionPool = pool; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mPlayerImpl = new TransitionPlayerImpl(); + } + + public void register(ShellTaskOrganizer taskOrganizer) { + taskOrganizer.registerTransitionPlayer(mPlayerImpl); } // TODO(shell-transitions): real animations @@ -110,51 +118,78 @@ public class Transitions extends ITransitionPlayer.Stub { } private static boolean isOpeningType(@WindowManager.TransitionType int type) { - return type == WindowManager.TRANSIT_OPEN - || type == WindowManager.TRANSIT_TO_FRONT + return type == TRANSIT_OPEN + || type == TRANSIT_TO_FRONT || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; } - @Override - public void onTransitionReady(@NonNull IBinder transitionToken, TransitionInfo info, + private void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", transitionToken, info); // start task - mMainExecutor.execute(() -> { - if (!mActiveTransitions.containsKey(transitionToken)) { - Slog.e(TAG, "Got transitionReady for non-active transition " + transitionToken - + " expecting one of " + mActiveTransitions.keySet()); - } - if (mActiveTransitions.get(transitionToken) != null) { - throw new IllegalStateException("Got a duplicate onTransitionReady call for " - + transitionToken); - } - mActiveTransitions.put(transitionToken, new ArrayList<>()); - for (int i = 0; i < info.getChanges().size(); ++i) { - final SurfaceControl leash = info.getChanges().get(i).getLeash(); - final int mode = info.getChanges().get(i).getMode(); - if (mode == TRANSIT_OPEN || mode == TRANSIT_SHOW) { + if (!mActiveTransitions.containsKey(transitionToken)) { + Slog.e(TAG, "Got transitionReady for non-active transition " + transitionToken + + " expecting one of " + mActiveTransitions.keySet()); + } + if (mActiveTransitions.get(transitionToken) != null) { + throw new IllegalStateException("Got a duplicate onTransitionReady call for " + + transitionToken); + } + mActiveTransitions.put(transitionToken, new ArrayList<>()); + boolean isOpening = isOpeningType(info.getType()); + if (info.getRootLeash().isValid()) { + t.show(info.getRootLeash()); + } + // changes should be ordered top-to-bottom in z + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = change.getLeash(); + final int mode = info.getChanges().get(i).getMode(); + + // Don't animate anything with an animating parent + if (change.getParent() != null) { + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { t.show(leash); t.setMatrix(leash, 1, 0, 0, 1); - if (isOpeningType(info.getType())) { - t.setAlpha(leash, 0.f); - startExampleAnimation(transitionToken, leash, true /* show */); - } else { - t.setAlpha(leash, 1.f); - } - } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_HIDE) { - if (!isOpeningType(info.getType())) { - startExampleAnimation(transitionToken, leash, false /* show */); - } } + continue; } - t.apply(); - onFinish(transitionToken); - }); + + t.reparent(leash, info.getRootLeash()); + t.setPosition(leash, change.getEndAbsBounds().left - info.getRootOffset().x, + change.getEndAbsBounds().top - info.getRootOffset().y); + // Put all the OPEN/SHOW on top + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { + t.show(leash); + t.setMatrix(leash, 1, 0, 0, 1); + if (isOpening) { + // put on top and fade in + t.setLayer(leash, info.getChanges().size() - i); + t.setAlpha(leash, 0.f); + startExampleAnimation(transitionToken, leash, true /* show */); + } else { + // put on bottom and leave it visible without fade + t.setLayer(leash, -i); + t.setAlpha(leash, 1.f); + } + } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { + if (isOpening) { + // put on bottom and leave visible without fade + t.setLayer(leash, -i); + } else { + // put on top and fade out + t.setLayer(leash, info.getChanges().size() - i); + startExampleAnimation(transitionToken, leash, false /* show */); + } + } else { + t.setLayer(leash, info.getChanges().size() - i); + } + } + t.apply(); + onFinish(transitionToken); } - @MainThread private void onFinish(IBinder transition) { if (!mActiveTransitions.get(transition).isEmpty()) return; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, @@ -163,16 +198,32 @@ public class Transitions extends ITransitionPlayer.Stub { mOrganizer.finishTransition(transition, null, null); } - @Override - public void requestStartTransition(int type, @NonNull IBinder transitionToken) { + private void requestStartTransition(int type, @NonNull IBinder transitionToken) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: type=%d %s", type, transitionToken); - mMainExecutor.execute(() -> { - if (mActiveTransitions.containsKey(transitionToken)) { - throw new RuntimeException("Transition already started " + transitionToken); - } - IBinder transition = mOrganizer.startTransition(type, transitionToken, null /* wct */); - mActiveTransitions.put(transition, null); - }); + + if (mActiveTransitions.containsKey(transitionToken)) { + throw new RuntimeException("Transition already started " + transitionToken); + } + IBinder transition = mOrganizer.startTransition(type, transitionToken, null /* wct */); + mActiveTransitions.put(transition, null); + } + + @BinderThread + private class TransitionPlayerImpl extends ITransitionPlayer.Stub { + @Override + public void onTransitionReady(IBinder iBinder, TransitionInfo transitionInfo, + SurfaceControl.Transaction transaction) throws RemoteException { + mMainExecutor.execute(() -> { + Transitions.this.onTransitionReady(iBinder, transitionInfo, transaction); + }); + } + + @Override + public void requestStartTransition(int i, IBinder iBinder) throws RemoteException { + mMainExecutor.execute(() -> { + Transitions.this.requestStartTransition(i, iBinder); + }); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java index acb9a5dae78c..834de3f15b1d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java @@ -18,11 +18,11 @@ package com.android.wm.shell; import static android.view.Display.DEFAULT_DISPLAY; -import android.app.WindowConfiguration; import android.os.RemoteException; -import android.view.WindowManagerGlobal; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.pip.PinnedStackListenerForwarder.PinnedStackListener; /** * The singleton wrapper to communicate between WindowManagerService and WMShell features @@ -31,32 +31,30 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder; public class WindowManagerShellWrapper { private static final String TAG = WindowManagerShellWrapper.class.getSimpleName(); - public static final int WINDOWING_MODE_PINNED = WindowConfiguration.WINDOWING_MODE_PINNED; - /** * Forwarder to which we can add multiple pinned stack listeners. Each listener will receive * updates from the window manager service. */ - private PinnedStackListenerForwarder mPinnedStackListenerForwarder = - new PinnedStackListenerForwarder(); + private final PinnedStackListenerForwarder mPinnedStackListenerForwarder; + + public WindowManagerShellWrapper(ShellExecutor shellMainExecutor) { + mPinnedStackListenerForwarder = new PinnedStackListenerForwarder(shellMainExecutor); + } /** * Adds a pinned stack listener, which will receive updates from the window manager service * along with any other pinned stack listeners that were added via this method. */ - public void addPinnedStackListener(PinnedStackListenerForwarder.PinnedStackListener listener) - throws - RemoteException { + public void addPinnedStackListener(PinnedStackListener listener) + throws RemoteException { mPinnedStackListenerForwarder.addListener(listener); - WindowManagerGlobal.getWindowManagerService().registerPinnedStackListener( - DEFAULT_DISPLAY, mPinnedStackListenerForwarder); + mPinnedStackListenerForwarder.register(DEFAULT_DISPLAY); } /** * Removes a pinned stack listener. */ - public void removePinnedStackListener( - PinnedStackListenerForwarder.PinnedStackListener listener) { + public void removePinnedStackListener(PinnedStackListener listener) { mPinnedStackListenerForwarder.removeListener(listener); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java index 357f777e1270..176c620fa119 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java @@ -23,6 +23,8 @@ import android.view.ViewPropertyAnimator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import javax.inject.Inject; + /** * Utility class to calculate general fling animation when the finger is released. */ @@ -368,6 +370,7 @@ public class FlingAnimationUtils { float mX2; float mY2; + @Inject public Builder(DisplayMetrics displayMetrics) { mDisplayMetrics = displayMetrics; reset(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java index f199072f7bca..cfbf8452ddae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -29,11 +29,13 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.split.SplitLayout; import java.io.PrintWriter; @@ -42,7 +44,7 @@ import java.io.PrintWriter; * {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair. * Also includes all UI for managing the pair like the divider. */ -class AppPair implements ShellTaskOrganizer.TaskListener { +class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChangeListener { private static final String TAG = AppPair.class.getSimpleName(); private ActivityManager.RunningTaskInfo mRootTaskInfo; @@ -55,7 +57,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener { private final AppPairsController mController; private final SyncTransactionQueue mSyncQueue; private final DisplayController mDisplayController; - private AppPairLayout mAppPairLayout; + private SplitLayout mSplitLayout; AppPair(AppPairsController controller) { mController = controller; @@ -92,11 +94,9 @@ class AppPair implements ShellTaskOrganizer.TaskListener { mTaskInfo1 = task1; mTaskInfo2 = task2; - mAppPairLayout = new AppPairLayout( + mSplitLayout = new SplitLayout( mDisplayController.getDisplayContext(mRootTaskInfo.displayId), - mDisplayController.getDisplay(mRootTaskInfo.displayId), - mRootTaskInfo.configuration, - mRootTaskLeash); + mRootTaskInfo.configuration, this, mRootTaskLeash); final WindowContainerToken token1 = task1.token; final WindowContainerToken token2 = task2.token; @@ -107,8 +107,8 @@ class AppPair implements ShellTaskOrganizer.TaskListener { .reparent(token2, mRootTaskInfo.token, true /* onTop */) .setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW) .setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW) - .setBounds(token1, mAppPairLayout.getBounds1()) - .setBounds(token2, mAppPairLayout.getBounds2()) + .setBounds(token1, mSplitLayout.getBounds1()) + .setBounds(token2, mSplitLayout.getBounds2()) // Moving the root task to top after the child tasks were repareted , or the root // task cannot be visible and focused. .reorder(mRootTaskInfo.token, true); @@ -117,6 +117,10 @@ class AppPair implements ShellTaskOrganizer.TaskListener { } void unpair() { + unpair(null /* toTopToken */); + } + + private void unpair(@Nullable WindowContainerToken toTopToken) { final WindowContainerToken token1 = mTaskInfo1.token; final WindowContainerToken token2 = mTaskInfo2.token; final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -124,23 +128,16 @@ class AppPair implements ShellTaskOrganizer.TaskListener { // Reparent out of this container and reset windowing mode. wct.setHidden(mRootTaskInfo.token, true) .reorder(mRootTaskInfo.token, false) - .reparent(token1, null, false /* onTop */) - .reparent(token2, null, false /* onTop */) + .reparent(token1, null, token1 == toTopToken /* onTop */) + .reparent(token2, null, token2 == toTopToken /* onTop */) .setWindowingMode(token1, WINDOWING_MODE_UNDEFINED) .setWindowingMode(token2, WINDOWING_MODE_UNDEFINED); mController.getTaskOrganizer().applyTransaction(wct); mTaskInfo1 = null; mTaskInfo2 = null; - mAppPairLayout.release(); - mAppPairLayout = null; - } - - void setVisible(boolean visible) { - if (mAppPairLayout == null) { - return; - } - mAppPairLayout.setDividerVisibility(visible); + mSplitLayout.release(); + mSplitLayout = null; } @Override @@ -160,37 +157,46 @@ class AppPair implements ShellTaskOrganizer.TaskListener { if (mTaskLeash1 == null || mTaskLeash2 == null) return; - setVisible(true); - final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash(); - final Rect dividerBounds = mAppPairLayout.getDividerBounds(); + mSplitLayout.init(); + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + final Rect dividerBounds = mSplitLayout.getDividerBounds(); // TODO: Is there more we need to do here? - mSyncQueue.runInSync(t -> t - .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, - mTaskInfo1.positionInParent.y) - .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, - mTaskInfo2.positionInParent.y) - .setLayer(dividerLeash, Integer.MAX_VALUE) - .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) - .show(mRootTaskLeash) - .show(dividerLeash) - .show(mTaskLeash1) - .show(mTaskLeash2)); + mSyncQueue.runInSync(t -> { + t.setLayer(dividerLeash, Integer.MAX_VALUE) + .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, + mTaskInfo1.positionInParent.y) + .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, + mTaskInfo2.positionInParent.y) + .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + .show(mRootTaskLeash) + .show(mTaskLeash1) + .show(mTaskLeash2); + }); } @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { if (taskInfo.taskId == getRootTaskId()) { + if (mRootTaskInfo.isVisible != taskInfo.isVisible) { + mSyncQueue.runInSync(t -> { + if (taskInfo.isVisible) { + t.show(mRootTaskLeash); + } else { + t.hide(mRootTaskLeash); + } + }); + } mRootTaskInfo = taskInfo; - if (mAppPairLayout != null - && mAppPairLayout.updateConfiguration(mRootTaskInfo.configuration)) { - // Update bounds when there is root bounds or orientation changed. + if (mSplitLayout != null + && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { + // Update bounds when root bounds or its orientation changed. final WindowContainerTransaction wct = new WindowContainerTransaction(); - final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash(); - final Rect dividerBounds = mAppPairLayout.getDividerBounds(); - final Rect bounds1 = mAppPairLayout.getBounds1(); - final Rect bounds2 = mAppPairLayout.getBounds2(); + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + final Rect dividerBounds = mSplitLayout.getDividerBounds(); + final Rect bounds1 = mSplitLayout.getBounds1(); + final Rect bounds2 = mSplitLayout.getBounds2(); wct.setBounds(mTaskInfo1.token, bounds1) .setBounds(mTaskInfo2.token, bounds2); @@ -198,7 +204,9 @@ class AppPair implements ShellTaskOrganizer.TaskListener { mSyncQueue.runInSync(t -> t .setPosition(mTaskLeash1, bounds1.left, bounds1.top) .setPosition(mTaskLeash2, bounds2.left, bounds2.top) - .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)); + .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + // Resets layer to divider bar to make sure it is always on top. + .setLayer(dividerLeash, Integer.MAX_VALUE)); } } else if (taskInfo.taskId == getTaskId1()) { mTaskInfo1 = taskInfo; @@ -240,4 +248,39 @@ class AppPair implements ShellTaskOrganizer.TaskListener { public String toString() { return TAG + "#" + getRootTaskId(); } + + @Override + public void onSnappedToDismiss(boolean snappedToEnd) { + unpair(snappedToEnd ? mTaskInfo1.token : mTaskInfo2.token /* toTopToken */); + } + + @Override + public void onBoundsChanging(SplitLayout layout) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) return; + final Rect dividerBounds = layout.getDividerBounds(); + final Rect bounds1 = layout.getBounds1(); + final Rect bounds2 = layout.getBounds2(); + mSyncQueue.runInSync(t -> t + .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + .setPosition(mTaskLeash1, bounds1.left, bounds1.top) + .setPosition(mTaskLeash2, bounds2.left, bounds2.top)); + } + + @Override + public void onBoundsChanged(SplitLayout layout) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) return; + final Rect dividerBounds = layout.getDividerBounds(); + final Rect bounds1 = layout.getBounds1(); + final Rect bounds2 = layout.getBounds2(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mTaskInfo1.token, bounds1) + .setBounds(mTaskInfo2.token, bounds2); + mController.getTaskOrganizer().applyTransaction(wct); + mSyncQueue.runInSync(t -> t + .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + .setPosition(mTaskLeash1, bounds1.left, bounds1.top) + .setPosition(mTaskLeash2, bounds2.left, bounds2.top)); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java deleted file mode 100644 index f8703f7ec0bc..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * 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.apppairs; - -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; -import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; -import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; -import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; -import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; - -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.Region; -import android.os.Binder; -import android.os.IBinder; -import android.view.Display; -import android.view.IWindow; -import android.view.LayoutInflater; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.WindowManager; -import android.view.WindowlessWindowManager; - -import com.android.wm.shell.R; - -/** - * Records and handles layout of a pair of apps. - */ -// TODO(172704238): add tests -final class AppPairLayout { - private static final String DIVIDER_WINDOW_TITLE = "AppPairDivider"; - private final Context mContext; - private final AppPairWindowManager mAppPairWindowManager; - private final SurfaceControlViewHost mViewHost; - - private final int mDividerWindowWidth; - private final int mDividerWindowInsets; - - private boolean mIsLandscape; - private Rect mRootBounds; - private DIVIDE_POLICY mDividePolicy; - - private DividerView mDividerView; - private SurfaceControl mDividerLeash; - - AppPairLayout( - Context context, - Display display, - Configuration configuration, - SurfaceControl rootLeash) { - mContext = context.createConfigurationContext(configuration); - mIsLandscape = isLandscape(configuration); - mRootBounds = configuration.windowConfiguration.getBounds(); - mDividerWindowWidth = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_thickness); - mDividerWindowInsets = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_insets); - - mAppPairWindowManager = new AppPairWindowManager(configuration, rootLeash); - mViewHost = new SurfaceControlViewHost(mContext, display, mAppPairWindowManager); - mDividePolicy = DIVIDE_POLICY.MIDDLE; - mDividePolicy.update(mIsLandscape, mRootBounds, mDividerWindowWidth, mDividerWindowInsets); - } - - boolean updateConfiguration(Configuration configuration) { - mAppPairWindowManager.setConfiguration(configuration); - final Rect rootBounds = configuration.windowConfiguration.getBounds(); - final boolean isLandscape = isLandscape(configuration); - if (mIsLandscape == isLandscape && isIdenticalBounds(mRootBounds, rootBounds)) { - return false; - } - - mIsLandscape = isLandscape; - mRootBounds = rootBounds; - mDividePolicy.update(mIsLandscape, mRootBounds, mDividerWindowWidth, mDividerWindowInsets); - mViewHost.relayout( - mDividePolicy.mDividerBounds.width(), - mDividePolicy.mDividerBounds.height()); - // TODO(172704238): handle divider bar rotation. - return true; - } - - Rect getBounds1() { - return mDividePolicy.mBounds1; - } - - Rect getBounds2() { - return mDividePolicy.mBounds2; - } - - Rect getDividerBounds() { - return mDividePolicy.mDividerBounds; - } - - SurfaceControl getDividerLeash() { - return mDividerLeash; - } - - void release() { - if (mViewHost == null) return; - mViewHost.release(); - } - - void setDividerVisibility(boolean visible) { - if (mDividerView == null) { - initDivider(); - } - if (visible) { - mDividerView.show(); - } else { - mDividerView.hide(); - } - } - - private void initDivider() { - final DividerView dividerView = (DividerView) LayoutInflater.from(mContext) - .inflate(R.layout.split_divider, null); - - WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - mDividePolicy.mDividerBounds.width(), - mDividePolicy.mDividerBounds.height(), - TYPE_DOCK_DIVIDER, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH - | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY, - PixelFormat.TRANSLUCENT); - lp.token = new Binder(); - lp.setTitle(DIVIDER_WINDOW_TITLE); - lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION; - - mViewHost.setView(dividerView, lp); - mDividerView = dividerView; - mDividerLeash = mAppPairWindowManager.getSurfaceControl(mViewHost.getWindowToken()); - } - - private static boolean isLandscape(Configuration configuration) { - return configuration.orientation == ORIENTATION_LANDSCAPE; - } - - private static boolean isIdenticalBounds(Rect bounds1, Rect bounds2) { - return bounds1.left == bounds2.left && bounds1.top == bounds2.top - && bounds1.right == bounds2.right && bounds1.bottom == bounds2.bottom; - } - - /** - * Indicates the policy of placing divider bar and corresponding split-screens. - */ - // TODO(172704238): add more divide policy and provide snap to resize feature for divider bar. - enum DIVIDE_POLICY { - MIDDLE; - - void update(boolean isLandscape, Rect rootBounds, int dividerWindowWidth, - int dividerWindowInsets) { - final int dividerOffset = dividerWindowWidth / 2; - final int boundsOffset = dividerOffset - dividerWindowInsets; - - mDividerBounds = new Rect(rootBounds); - mBounds1 = new Rect(rootBounds); - mBounds2 = new Rect(rootBounds); - - switch (this) { - case MIDDLE: - default: - if (isLandscape) { - mDividerBounds.left = rootBounds.width() / 2 - dividerOffset; - mDividerBounds.right = rootBounds.width() / 2 + dividerOffset; - mBounds1.left = rootBounds.width() / 2 + boundsOffset; - mBounds2.right = rootBounds.width() / 2 - boundsOffset; - } else { - mDividerBounds.top = rootBounds.height() / 2 - dividerOffset; - mDividerBounds.bottom = rootBounds.height() / 2 + dividerOffset; - mBounds1.bottom = rootBounds.height() / 2 - boundsOffset; - mBounds2.top = rootBounds.height() / 2 + boundsOffset; - } - } - } - - Rect mDividerBounds; - Rect mBounds1; - Rect mBounds2; - } - - /** - * WindowManger for app pair. Holds view hierarchy for the root task. - */ - private static final class AppPairWindowManager extends WindowlessWindowManager { - AppPairWindowManager(Configuration config, SurfaceControl rootSurface) { - super(config, rootSurface, null /* hostInputToken */); - } - - @Override - public void setTouchRegion(IBinder window, Region region) { - super.setTouchRegion(window, region); - } - - @Override - public SurfaceControl getSurfaceControl(IWindow window) { - return super.getSurfaceControl(window); - } - - @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java index af06764145e3..f5aa852c87ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java @@ -20,11 +20,14 @@ import android.app.ActivityManager; import androidx.annotation.NonNull; +import com.android.wm.shell.common.annotations.ExternalThread; + import java.io.PrintWriter; /** * Interface to engage app pairs feature. */ +@ExternalThread public interface AppPairs { /** Pairs indicated tasks. */ boolean pair(int task1, int task2); @@ -36,6 +39,4 @@ public interface AppPairs { void dump(@NonNull PrintWriter pw, String prefix); /** Called when the shell organizer has been registered. */ void onOrganizerRegistered(); - /** Called when the visibility of the keyguard changes. */ - void onKeyguardVisibilityChanged(boolean showing); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java index 925a4f36d5e6..f2f09820639a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java @@ -16,8 +16,6 @@ package com.android.wm.shell.apppairs; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; - import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import android.app.ActivityManager; @@ -30,15 +28,13 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TaskStackListenerCallback; -import com.android.wm.shell.common.TaskStackListenerImpl; import java.io.PrintWriter; /** * Class manages app-pairs multitasking mode and implements the main interface {@link AppPairs}. */ -public class AppPairsController implements AppPairs, TaskStackListenerCallback { +public class AppPairsController implements AppPairs { private static final String TAG = AppPairsController.class.getSimpleName(); private final ShellTaskOrganizer mTaskOrganizer; @@ -48,14 +44,12 @@ public class AppPairsController implements AppPairs, TaskStackListenerCallback { // Active app-pairs mapped by root task id key. private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>(); private final DisplayController mDisplayController; - private int mForegroundTaskId = INVALID_TASK_ID; public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, - DisplayController displayController, TaskStackListenerImpl taskStackListener) { + DisplayController displayController) { mTaskOrganizer = organizer; mSyncQueue = syncQueue; mDisplayController = displayController; - taskStackListener.addListener(this); } @Override @@ -71,27 +65,6 @@ public class AppPairsController implements AppPairs, TaskStackListenerCallback { } @Override - public void onTaskMovedToFront(int taskId) { - mForegroundTaskId = INVALID_TASK_ID; - for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { - final AppPair candidate = mActiveAppPairs.valueAt(i); - final boolean containForegroundTask = candidate.contains(taskId); - candidate.setVisible(containForegroundTask); - if (containForegroundTask) { - mForegroundTaskId = candidate.getRootTaskId(); - } - } - } - - @Override - public void onKeyguardVisibilityChanged(boolean showing) { - if (mForegroundTaskId == INVALID_TASK_ID) { - return; - } - mActiveAppPairs.get(mForegroundTaskId).setVisible(!showing); - } - - @Override public boolean pair(int taskId1, int taskId2) { final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1); final ActivityManager.RunningTaskInfo task2 = mTaskOrganizer.getRunningTaskInfo(taskId2); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java deleted file mode 100644 index 41b5e47e7fa9..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.apppairs; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Stack divider for app pair. - */ -public class DividerView extends FrameLayout { - public DividerView(@NonNull Context context) { - super(context); - } - - public DividerView(@NonNull Context context, - @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - void show() { - post(() -> setVisibility(View.VISIBLE)); - } - - void hide() { - post(() -> setVisibility(View.INVISIBLE)); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index aa7355b61eda..40b41e11c8aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1213,7 +1213,7 @@ public class BubbleController implements Bubbles { @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { if (mStackView != null) { - mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight)); + mStackView.onImeVisibilityChanged(imeVisible, imeHeight); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 92d15c5feaca..fa5ac449cd54 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -30,6 +30,8 @@ import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.android.wm.shell.common.annotations.ExternalThread; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -40,6 +42,7 @@ import java.util.function.IntConsumer; /** * Interface to engage bubbles feature. */ +@ExternalThread public interface Bubbles { @Retention(SOURCE) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java deleted file mode 100644 index 96b9f86673fc..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.common; - -import static android.os.Process.THREAD_PRIORITY_DISPLAY; - -import android.annotation.NonNull; -import android.os.HandlerThread; -import android.util.Singleton; - -/** - * A singleton thread for Shell to run animations on. - */ -public class AnimationThread extends HandlerThread { - private ShellExecutor mExecutor; - - private AnimationThread() { - super("wmshell.anim", THREAD_PRIORITY_DISPLAY); - } - - /** Get the singleton instance of this thread */ - public static AnimationThread instance() { - return sAnimationThreadSingleton.get(); - } - - /** - * @return a shared {@link ShellExecutor} associated with this thread - * @hide - */ - @NonNull - public ShellExecutor getExecutor() { - if (mExecutor == null) { - mExecutor = new HandlerExecutor(getThreadHandler()); - } - return mExecutor; - } - - private static final Singleton<AnimationThread> sAnimationThreadSingleton = - new Singleton<AnimationThread>() { - @Override - protected AnimationThread create() { - final AnimationThread animThread = new AnimationThread(); - animThread.start(); - return animThread; - } - }; -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java index 3263f79888d6..cb4584c41184 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -23,6 +23,10 @@ import android.view.IDisplayWindowRotationController; import android.view.IWindowManager; import android.window.WindowContainerTransaction; +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.annotations.ShellMainThread; + import java.util.ArrayList; /** @@ -35,39 +39,18 @@ public class DisplayChangeController { private final Handler mHandler; private final IWindowManager mWmService; + private final IDisplayWindowRotationController mControllerImpl; private final ArrayList<OnDisplayChangingListener> mRotationListener = new ArrayList<>(); private final ArrayList<OnDisplayChangingListener> mTmpListeners = new ArrayList<>(); - private final IDisplayWindowRotationController mDisplayRotationController = - new IDisplayWindowRotationController.Stub() { - @Override - public void onRotateDisplay(int displayId, final int fromRotation, - final int toRotation, IDisplayWindowRotationCallback callback) { - mHandler.post(() -> { - WindowContainerTransaction t = new WindowContainerTransaction(); - synchronized (mRotationListener) { - mTmpListeners.clear(); - // Make a local copy in case the handlers add/remove themselves. - mTmpListeners.addAll(mRotationListener); - } - for (OnDisplayChangingListener c : mTmpListeners) { - c.onRotateDisplay(displayId, fromRotation, toRotation, t); - } - try { - callback.continueRotateDisplay(toRotation, t); - } catch (RemoteException e) { - } - }); - } - }; - public DisplayChangeController(Handler mainHandler, IWindowManager wmService) { mHandler = mainHandler; mWmService = wmService; + mControllerImpl = new DisplayWindowRotationControllerImpl(); try { - mWmService.setDisplayWindowRotationController(mDisplayRotationController); + mWmService.setDisplayWindowRotationController(mControllerImpl); } catch (RemoteException e) { throw new RuntimeException("Unable to register rotation controller"); } @@ -91,10 +74,41 @@ public class DisplayChangeController { } } + private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation, + IDisplayWindowRotationCallback callback) { + WindowContainerTransaction t = new WindowContainerTransaction(); + synchronized (mRotationListener) { + mTmpListeners.clear(); + // Make a local copy in case the handlers add/remove themselves. + mTmpListeners.addAll(mRotationListener); + } + for (OnDisplayChangingListener c : mTmpListeners) { + c.onRotateDisplay(displayId, fromRotation, toRotation, t); + } + try { + callback.continueRotateDisplay(toRotation, t); + } catch (RemoteException e) { + } + } + + @BinderThread + private class DisplayWindowRotationControllerImpl + extends IDisplayWindowRotationController.Stub { + @Override + public void onRotateDisplay(int displayId, final int fromRotation, + final int toRotation, IDisplayWindowRotationCallback callback) { + mHandler.post(() -> { + DisplayChangeController.this.onRotateDisplay(displayId, fromRotation, toRotation, + callback); + }); + } + } + /** * Give a listener a chance to queue up configuration changes to execute as part of a * display rotation. The contents of {@link #onRotateDisplay} must run synchronously. */ + @ShellMainThread public interface OnDisplayChangingListener { /** * Called before the display is rotated. Contents of this method must run synchronously. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 418973204add..a413c052cb6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -28,7 +28,10 @@ import android.view.Display; import android.view.IDisplayWindowListener; import android.view.IWindowManager; +import androidx.annotation.BinderThread; + import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; +import com.android.wm.shell.common.annotations.ShellMainThread; import java.util.ArrayList; @@ -45,6 +48,7 @@ public class DisplayController { private final Context mContext; private final IWindowManager mWmService; private final DisplayChangeController mChangeController; + private final IDisplayWindowListener mDisplayContainerListener; private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); @@ -57,119 +61,13 @@ public class DisplayController { return displayManager.getDisplay(displayId); } - private final IDisplayWindowListener mDisplayContainerListener = - new IDisplayWindowListener.Stub() { - @Override - public void onDisplayAdded(int displayId) { - mHandler.post(() -> { - synchronized (mDisplays) { - if (mDisplays.get(displayId) != null) { - return; - } - Display display = getDisplay(displayId); - if (display == null) { - // It's likely that the display is private to some app and thus not - // accessible by system-ui. - return; - } - DisplayRecord record = new DisplayRecord(); - record.mDisplayId = displayId; - record.mContext = (displayId == Display.DEFAULT_DISPLAY) ? mContext - : mContext.createDisplayContext(display); - record.mDisplayLayout = new DisplayLayout(record.mContext, display); - mDisplays.put(displayId, record); - for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { - mDisplayChangedListeners.get(i).onDisplayAdded(displayId); - } - } - }); - } - - @Override - public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { - mHandler.post(() -> { - synchronized (mDisplays) { - DisplayRecord dr = mDisplays.get(displayId); - if (dr == null) { - Slog.w(TAG, "Skipping Display Configuration change on non-added" - + " display."); - return; - } - Display display = getDisplay(displayId); - if (display == null) { - Slog.w(TAG, "Skipping Display Configuration change on invalid" - + " display. It may have been removed."); - return; - } - Context perDisplayContext = mContext; - if (displayId != Display.DEFAULT_DISPLAY) { - perDisplayContext = mContext.createDisplayContext(display); - } - dr.mContext = perDisplayContext.createConfigurationContext(newConfig); - dr.mDisplayLayout = new DisplayLayout(dr.mContext, display); - for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { - mDisplayChangedListeners.get(i).onDisplayConfigurationChanged( - displayId, newConfig); - } - } - }); - } - - @Override - public void onDisplayRemoved(int displayId) { - mHandler.post(() -> { - synchronized (mDisplays) { - if (mDisplays.get(displayId) == null) { - return; - } - for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { - mDisplayChangedListeners.get(i).onDisplayRemoved(displayId); - } - mDisplays.remove(displayId); - } - }); - } - - @Override - public void onFixedRotationStarted(int displayId, int newRotation) { - mHandler.post(() -> { - synchronized (mDisplays) { - if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { - Slog.w(TAG, "Skipping onFixedRotationStarted on unknown" - + " display, displayId=" + displayId); - return; - } - for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { - mDisplayChangedListeners.get(i).onFixedRotationStarted( - displayId, newRotation); - } - } - }); - } - - @Override - public void onFixedRotationFinished(int displayId) { - mHandler.post(() -> { - synchronized (mDisplays) { - if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { - Slog.w(TAG, "Skipping onFixedRotationFinished on unknown" - + " display, displayId=" + displayId); - return; - } - for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { - mDisplayChangedListeners.get(i).onFixedRotationFinished(displayId); - } - } - }); - } - }; - public DisplayController(Context context, Handler handler, IWindowManager wmService) { mHandler = handler; mContext = context; mWmService = wmService; mChangeController = new DisplayChangeController(mHandler, mWmService); + mDisplayContainerListener = new DisplayWindowListenerImpl(); try { mWmService.registerDisplayWindowListener(mDisplayContainerListener); } catch (RemoteException e) { @@ -232,18 +130,146 @@ public class DisplayController { mChangeController.removeRotationListener(controller); } + private void onDisplayAdded(int displayId) { + synchronized (mDisplays) { + if (mDisplays.get(displayId) != null) { + return; + } + Display display = getDisplay(displayId); + if (display == null) { + // It's likely that the display is private to some app and thus not + // accessible by system-ui. + return; + } + DisplayRecord record = new DisplayRecord(); + record.mDisplayId = displayId; + record.mContext = (displayId == Display.DEFAULT_DISPLAY) ? mContext + : mContext.createDisplayContext(display); + record.mDisplayLayout = new DisplayLayout(record.mContext, display); + mDisplays.put(displayId, record); + for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { + mDisplayChangedListeners.get(i).onDisplayAdded(displayId); + } + } + } + + private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + synchronized (mDisplays) { + DisplayRecord dr = mDisplays.get(displayId); + if (dr == null) { + Slog.w(TAG, "Skipping Display Configuration change on non-added" + + " display."); + return; + } + Display display = getDisplay(displayId); + if (display == null) { + Slog.w(TAG, "Skipping Display Configuration change on invalid" + + " display. It may have been removed."); + return; + } + Context perDisplayContext = mContext; + if (displayId != Display.DEFAULT_DISPLAY) { + perDisplayContext = mContext.createDisplayContext(display); + } + dr.mContext = perDisplayContext.createConfigurationContext(newConfig); + dr.mDisplayLayout = new DisplayLayout(dr.mContext, display); + for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { + mDisplayChangedListeners.get(i).onDisplayConfigurationChanged( + displayId, newConfig); + } + } + } + + private void onDisplayRemoved(int displayId) { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null) { + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i).onDisplayRemoved(displayId); + } + mDisplays.remove(displayId); + } + } + + private void onFixedRotationStarted(int displayId, int newRotation) { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { + Slog.w(TAG, "Skipping onFixedRotationStarted on unknown" + + " display, displayId=" + displayId); + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i).onFixedRotationStarted( + displayId, newRotation); + } + } + } + + private void onFixedRotationFinished(int displayId) { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { + Slog.w(TAG, "Skipping onFixedRotationFinished on unknown" + + " display, displayId=" + displayId); + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i).onFixedRotationFinished(displayId); + } + } + } + private static class DisplayRecord { int mDisplayId; Context mContext; DisplayLayout mDisplayLayout; } + @BinderThread + private class DisplayWindowListenerImpl extends IDisplayWindowListener.Stub { + @Override + public void onDisplayAdded(int displayId) { + mHandler.post(() -> { + DisplayController.this.onDisplayAdded(displayId); + }); + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + mHandler.post(() -> { + DisplayController.this.onDisplayConfigurationChanged(displayId, newConfig); + }); + } + + @Override + public void onDisplayRemoved(int displayId) { + mHandler.post(() -> { + DisplayController.this.onDisplayRemoved(displayId); + }); + } + + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + mHandler.post(() -> { + DisplayController.this.onFixedRotationStarted(displayId, newRotation); + }); + } + + @Override + public void onFixedRotationFinished(int displayId) { + mHandler.post(() -> { + DisplayController.this.onFixedRotationFinished(displayId); + }); + } + } + /** * Gets notified when a display is added/removed to the WM hierarchy and when a display's * window-configuration changes. * * @see IDisplayWindowListener */ + @ShellMainThread public interface OnDisplaysChangedListener { /** * Called when a display has been added to the WM hierarchy. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index ea18a19c2ee5..3fbd7ed0ec5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -24,7 +24,6 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; -import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Slog; @@ -40,6 +39,8 @@ import android.view.WindowInsets; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import androidx.annotation.BinderThread; + import com.android.internal.view.IInputMethodManager; import java.util.ArrayList; @@ -197,6 +198,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mRotation = initialRotation; } + @BinderThread @Override public void insetsChanged(InsetsState insetsState) { mExecutor.execute(() -> { @@ -204,6 +206,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged return; } + mImeShowing = insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME); + final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME); final Rect newFrame = newSource.getFrame(); final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame(); @@ -216,6 +220,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged }); } + @BinderThread @Override public void insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls) { @@ -266,6 +271,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + @BinderThread @Override public void showInsets(int types, boolean fromIme) { if ((types & WindowInsets.Type.ime()) == 0) { @@ -275,6 +281,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mExecutor.execute(() -> startAnimation(true /* show */, false /* forceRestart */)); } + @BinderThread @Override public void hideInsets(int types, boolean fromIme) { if ((types & WindowInsets.Type.ime()) == 0) { @@ -284,6 +291,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mExecutor.execute(() -> startAnimation(false /* show */, false /* forceRestart */)); } + @BinderThread @Override public void topFocusedWindowChanged(String packageName) { // no-op diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java index cd75840b8c71..fa0a75c2d364 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java @@ -28,6 +28,17 @@ public class HandlerExecutor implements ShellExecutor { } @Override + public void execute(@NonNull Runnable command) { + if (mHandler.getLooper().isCurrentThread()) { + command.run(); + return; + } + if (!mHandler.post(command)) { + throw new RuntimeException(mHandler + " is probably exiting"); + } + } + + @Override public void executeDelayed(@NonNull Runnable r, long delayMillis) { if (!mHandler.postDelayed(r, delayMillis)) { throw new RuntimeException(mHandler + " is probably exiting"); @@ -38,11 +49,4 @@ public class HandlerExecutor implements ShellExecutor { public void removeCallbacks(@NonNull Runnable r) { mHandler.removeCallbacks(r); } - - @Override - public void execute(@NonNull Runnable command) { - if (!mHandler.post(command)) { - throw new RuntimeException(mHandler + " is probably exiting"); - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java index aafe2407a1ea..22b831b7565e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java @@ -16,13 +16,40 @@ package com.android.wm.shell.common; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; /** * Super basic Executor interface that adds support for delayed execution and removing callbacks. * Intended to wrap Handler while better-supporting testing. */ public interface ShellExecutor extends Executor { + + /** + * Executes the given runnable. If the caller is running on the same looper as this executor, + * the runnable must be executed immediately. + */ + @Override + void execute(Runnable runnable); + + /** + * Executes the given runnable in a blocking call. If the caller is running on the same looper + * as this executor, the runnable must be executed immediately. + * + * @throws InterruptedException if runnable does not return in the time specified by + * {@param waitTimeout} + */ + default void executeBlocking(Runnable runnable, int waitTimeout, TimeUnit waitTimeUnit) + throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + execute(() -> { + runnable.run(); + latch.countDown(); + }); + latch.await(waitTimeout, waitTimeUnit); + } + /** * See {@link android.os.Handler#postDelayed(Runnable, long)}. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java index 9cb125087cd9..7321dc88770d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java @@ -24,6 +24,10 @@ import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import android.window.WindowOrganizer; +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.annotations.ShellMainThread; + import java.util.ArrayList; /** @@ -151,6 +155,7 @@ public final class SyncTransactionQueue { mHandler.postDelayed(mOnReplyTimeout, REPLY_TIMEOUT); } + @BinderThread @Override public void onTransactionReady(int id, @NonNull SurfaceControl.Transaction t) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java index 0f6dd93f9c16..5e077188c415 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java @@ -19,12 +19,10 @@ package com.android.wm.shell.common; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ITaskStackListener; -import android.app.TaskInfo; import android.content.ComponentName; import android.os.IBinder; import androidx.annotation.BinderThread; -import androidx.annotation.MainThread; /** * An interface to track task stack changes. Classes should implement this instead of @@ -32,85 +30,61 @@ import androidx.annotation.MainThread; */ public interface TaskStackListenerCallback { - @MainThread default void onRecentTaskListUpdated() { } - @MainThread default void onRecentTaskListFrozenChanged(boolean frozen) { } @BinderThread default void onTaskStackChangedBackground() { } - @MainThread default void onTaskStackChanged() { } - @MainThread default void onTaskProfileLocked(int taskId, int userId) { } - @MainThread default void onTaskDisplayChanged(int taskId, int newDisplayId) { } - @MainThread default void onTaskCreated(int taskId, ComponentName componentName) { } - @MainThread default void onTaskRemoved(int taskId) { } - @MainThread default void onTaskMovedToFront(int taskId) { } - @MainThread default void onTaskMovedToFront(RunningTaskInfo taskInfo) { onTaskMovedToFront(taskInfo.taskId); } - @MainThread default void onTaskDescriptionChanged(RunningTaskInfo taskInfo) { } - @MainThread default void onTaskSnapshotChanged(int taskId, ActivityManager.TaskSnapshot snapshot) { } - @MainThread default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { } - @MainThread default void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { } - @MainThread default void onActivityPinned(String packageName, int userId, int taskId, int stackId) { } - @MainThread default void onActivityUnpinned() { } - @MainThread default void onActivityForcedResizable(String packageName, int taskId, int reason) { } - @MainThread default void onActivityDismissingDockedStack() { } - @MainThread default void onActivityLaunchOnSecondaryDisplayFailed() { } - @MainThread default void onActivityLaunchOnSecondaryDisplayFailed(RunningTaskInfo taskInfo) { onActivityLaunchOnSecondaryDisplayFailed(); } - @MainThread default void onActivityLaunchOnSecondaryDisplayRerouted() { } - @MainThread default void onActivityLaunchOnSecondaryDisplayRerouted(RunningTaskInfo taskInfo) { onActivityLaunchOnSecondaryDisplayRerouted(); } - @MainThread default void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { } - @MainThread default void onActivityRotation(int displayId) { } - @MainThread default void onSizeCompatModeActivityChanged(int displayId, IBinder activityToken) { } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java new file mode 100644 index 000000000000..4009ad21b9b8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java @@ -0,0 +1,18 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync + * instead of the app vsync. + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ChoreographerSfVsync {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java new file mode 100644 index 000000000000..7560f71d1f98 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java @@ -0,0 +1,15 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or class that is called from an external thread to the Shell threads. */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ExternalThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java new file mode 100644 index 000000000000..0479f8780c79 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java @@ -0,0 +1,15 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellAnimationThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java new file mode 100644 index 000000000000..423f4ce3bfd4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java @@ -0,0 +1,15 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell main-thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellMainThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java new file mode 100644 index 000000000000..50d9fe8629ac --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -0,0 +1,171 @@ +/* + * 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.common.split; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceControlViewHost; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.policy.DividerSnapAlgorithm; + +/** + * Stack divider for app pair. + */ +// TODO(b/172704238): add handle view to indicate touching status. +public class DividerView extends FrameLayout implements View.OnTouchListener { + private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + + private SplitLayout mSplitLayout; + private SurfaceControlViewHost mViewHost; + private DragListener mDragListener; + + private VelocityTracker mVelocityTracker; + private boolean mMoving; + private int mStartPos; + + public DividerView(@NonNull Context context) { + super(context); + } + + public DividerView(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** Sets up essential dependencies of the divider bar. */ + public void setup( + SplitLayout layout, + SurfaceControlViewHost viewHost, + @Nullable DragListener dragListener) { + mSplitLayout = layout; + mViewHost = viewHost; + mDragListener = dragListener; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mSplitLayout == null) { + return false; + } + + final int action = event.getAction() & MotionEvent.ACTION_MASK; + final boolean isLandscape = isLandscape(); + // Using raw xy to prevent lost track of motion events while moving divider bar. + final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mVelocityTracker = VelocityTracker.obtain(); + mVelocityTracker.addMovement(event); + setSlippery(false); + mStartPos = touchPos; + mMoving = false; + break; + case MotionEvent.ACTION_MOVE: + mVelocityTracker.addMovement(event); + if (!mMoving && Math.abs(touchPos - mStartPos) > mTouchSlop) { + mStartPos = touchPos; + mMoving = true; + if (mDragListener != null) { + mDragListener.onDragStart(); + } + } + if (mMoving) { + final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; + mSplitLayout.updateDividePosition(position); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mVelocityTracker.addMovement(event); + mVelocityTracker.computeCurrentVelocity(1000 /* units */); + final float velocity = isLandscape + ? mVelocityTracker.getXVelocity() + : mVelocityTracker.getYVelocity(); + setSlippery(true); + mMoving = false; + if (mDragListener != null) { + mDragListener.onDragEnd(); + } + + final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; + final DividerSnapAlgorithm.SnapTarget snapTarget = + mSplitLayout.findSnapTarget(position, velocity); + mSplitLayout.setSnapTarget(snapTarget); + break; + } + return true; + } + + private void setSlippery(boolean slippery) { + if (mViewHost == null) { + return; + } + + final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams(); + final boolean isSlippery = (lp.flags & FLAG_SLIPPERY) != 0; + if (isSlippery == slippery) { + return; + } + + if (slippery) { + lp.flags |= FLAG_SLIPPERY; + } else { + lp.flags &= ~FLAG_SLIPPERY; + } + mViewHost.relayout(lp); + } + + private boolean isLandscape() { + return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; + } + + /** Monitors dragging action of the divider bar. */ + // TODO(b/172704238): add listeners to deal with resizing state of the app windows. + public interface DragListener { + /** Called when start dragging. */ + void onDragStart(); + /** Called when stop dragging. */ + void onDragEnd(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java new file mode 100644 index 000000000000..e11037f55cfa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -0,0 +1,205 @@ +/* + * 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.common.split; + +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; +import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.annotation.Nullable; + +import com.android.internal.policy.DividerSnapAlgorithm; + +/** + * Records and handles layout of splits. Helps to calculate proper bounds when configuration or + * divide position changes. + */ +public class SplitLayout { + private final int mDividerWindowWidth; + private final int mDividerInsets; + private final int mDividerSize; + + private final Rect mRootBounds = new Rect(); + private final Rect mDividerBounds = new Rect(); + private final Rect mBounds1 = new Rect(); + private final Rect mBounds2 = new Rect(); + private final LayoutChangeListener mLayoutChangeListener; + private final SplitWindowManager mSplitWindowManager; + + private Context mContext; + private DividerSnapAlgorithm mDividerSnapAlgorithm; + private int mDividePosition; + + public SplitLayout(Context context, Configuration configuration, + LayoutChangeListener layoutChangeListener, SurfaceControl rootLeash) { + mContext = context.createConfigurationContext(configuration); + mLayoutChangeListener = layoutChangeListener; + mSplitWindowManager = new SplitWindowManager(mContext, configuration, rootLeash); + + mDividerWindowWidth = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_thickness); + mDividerInsets = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_insets); + mDividerSize = mDividerWindowWidth - mDividerInsets * 2; + + mRootBounds.set(configuration.windowConfiguration.getBounds()); + mDividerSnapAlgorithm = getSnapAlgorithm(context.getResources(), mRootBounds); + mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; + updateBounds(mDividePosition); + } + + /** Gets bounds of the primary split. */ + public Rect getBounds1() { + return mBounds1; + } + + /** Gets bounds of the secondary split. */ + public Rect getBounds2() { + return mBounds2; + } + + /** Gets bounds of divider window. */ + public Rect getDividerBounds() { + return mDividerBounds; + } + + /** Returns leash of the current divider bar. */ + @Nullable + public SurfaceControl getDividerLeash() { + return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl(); + } + + int getDividePosition() { + return mDividePosition; + } + + /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ + public boolean updateConfiguration(Configuration configuration) { + final Rect rootBounds = configuration.windowConfiguration.getBounds(); + if (mRootBounds.equals(rootBounds)) { + return false; + } + + mContext = mContext.createConfigurationContext(configuration); + mSplitWindowManager.setConfiguration(configuration); + mRootBounds.set(rootBounds); + mDividerSnapAlgorithm = getSnapAlgorithm(mContext.getResources(), mRootBounds); + mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; + updateBounds(mDividePosition); + release(); + init(); + return true; + } + + /** Updates recording bounds of divider window and both of the splits. */ + private void updateBounds(int position) { + mDividerBounds.set(mRootBounds); + mBounds1.set(mRootBounds); + mBounds2.set(mRootBounds); + if (isLandscape(mRootBounds)) { + mDividerBounds.left = position - mDividerInsets; + mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth; + mBounds1.right = mBounds1.left + position; + mBounds2.left = mBounds1.right + mDividerSize; + } else { + mDividerBounds.top = position - mDividerInsets; + mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth; + mBounds1.bottom = mBounds1.top + position; + mBounds2.top = mBounds1.bottom + mDividerSize; + } + } + + /** Inflates {@link DividerView} on the root surface. */ + public void init() { + mSplitWindowManager.init(this); + } + + /** Releases the surface holding the current {@link DividerView}. */ + public void release() { + mSplitWindowManager.release(); + } + + /** + * Updates bounds with the passing position. Usually used to update recording bounds while + * performing animation or dragging divider bar to resize the splits. + */ + public void updateDividePosition(int position) { + updateBounds(position); + mLayoutChangeListener.onBoundsChanging(this); + } + + /** + * Sets new divide position and updates bounds correspondingly. Notifies listener if the new + * target indicates dismissing split. + */ + public void setSnapTarget(DividerSnapAlgorithm.SnapTarget snapTarget) { + switch(snapTarget.flag) { + case FLAG_DISMISS_START: + mLayoutChangeListener.onSnappedToDismiss(false /* snappedToEnd */); + break; + case FLAG_DISMISS_END: + mLayoutChangeListener.onSnappedToDismiss(true /* snappedToEnd */); + break; + default: + mDividePosition = snapTarget.position; + updateBounds(mDividePosition); + mLayoutChangeListener.onBoundsChanged(this); + break; + } + } + + /** + * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity. + */ + public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity) { + return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity); + } + + private DividerSnapAlgorithm getSnapAlgorithm(Resources resources, Rect rootBounds) { + final boolean isLandscape = isLandscape(rootBounds); + return new DividerSnapAlgorithm( + resources, + rootBounds.width(), + rootBounds.height(), + mDividerSize, + !isLandscape, + new Rect() /* insets */, + isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); + } + + private static boolean isLandscape(Rect bounds) { + return bounds.width() > bounds.height(); + } + + /** Listens layout change event. */ + public interface LayoutChangeListener { + /** Calls when dismissing split. */ + void onSnappedToDismiss(boolean snappedToEnd); + /** Calls when the bounds is changing due to animation or dragging divider bar. */ + void onBoundsChanging(SplitLayout layout); + /** Calls when the target bounds changed. */ + void onBoundsChanged(SplitLayout layout); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java new file mode 100644 index 000000000000..e4121986bcb7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -0,0 +1,116 @@ +/* + * 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.common.split; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; +import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.Binder; +import android.os.IBinder; +import android.view.IWindow; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +/** + * Holds view hierarchy of a root surface and helps to inflate {@link DividerView} for a split. + */ +public final class SplitWindowManager extends WindowlessWindowManager { + private static final String DIVIDER_WINDOW_TITLE = "SplitDivider"; + + private Context mContext; + private SurfaceControlViewHost mViewHost; + + public SplitWindowManager(Context context, Configuration config, SurfaceControl rootSurface) { + super(config, rootSurface, null /* hostInputToken */); + mContext = context.createConfigurationContext(config); + } + + @Override + public void setTouchRegion(IBinder window, Region region) { + super.setTouchRegion(window, region); + } + + @Override + public SurfaceControl getSurfaceControl(IWindow window) { + return super.getSurfaceControl(window); + } + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + mContext = mContext.createConfigurationContext(configuration); + } + + /** Inflates {@link DividerView} on to the root surface. */ + void init(SplitLayout splitLayout) { + if (mViewHost == null) { + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + } + + final Rect dividerBounds = splitLayout.getDividerBounds(); + final DividerView dividerView = (DividerView) LayoutInflater.from(mContext) + .inflate(R.layout.split_divider, null /* root */); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + dividerBounds.width(), dividerBounds.height(), TYPE_DOCK_DIVIDER, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH + | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT); + lp.token = new Binder(); + lp.setTitle(DIVIDER_WINDOW_TITLE); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + mViewHost.setView(dividerView, lp); + dividerView.setup(splitLayout, mViewHost, null /* dragListener */); + } + + /** + * Releases the surface control of the current {@link DividerView} and tear down the view + * hierarchy. + */ + void release() { + if (mViewHost == null) return; + mViewHost.release(); + mViewHost = null; + } + + /** + * Gets {@link SurfaceControl} of the surface holding divider view. @return {@code null} if not + * feasible. + */ + @Nullable + SurfaceControl getSurfaceControl() { + return mViewHost == null ? null : getSurfaceControl(mViewHost.getWindowToken()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 625c0a7b5d19..a89c8bb29c1b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -54,7 +54,6 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreen; -import java.util.Objects; import java.util.Optional; /** @@ -108,16 +107,22 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange DragLayout dragLayout = new DragLayout(context, mSplitScreen); rootView.addView(dragLayout, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - wm.addView(rootView, layoutParams); - - mDisplayDropTargets.put(displayId, - new PerDisplay(displayId, context, wm, rootView, dragLayout)); + try { + wm.addView(rootView, layoutParams); + mDisplayDropTargets.put(displayId, + new PerDisplay(displayId, context, wm, rootView, dragLayout)); + } catch (WindowManager.InvalidDisplayException e) { + Slog.w(TAG, "Unable to add view for display id: " + displayId); + } } @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display changed: %d", displayId); final PerDisplay pd = mDisplayDropTargets.get(displayId); + if (pd == null) { + return; + } pd.rootView.requestApplyInsets(); } @@ -125,6 +130,9 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange public void onDisplayRemoved(int displayId) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display removed: %d", displayId); final PerDisplay pd = mDisplayDropTargets.get(displayId); + if (pd == null) { + return; + } pd.wm.removeViewImmediate(pd.rootView); mDisplayDropTargets.remove(displayId); } @@ -139,6 +147,10 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange final PerDisplay pd = mDisplayDropTargets.get(displayId); final ClipDescription description = event.getClipDescription(); + if (pd == null) { + return false; + } + if (event.getAction() == ACTION_DRAG_STARTED) { final boolean hasValidClipData = event.getClipData().getItemCount() > 0 && (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 8a547b4477fd..7b3b5dbfa51c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -18,12 +18,10 @@ package com.android.wm.shell.draganddrop; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; import static android.content.ClipDescription.EXTRA_PENDING_INTENT; -import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; import static android.content.Intent.EXTRA_PACKAGE_NAME; @@ -78,7 +76,7 @@ public class DragAndDropPolicy { private static final String TAG = DragAndDropPolicy.class.getSimpleName(); private final Context mContext; - private final IActivityTaskManager mIActivityTaskManager; + private final ActivityTaskManager mActivityTaskManager; private final Starter mStarter; private final SplitScreen mSplitScreen; private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); @@ -86,15 +84,15 @@ public class DragAndDropPolicy { private DragSession mSession; public DragAndDropPolicy(Context context, SplitScreen splitScreen) { - this(context, ActivityTaskManager.getService(), splitScreen, + this(context, ActivityTaskManager.getInstance(), splitScreen, new DefaultStarter(context, splitScreen)); } @VisibleForTesting - DragAndDropPolicy(Context context, IActivityTaskManager activityTaskManager, + DragAndDropPolicy(Context context, ActivityTaskManager activityTaskManager, SplitScreen splitScreen, Starter starter) { mContext = context; - mIActivityTaskManager = activityTaskManager; + mActivityTaskManager = activityTaskManager; mSplitScreen = splitScreen; mStarter = starter; } @@ -103,7 +101,7 @@ public class DragAndDropPolicy { * Starts a new drag session with the given initial drag data. */ void start(DisplayLayout displayLayout, ClipData data) { - mSession = new DragSession(mContext, mIActivityTaskManager, displayLayout, data); + mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data); // TODO(b/169894807): Also update the session data with task stack changes mSession.update(); } @@ -271,7 +269,7 @@ public class DragAndDropPolicy { */ private static class DragSession { private final Context mContext; - private final IActivityTaskManager mIActivityTaskManager; + private final ActivityTaskManager mActivityTaskManager; private final ClipData mInitialDragData; final DisplayLayout displayLayout; @@ -285,10 +283,10 @@ public class DragAndDropPolicy { boolean dragItemSupportsSplitscreen; boolean isPhone; - DragSession(Context context, IActivityTaskManager activityTaskManager, + DragSession(Context context, ActivityTaskManager activityTaskManager, DisplayLayout dispLayout, ClipData data) { mContext = context; - mIActivityTaskManager = activityTaskManager; + mActivityTaskManager = activityTaskManager; mInitialDragData = data; displayLayout = dispLayout; } @@ -298,19 +296,14 @@ public class DragAndDropPolicy { */ void update() { - try { - List<ActivityManager.RunningTaskInfo> tasks = - mIActivityTaskManager.getFilteredTasks(1, - false /* filterOnlyVisibleRecents */); - if (!tasks.isEmpty()) { - final ActivityManager.RunningTaskInfo task = tasks.get(0); - runningTaskWinMode = task.getWindowingMode(); - runningTaskActType = task.getActivityType(); - runningTaskId = task.taskId; - runningTaskIsResizeable = task.isResizeable; - } - } catch (RemoteException e) { - // Fall through + List<ActivityManager.RunningTaskInfo> tasks = + mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); + if (!tasks.isEmpty()) { + final ActivityManager.RunningTaskInfo task = tasks.get(0); + runningTaskWinMode = task.getWindowingMode(); + runningTaskActType = task.getActivityType(); + runningTaskId = task.taskId; + runningTaskIsResizeable = task.isResizeable; } final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java index 38e0519b7a90..3a2f0da6bf03 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java @@ -20,11 +20,14 @@ import android.content.res.Configuration; import androidx.annotation.NonNull; +import com.android.wm.shell.common.annotations.ExternalThread; + import java.io.PrintWriter; /** * Interface to engage hide display cutout feature. */ +@ExternalThread public interface HideDisplayCutout { /** * Notifies {@link Configuration} changed. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java index 090d2270817b..4e62ea6e7233 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java @@ -272,8 +272,9 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer { @VisibleForTesting void applyBoundsAndOffsets(WindowContainerToken token, SurfaceControl leash, WindowContainerTransaction wct, SurfaceControl.Transaction t) { - wct.setBounds(token, mCurrentDisplayBounds.isEmpty() ? null : mCurrentDisplayBounds); + wct.setBounds(token, mCurrentDisplayBounds); t.setPosition(leash, mOffsetX, mOffsetY); + t.setWindowCrop(leash, mCurrentDisplayBounds.width(), mCurrentDisplayBounds.height()); } @VisibleForTesting diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/letterbox/LetterboxTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/letterbox/LetterboxTaskListener.java index 061d3f86b669..490ef3296be6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/letterbox/LetterboxTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/letterbox/LetterboxTaskListener.java @@ -107,6 +107,7 @@ public class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener { transaction.setWindowCrop(leash, crop); } + // TODO(b/173440321): Correct presentation of letterboxed activities in One-handed mode. private void resolveTaskPositionAndCrop( ActivityManager.RunningTaskInfo taskInfo, Point positionInParent, @@ -125,15 +126,18 @@ public class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener { final Rect activityBounds = taskInfo.letterboxActivityBounds; Insets insets = getInsets(); + Rect displayBoundsWithInsets = + new Rect(mWindowManager.getMaximumWindowMetrics().getBounds()); + displayBoundsWithInsets.inset(insets); Rect taskBoundsWithInsets = new Rect(taskBounds); - applyInsets(taskBoundsWithInsets, insets, taskInfo.parentBounds); + taskBoundsWithInsets.intersect(displayBoundsWithInsets); Rect activityBoundsWithInsets = new Rect(activityBounds); - applyInsets(activityBoundsWithInsets, insets, taskInfo.parentBounds); + activityBoundsWithInsets.intersect(displayBoundsWithInsets); Rect parentBoundsWithInsets = new Rect(parentBounds); - applyInsets(parentBoundsWithInsets, insets, parentBounds); + parentBoundsWithInsets.intersect(displayBoundsWithInsets); // Crop need to be in the task coordinates. crop.set(activityBoundsWithInsets); @@ -160,8 +164,6 @@ public class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener { switch (gravity) { case Gravity.TOP: positionInParent.y += taskBoundsWithInsets.top - activityBoundsWithInsets.top; - // Showing status bar decor view. - crop.top -= activityBoundsWithInsets.top - activityBounds.top; break; case Gravity.CENTER: positionInParent.y += @@ -183,8 +185,6 @@ public class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener { final int gravity = mLetterboxConfigController.getLandscapeGravity(); // Align activity to the top. positionInParent.y += taskBoundsWithInsets.top - activityBoundsWithInsets.top; - // Showing status bar decor view. - crop.top -= activityBoundsWithInsets.top - activityBounds.top; switch (gravity) { case Gravity.LEFT: positionInParent.x += taskBoundsWithInsets.left - activityBoundsWithInsets.left; @@ -205,6 +205,53 @@ public class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener { + " for task: #" + taskInfo.taskId); } } + + // New bounds of the activity after it's repositioned with required gravity. + Rect newActivityBounds = new Rect(activityBounds); + // Task's surfce will be repositioned to positionInParent together with the activity + // inside it so the new activity bounds are the original activity bounds offset by + // the task's offset. + newActivityBounds.offset( + positionInParent.x - taskBounds.left, positionInParent.y - taskBounds.top); + Rect newActivityBoundsWithInsets = new Rect(newActivityBounds); + newActivityBoundsWithInsets.intersect(displayBoundsWithInsets); + // Activity handles insets on its own (e.g. under status bar or navigation bar). + // crop that is calculated above crops all insets from an activity and below insets that + // can be shown are added back to the crop bounds (e.g. if activity is still shown at the + // top of the display then the top inset won't be cropped). + // After task's surface is repositioned, intersection between an activity and insets can + // change but if it doesn't, the activity should be shown under insets to maximize visible + // area. + // Also, an activity can use area under insets and insets shouldn't be cropped in this case + // regardless of a position on the screen. + final Rect activityInsetsFromCore = taskInfo.letterboxActivityInsets; + if (newActivityBounds.top - newActivityBoundsWithInsets.top + == activityBounds.top - activityBoundsWithInsets.top + // Check whether an activity is shown under inset. If it is, then the inset from + // WM Core and the inset computed here will be different because local insets + // doesn't take into account visibility of insets requested by the activity. + || activityBoundsWithInsets.top - activityBounds.top + != activityInsetsFromCore.top) { + crop.top -= activityBoundsWithInsets.top - activityBounds.top; + } + if (newActivityBounds.bottom - newActivityBoundsWithInsets.bottom + == activityBounds.bottom - activityBoundsWithInsets.bottom + || activityBounds.bottom - activityBoundsWithInsets.bottom + != activityInsetsFromCore.bottom) { + crop.bottom += activityBounds.bottom - activityBoundsWithInsets.bottom; + } + if (newActivityBounds.left - newActivityBoundsWithInsets.left + == activityBounds.left - activityBoundsWithInsets.left + || activityBoundsWithInsets.left - activityBounds.left + != activityInsetsFromCore.left) { + crop.left -= activityBoundsWithInsets.left - activityBounds.left; + } + if (newActivityBounds.right - newActivityBoundsWithInsets.right + == activityBounds.right - activityBoundsWithInsets.right + || activityBounds.right - activityBoundsWithInsets.right + != activityInsetsFromCore.right) { + crop.right += activityBounds.right - activityBoundsWithInsets.right; + } } private Insets getInsets() { @@ -217,10 +264,4 @@ public class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener { | WindowInsets.Type.displayCutout()); } - private void applyInsets(Rect innerBounds, Insets insets, Rect outerBounds) { - Rect outerBoundsWithInsets = new Rect(outerBounds); - outerBoundsWithInsets.inset(insets); - innerBounds.intersect(outerBoundsWithInsets); - } - } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java index 9bb709f9a82a..821a00703adf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -18,6 +18,7 @@ package com.android.wm.shell.onehanded; import androidx.annotation.NonNull; +import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback; import java.io.PrintWriter; @@ -25,6 +26,7 @@ import java.io.PrintWriter; /** * Interface to engage one handed feature. */ +@ExternalThread public interface OneHanded { /** * Return one handed settings enabled or not. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java index 993e0e7ed016..d59aec2cc446 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java @@ -19,12 +19,15 @@ package com.android.wm.shell.pip; import android.app.RemoteAction; import android.content.ComponentName; import android.content.pm.ParceledListSlice; -import android.view.DisplayInfo; -import android.view.IPinnedStackController; +import android.os.RemoteException; import android.view.IPinnedStackListener; +import android.view.WindowManagerGlobal; + +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.ShellExecutor; import java.util.ArrayList; -import java.util.List; /** * PinnedStackListener that simply forwards all calls to each listener added via @@ -32,8 +35,15 @@ import java.util.List; * {@link com.android.server.wm.WindowManagerService#registerPinnedStackListener} replaces any * previously set listener. */ -public class PinnedStackListenerForwarder extends IPinnedStackListener.Stub { - private List<PinnedStackListener> mListeners = new ArrayList<>(); +public class PinnedStackListenerForwarder { + + private final IPinnedStackListener mListenerImpl = new PinnedStackListenerImpl(); + private final ShellExecutor mShellMainExecutor; + private final ArrayList<PinnedStackListener> mListeners = new ArrayList<>(); + + public PinnedStackListenerForwarder(ShellExecutor shellMainExecutor) { + mShellMainExecutor = shellMainExecutor; + } /** Adds a listener to receive updates from the WindowManagerService. */ public void addListener(PinnedStackListener listener) { @@ -45,59 +55,76 @@ public class PinnedStackListenerForwarder extends IPinnedStackListener.Stub { mListeners.remove(listener); } - @Override - public void onListenerRegistered(IPinnedStackController controller) { - for (PinnedStackListener listener : mListeners) { - listener.onListenerRegistered(controller); - } + public void register(int displayId) throws RemoteException { + WindowManagerGlobal.getWindowManagerService().registerPinnedStackListener( + displayId, mListenerImpl); } - @Override - public void onMovementBoundsChanged(boolean fromImeAdjustment) { + private void onMovementBoundsChanged(boolean fromImeAdjustment) { for (PinnedStackListener listener : mListeners) { listener.onMovementBoundsChanged(fromImeAdjustment); } } - @Override - public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + private void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { for (PinnedStackListener listener : mListeners) { listener.onImeVisibilityChanged(imeVisible, imeHeight); } } - @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + private void onActionsChanged(ParceledListSlice<RemoteAction> actions) { for (PinnedStackListener listener : mListeners) { listener.onActionsChanged(actions); } } - @Override - public void onActivityHidden(ComponentName componentName) { + private void onActivityHidden(ComponentName componentName) { for (PinnedStackListener listener : mListeners) { listener.onActivityHidden(componentName); } } - @Override - public void onDisplayInfoChanged(DisplayInfo displayInfo) { + private void onAspectRatioChanged(float aspectRatio) { for (PinnedStackListener listener : mListeners) { - listener.onDisplayInfoChanged(displayInfo); + listener.onAspectRatioChanged(aspectRatio); } } - @Override - public void onConfigurationChanged() { - for (PinnedStackListener listener : mListeners) { - listener.onConfigurationChanged(); + @BinderThread + private class PinnedStackListenerImpl extends IPinnedStackListener.Stub { + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onMovementBoundsChanged(fromImeAdjustment); + }); } - } - @Override - public void onAspectRatioChanged(float aspectRatio) { - for (PinnedStackListener listener : mListeners) { - listener.onAspectRatioChanged(aspectRatio); + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onImeVisibilityChanged(imeVisible, imeHeight); + }); + } + + @Override + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onActionsChanged(actions); + }); + } + + @Override + public void onActivityHidden(ComponentName componentName) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onActivityHidden(componentName); + }); + } + + @Override + public void onAspectRatioChanged(float aspectRatio) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onAspectRatioChanged(aspectRatio); + }); } } @@ -106,8 +133,6 @@ public class PinnedStackListenerForwarder extends IPinnedStackListener.Stub { * Subclasses can ignore those methods they do not intend to take action upon. */ public static class PinnedStackListener { - public void onListenerRegistered(IPinnedStackController controller) {} - public void onMovementBoundsChanged(boolean fromImeAdjustment) {} public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} @@ -116,10 +141,6 @@ public class PinnedStackListenerForwarder extends IPinnedStackListener.Stub { public void onActivityHidden(ComponentName componentName) {} - public void onDisplayInfoChanged(DisplayInfo displayInfo) {} - - public void onConfigurationChanged() {} - public void onAspectRatioChanged(float aspectRatio) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index 9fa222ad4fdd..1f07542c9a27 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -17,12 +17,13 @@ package com.android.wm.shell.pip; import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.graphics.Rect; +import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.pip.phone.PipTouchHandler; import java.io.PrintWriter; @@ -31,6 +32,7 @@ import java.util.function.Consumer; /** * Interface to engage picture in picture feature. */ +@ExternalThread public interface Pip { /** * Closes PIP (PIPed activity and PIP system UI). @@ -81,6 +83,12 @@ public interface Pip { } /** + * Called when configuration is changed. + */ + default void onConfigurationChanged(Configuration newConfig) { + } + + /** * Called when display size or font size of settings changed */ default void onDensityOrFontScaleChanged() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index d82946269ee8..fe018115408a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -54,6 +54,7 @@ public class PipAnimationController { public static final int TRANSITION_DIRECTION_LEAVE_PIP = 3; public static final int TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN = 4; public static final int TRANSITION_DIRECTION_REMOVE_STACK = 5; + public static final int TRANSITION_DIRECTION_SNAP_AFTER_RESIZE = 6; @IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = { TRANSITION_DIRECTION_NONE, @@ -61,7 +62,8 @@ public class PipAnimationController { TRANSITION_DIRECTION_TO_PIP, TRANSITION_DIRECTION_LEAVE_PIP, TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN, - TRANSITION_DIRECTION_REMOVE_STACK + TRANSITION_DIRECTION_REMOVE_STACK, + TRANSITION_DIRECTION_SNAP_AFTER_RESIZE }) @Retention(RetentionPolicy.SOURCE) public @interface TransitionDirection {} @@ -109,13 +111,27 @@ public class PipAnimationController { } @SuppressWarnings("unchecked") + /** + * Construct and return an animator that animates from the {@param startBounds} to the + * {@param endBounds} with the given {@param direction}. If {@param direction} is type + * {@link ANIM_TYPE_BOUNDS}, then {@param sourceHintRect} will be used to animate + * in a better, more smooth manner. + * + * In the case where one wants to start animation during an intermediate animation (for example, + * if the user is currently doing a pinch-resize, and upon letting go now PiP needs to animate + * to the correct snap fraction region), then provide the base bounds, which is current PiP + * leash bounds before transformation/any animation. This is so when we try to construct + * the different transformation matrices for the animation, we are constructing this based off + * the PiP original bounds, rather than the {@param startBounds}, which is post-transformed. + */ @VisibleForTesting - public PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds, - Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction) { + public PipTransitionAnimator getAnimator(SurfaceControl leash, Rect baseBounds, + Rect startBounds, Rect endBounds, Rect sourceHintRect, + @PipAnimationController.TransitionDirection int direction) { if (mCurrentAnimator == null) { mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect, - direction)); + PipTransitionAnimator.ofBounds(leash, startBounds, startBounds, endBounds, + sourceHintRect, direction)); } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA && mCurrentAnimator.isRunning()) { // If we are still animating the fade into pip, then just move the surface and ensure @@ -130,8 +146,8 @@ public class PipAnimationController { } else { mCurrentAnimator.cancel(); mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect, - direction)); + PipTransitionAnimator.ofBounds(leash, baseBounds, startBounds, endBounds, + sourceHintRect, direction)); } return mCurrentAnimator; } @@ -180,6 +196,7 @@ public class PipAnimationController { private final @AnimationType int mAnimationType; private final Rect mDestinationBounds = new Rect(); + private T mBaseValue; protected T mCurrentValue; protected T mStartValue; private T mEndValue; @@ -190,10 +207,11 @@ public class PipAnimationController { private @TransitionDirection int mTransitionDirection; private PipTransitionAnimator(SurfaceControl leash, @AnimationType int animationType, - Rect destinationBounds, T startValue, T endValue) { + Rect destinationBounds, T baseValue, T startValue, T endValue) { mLeash = leash; mAnimationType = animationType; mDestinationBounds.set(destinationBounds); + mBaseValue = baseValue; mStartValue = startValue; mEndValue = endValue; addListener(this); @@ -263,6 +281,10 @@ public class PipAnimationController { return mStartValue; } + T getBaseValue() { + return mBaseValue; + } + @VisibleForTesting public T getEndValue() { return mEndValue; @@ -334,7 +356,7 @@ public class PipAnimationController { static PipTransitionAnimator<Float> ofAlpha(SurfaceControl leash, Rect destinationBounds, float startValue, float endValue) { return new PipTransitionAnimator<Float>(leash, ANIM_TYPE_ALPHA, - destinationBounds, startValue, endValue) { + destinationBounds, startValue, startValue, endValue) { @Override void applySurfaceControlTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, float fraction) { @@ -367,7 +389,7 @@ public class PipAnimationController { } static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash, - Rect startValue, Rect endValue, Rect sourceHintRect, + Rect baseValue, Rect startValue, Rect endValue, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction) { // Just for simplicity we'll interpolate between the source rect hint insets and empty // insets to calculate the window crop @@ -375,7 +397,7 @@ public class PipAnimationController { if (isOutPipDirection(direction)) { initialSourceValue = new Rect(endValue); } else { - initialSourceValue = new Rect(startValue); + initialSourceValue = new Rect(baseValue); } final Rect sourceHintRectInsets; @@ -391,22 +413,24 @@ public class PipAnimationController { // construct new Rect instances in case they are recycled return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS, - endValue, new Rect(startValue), new Rect(endValue)) { + endValue, new Rect(baseValue), new Rect(startValue), new Rect(endValue)) { private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); @Override void applySurfaceControlTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, float fraction) { + final Rect base = getBaseValue(); final Rect start = getStartValue(); final Rect end = getEndValue(); Rect bounds = mRectEvaluator.evaluate(fraction, start, end); setCurrentValue(bounds); if (inScaleTransition() || sourceHintRect == null) { + if (isOutPipDirection(direction)) { getSurfaceTransactionHelper().scale(tx, leash, end, bounds); } else { - getSurfaceTransactionHelper().scale(tx, leash, start, bounds); + getSurfaceTransactionHelper().scale(tx, leash, base, bounds); } } else { final Rect insets; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java index 1bb5eda25058..484592e87a20 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -102,9 +102,7 @@ public class PipBoundsAlgorithm { return mSnapAlgorithm; } - /** - * Responds to IPinnedStackListener on configuration change. - */ + /** Responds to configuration change. */ public void onConfigurationChanged(Context context) { reloadResources(context); } @@ -125,15 +123,16 @@ public class PipBoundsAlgorithm { /** Returns the destination bounds to place the PIP window on entry. */ public Rect getEntryDestinationBounds() { final PipBoundsState.PipReentryState reentryState = mPipBoundsState.getReentryState(); - final boolean shouldRestoreReentryBounds = reentryState != null; - final Rect destinationBounds = shouldRestoreReentryBounds + final Rect destinationBounds = reentryState != null ? getDefaultBounds(reentryState.getSnapFraction(), reentryState.getSize()) : getDefaultBounds(); - return transformBoundsToAspectRatioIfValid(destinationBounds, + final boolean useCurrentSize = reentryState != null && reentryState.getSize() != null; + final Rect r = transformBoundsToAspectRatioIfValid(destinationBounds, mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, - shouldRestoreReentryBounds); + useCurrentSize); + return r; } /** Returns the current bounds adjusted to the new aspect ratio, if valid. */ @@ -223,24 +222,36 @@ public class PipBoundsAlgorithm { private Rect getDefaultBounds(float snapFraction, Size size) { final Rect defaultBounds = new Rect(); if (snapFraction != INVALID_SNAP_FRACTION && size != null) { + // The default bounds are the given size positioned at the given snap fraction. defaultBounds.set(0, 0, size.getWidth(), size.getHeight()); final Rect movementBounds = getMovementBounds(defaultBounds); mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction); + return defaultBounds; + } + + // Calculate the default size. + final Size defaultSize; + final Rect insetBounds = new Rect(); + getInsetBounds(insetBounds); + final DisplayInfo displayInfo = mPipBoundsState.getDisplayInfo(); + final Size overrideMinSize = mPipBoundsState.getOverrideMinSize(); + if (overrideMinSize != null) { + // The override minimal size is set, use that as the default size making sure it's + // adjusted to the aspect ratio. + defaultSize = adjustSizeToAspectRatio(overrideMinSize, mDefaultAspectRatio); + } else { + // Calculate the default size using the display size and default min edge size. + defaultSize = getSizeForAspectRatio(mDefaultAspectRatio, + mDefaultMinSize, displayInfo.logicalWidth, displayInfo.logicalHeight); + } + + // Now that we have the default size, apply the snap fraction if valid or position the + // bounds using the default gravity. + if (snapFraction != INVALID_SNAP_FRACTION) { + defaultBounds.set(0, 0, defaultSize.getWidth(), defaultSize.getHeight()); + final Rect movementBounds = getMovementBounds(defaultBounds); + mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction); } else { - final Rect insetBounds = new Rect(); - getInsetBounds(insetBounds); - final DisplayInfo displayInfo = mPipBoundsState.getDisplayInfo(); - final Size defaultSize; - final Size overrideMinSize = mPipBoundsState.getOverrideMinSize(); - if (overrideMinSize != null) { - // The override minimal size is set, use that as the default size making sure it's - // adjusted to the aspect ratio. - defaultSize = adjustSizeToAspectRatio(overrideMinSize, mDefaultAspectRatio); - } else { - // Calculate the default size using the display size and default min edge size. - defaultSize = getSizeForAspectRatio(mDefaultAspectRatio, - mDefaultMinSize, displayInfo.logicalWidth, displayInfo.logicalHeight); - } Gravity.apply(mDefaultStackGravity, defaultSize.getWidth(), defaultSize.getHeight(), insetBounds, 0, Math.max( mPipBoundsState.isImeShowing() ? mPipBoundsState.getImeHeight() : 0, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java index 53aa61477483..4493d38d2144 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -76,6 +76,8 @@ public final class PipBoundsState { private int mImeHeight; private boolean mIsShelfShowing; private int mShelfHeight; + /** Whether the user has resized the PIP manually. */ + private boolean mHasUserResizedPip; private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable BiConsumer<Boolean, Integer> mOnShelfVisibilityChangeCallback; @@ -189,8 +191,8 @@ public final class PipBoundsState { } /** Save the reentry state to restore to when re-entering PIP mode. */ - public void saveReentryState(@NonNull Rect bounds, float fraction) { - mPipReentryState = new PipReentryState(new Size(bounds.width(), bounds.height()), fraction); + public void saveReentryState(Size size, float fraction) { + mPipReentryState = new PipReentryState(size, fraction); } /** Returns the saved reentry state. */ @@ -205,6 +207,7 @@ public final class PipBoundsState { mLastPipComponentName = lastPipComponentName; if (changed) { clearReentryState(); + setHasUserResizedPip(false); } } @@ -329,6 +332,16 @@ public final class PipBoundsState { return mShelfHeight; } + /** Returns whether the user has resized the PIP. */ + public boolean hasUserResizedPip() { + return mHasUserResizedPip; + } + + /** Set whether the user has resized the PIP. */ + public void setHasUserResizedPip(boolean hasUserResizedPip) { + mHasUserResizedPip = hasUserResizedPip; + } + /** * Registers a callback when the minimal size of PIP that is set by the app changes. */ @@ -397,15 +410,15 @@ public final class PipBoundsState { static final class PipReentryState { private static final String TAG = PipReentryState.class.getSimpleName(); - private final @NonNull Size mSize; + private final @Nullable Size mSize; private final float mSnapFraction; - PipReentryState(@NonNull Size size, float snapFraction) { + PipReentryState(@Nullable Size size, float snapFraction) { mSize = size; mSnapFraction = snapFraction; } - @NonNull + @Nullable Size getSize() { return mSize; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java new file mode 100644 index 000000000000..8d9ad4d1b96c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java @@ -0,0 +1,106 @@ +/* + * 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.pip; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; +import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.annotation.Nullable; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.RemoteAction; +import android.content.pm.ParceledListSlice; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.view.WindowManager; + +/** + * Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into + * PiP menu when certain events happen (task appear/vanish, PiP move, etc.) + */ +public interface PipMenuController { + + String MENU_WINDOW_TITLE = "PipMenuView"; + + /** + * Called when + * {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)} + * is called. + */ + void attach(SurfaceControl leash); + + /** + * Called when + * {@link PipTaskOrganizer#onTaskVanished(RunningTaskInfo)} is called. + */ + void detach(); + + /** + * Check if menu is visible or not. + */ + boolean isMenuVisible(); + + /** + * Show the PIP menu. + */ + void showMenu(); + + /** + * Given a set of actions, update the menu. + */ + void setAppActions(ParceledListSlice<RemoteAction> appActions); + + /** + * Resize the PiP menu with the given bounds. The PiP SurfaceControl is given if there is a + * need to synchronize the movements on the same frame as PiP. + */ + default void resizePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) {} + + /** + * Move the PiP menu with the given bounds. The PiP SurfaceControl is given if there is a + * need to synchronize the movements on the same frame as PiP. + */ + default void movePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) {} + + /** + * Update the PiP menu with the given bounds for re-layout purposes. + */ + default void updateMenuBounds(Rect destinationBounds) {} + + /** + * Returns a default LayoutParams for the PIP Menu. + * @param width the PIP stack width. + * @param height the PIP stack height. + */ + default WindowManager.LayoutParams getPipMenuLayoutParams(String title, int width, int height) { + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(width, height, + TYPE_APPLICATION_OVERLAY, + FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY | FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT); + lp.privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY; + lp.setTitle(title); + return lp; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 7cc2a419354e..9081783eeab8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -30,6 +30,7 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; @@ -63,7 +64,6 @@ import com.android.internal.os.SomeArgs; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.pip.phone.PipMenuActivityController; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.pip.phone.PipUpdateThread; import com.android.wm.shell.splitscreen.SplitScreen; @@ -135,8 +135,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final Handler mUpdateHandler; private final PipBoundsState mPipBoundsState; private final PipBoundsAlgorithm mPipBoundsAlgorithm; - // TODO(b/172286265): Remove dependency on .pip.PHONE.PipMenuActivityController - private final PipMenuActivityController mMenuActivityController; + private final @NonNull PipMenuController mPipMenuController; private final PipAnimationController mPipAnimationController; private final PipUiEventLogger mPipUiEventLoggerLogger; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); @@ -264,7 +263,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public PipTaskOrganizer(Context context, @NonNull PipBoundsState pipBoundsState, @NonNull PipBoundsAlgorithm boundsHandler, - PipMenuActivityController menuActivityController, + @NonNull PipMenuController pipMenuController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, Optional<SplitScreen> splitScreenOptional, @NonNull DisplayController displayController, @@ -274,7 +273,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks); mPipBoundsState = pipBoundsState; mPipBoundsAlgorithm = boundsHandler; - mMenuActivityController = menuActivityController; + mPipMenuController = pipMenuController; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); mSurfaceTransactionHelper = surfaceTransactionHelper; @@ -501,9 +500,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mOnDisplayIdChangeCallback.accept(info.displayId); } - if (mMenuActivityController != null) { - mMenuActivityController.onTaskAppeared(); - } + mPipMenuController.attach(leash); if (mShouldIgnoreEnteringPipTransition) { final Rect destinationBounds = mPipBoundsState.getBounds(); @@ -674,9 +671,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPictureInPictureParams = null; mState = State.UNDEFINED; mPipUiEventLoggerLogger.setTaskInfo(null); - if (mMenuActivityController != null) { - mMenuActivityController.onTaskVanished(); - } + mPipMenuController.detach(); } @Override @@ -819,6 +814,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback); } + /** + * Animates resizing of the pinned stack given the duration and start bounds. + * This is used when the starting bounds is not the current PiP bounds. + */ + public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, + Consumer<Rect> updateBoundsCallback) { + if (mShouldDeferEnteringPip) { + Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); + return; + } + scheduleAnimateResizePip(fromBounds, toBounds, null /* sourceHintRect */, + TRANSITION_DIRECTION_SNAP_AFTER_RESIZE, duration, updateBoundsCallback); + } + private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback) { @@ -956,9 +965,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) .round(tx, mLeash, mState.isInPip()); - if (mMenuActivityController != null && mMenuActivityController.isMenuVisible()) { + if (mPipMenuController.isMenuVisible()) { runOnMainHandler(() -> - mMenuActivityController.resizePipMenu(mLeash, tx, destinationBounds)); + mPipMenuController.resizePipMenu(mLeash, tx, destinationBounds)); } else { tx.apply(); } @@ -982,9 +991,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds); - if (mMenuActivityController != null && mMenuActivityController.isMenuVisible()) { + if (mPipMenuController.isMenuVisible()) { runOnMainHandler(() -> - mMenuActivityController.movePipMenu(mLeash, tx, destinationBounds)); + mPipMenuController.movePipMenu(mLeash, tx, destinationBounds)); } else { tx.apply(); } @@ -1001,8 +1010,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { removePipImmediately(); return; - } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA - && mMenuActivityController != null) { + } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { // TODO: Synchronize this correctly in #applyEnterPipSyncTransaction finishResizeForMenu(destinationBounds); return; @@ -1015,13 +1023,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } private void finishResizeForMenu(Rect destinationBounds) { - if (mMenuActivityController == null) { - if (DEBUG) Log.d(TAG, "mMenuActivityController is null"); - return; - } runOnMainHandler(() -> { - mMenuActivityController.movePipMenu(null, null, destinationBounds); - mMenuActivityController.updateMenuBounds(destinationBounds); + mPipMenuController.movePipMenu(null, null, destinationBounds); + mPipMenuController.updateMenuBounds(destinationBounds); }); } @@ -1083,8 +1087,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Log.w(TAG, "Abort animation, invalid leash"); return; } + Rect baseBounds = direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE + ? mPipBoundsState.getBounds() : currentBounds; mPipAnimationController - .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect, direction) + .getAnimator(mLeash, baseBounds, currentBounds, destinationBounds, sourceHintRect, + direction) .setTransitionDirection(direction) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(durationMs) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActivityController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 1d5430008501..5db8f3d7ef40 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActivityController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -18,12 +18,6 @@ package com.android.wm.shell.pip.phone; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; -import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; -import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; -import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; import android.annotation.Nullable; @@ -33,7 +27,6 @@ import android.app.RemoteAction; import android.content.Context; import android.content.pm.ParceledListSlice; import android.graphics.Matrix; -import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.os.Debug; @@ -44,27 +37,26 @@ import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SyncRtSurfaceTransactionApplier; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; -import android.view.WindowManager; import android.view.WindowManagerGlobal; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipMediaController.ActionListener; +import com.android.wm.shell.pip.PipMenuController; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; /** - * Manages the PiP menu activity which can show menu options or a scrim. + * Manages the PiP menu view which can show menu options or a scrim. * * The current media session provides actions whenever there are no valid actions provided by the * current PiP activity. Otherwise, those actions always take precedence. */ -public class PipMenuActivityController { +public class PhonePipMenuController implements PipMenuController { private static final String TAG = "PipMenuActController"; - private static final String MENU_WINDOW_TITLE = "PipMenuView"; private static final boolean DEBUG = false; public static final int MENU_STATE_NONE = 0; @@ -124,7 +116,7 @@ public class PipMenuActivityController { } }; - public PipMenuActivityController(Context context, + public PhonePipMenuController(Context context, PipMediaController mediaController, SystemWindows systemWindows) { mContext = context; mMediaController = mediaController; @@ -138,20 +130,22 @@ public class PipMenuActivityController { /** * Attach the menu when the PiP task first appears. */ - public void onTaskAppeared() { + @Override + public void attach(SurfaceControl leash) { attachPipMenuView(); } /** * Detach the menu when the PiP task is gone. */ - public void onTaskVanished() { + @Override + public void detach() { hideMenu(); detachPipMenuView(); } - public void onPinnedStackAnimationEnded() { + void onPinnedStackAnimationEnded() { if (isMenuVisible()) { mPipMenuView.onPipAnimationEnded(); } @@ -163,7 +157,9 @@ public class PipMenuActivityController { detachPipMenuView(); } mPipMenuView = new PipMenuView(mContext, this); - mSystemWindows.addView(mPipMenuView, getPipMenuLayoutParams(0, 0), 0, SHELL_ROOT_LAYER_PIP); + mSystemWindows.addView(mPipMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); } private void detachPipMenuView() { @@ -181,9 +177,11 @@ public class PipMenuActivityController { * Updates the layout parameters of the menu. * @param destinationBounds New Menu bounds. */ + @Override public void updateMenuBounds(Rect destinationBounds) { mSystemWindows.updateViewLayout(mPipMenuView, - getPipMenuLayoutParams(destinationBounds.width(), destinationBounds.height())); + getPipMenuLayoutParams(MENU_WINDOW_TITLE, destinationBounds.width(), + destinationBounds.height())); } /** @@ -206,6 +204,16 @@ public class PipMenuActivityController { } /** + * When other components requests the menu controller directly to show the menu, we must + * first fire off the request to the other listeners who will then propagate the call + * back to the controller with the right parameters. + */ + @Override + public void showMenu() { + mListeners.forEach(Listener::onPipShowMenu); + } + + /** * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu * upon PiP window transition is finished. */ @@ -239,7 +247,9 @@ public class PipMenuActivityController { + " callers=\n" + Debug.getCallers(5, " ")); } - maybeCreateSyncApplier(); + if (!maybeCreateSyncApplier()) { + return; + } mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, showResizeHandle); @@ -248,6 +258,7 @@ public class PipMenuActivityController { /** * Move the PiP menu, which does a translation and possibly a scale transformation. */ + @Override public void movePipMenu(@Nullable SurfaceControl pipLeash, @Nullable SurfaceControl.Transaction t, Rect destinationBounds) { @@ -288,6 +299,7 @@ public class PipMenuActivityController { /** * Does an immediate window crop of the PiP menu. */ + @Override public void resizePipMenu(@Nullable SurfaceControl pipLeash, @Nullable SurfaceControl.Transaction t, Rect destinationBounds) { @@ -389,8 +401,9 @@ public class PipMenuActivityController { } /** - * Sets the menu actions to the actions provided by the current PiP activity. + * Sets the menu actions to the actions provided by the current PiP menu. */ + @Override public void setAppActions(ParceledListSlice<RemoteAction> appActions) { mAppActions = appActions; updateMenuActions(); @@ -404,10 +417,6 @@ public class PipMenuActivityController { mListeners.forEach(Listener::onPipDismiss); } - void onPipShowMenu() { - mListeners.forEach(Listener::onPipShowMenu); - } - /** * @return the best set of actions to show in the PiP menu. */ @@ -419,21 +428,6 @@ public class PipMenuActivityController { } /** - * Returns a default LayoutParams for the PIP Menu. - * @param width the PIP stack width. - * @param height the PIP stack height. - */ - public static WindowManager.LayoutParams getPipMenuLayoutParams(int width, int height) { - final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(width, height, - TYPE_APPLICATION_OVERLAY, - FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY | FLAG_NOT_TOUCHABLE, - PixelFormat.TRANSLUCENT); - lp.privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY; - lp.setTitle(MENU_WINDOW_TITLE); - return lp; - } - - /** * Updates the PiP menu with the best set of actions provided. */ private void updateMenuActions() { @@ -519,7 +513,7 @@ public class PipMenuActivityController { } } - public void dump(PrintWriter pw, String prefix) { + void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); pw.println(innerPrefix + "mMenuState=" + mMenuState); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java index f153aa5a1beb..7194fc70025c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -20,15 +20,18 @@ import android.content.Context; import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; -import android.os.Handler; import android.os.RemoteException; import android.view.MagnificationSpec; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import androidx.annotation.BinderThread; + import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; @@ -40,8 +43,7 @@ import java.util.List; * Expose the touch actions to accessibility as if this object were a window with a single view. * That pseudo-view exposes all of the actions this object can perform. */ -public class PipAccessibilityInteractionConnection - extends IAccessibilityInteractionConnection.Stub { +public class PipAccessibilityInteractionConnection { public interface AccessibilityCallbacks { void onAccessibilityShowMenu(); @@ -50,14 +52,15 @@ public class PipAccessibilityInteractionConnection private static final long ACCESSIBILITY_NODE_ID = 1; private List<AccessibilityNodeInfo> mAccessibilityNodeInfoList; - private Context mContext; - private Handler mHandler; + private final Context mContext; + private final ShellExecutor mShellMainExcutor; private final @NonNull PipBoundsState mPipBoundsState; - private PipMotionHelper mMotionHelper; - private PipTaskOrganizer mTaskOrganizer; - private PipSnapAlgorithm mSnapAlgorithm; - private Runnable mUpdateMovementBoundCallback; - private AccessibilityCallbacks mCallbacks; + private final PipMotionHelper mMotionHelper; + private final PipTaskOrganizer mTaskOrganizer; + private final PipSnapAlgorithm mSnapAlgorithm; + private final Runnable mUpdateMovementBoundCallback; + private final AccessibilityCallbacks mCallbacks; + private final IAccessibilityInteractionConnection mConnectionImpl; private final Rect mNormalBounds = new Rect(); private final Rect mExpandedBounds = new Rect(); @@ -69,19 +72,23 @@ public class PipAccessibilityInteractionConnection @NonNull PipBoundsState pipBoundsState, PipMotionHelper motionHelper, PipTaskOrganizer taskOrganizer, PipSnapAlgorithm snapAlgorithm, AccessibilityCallbacks callbacks, Runnable updateMovementBoundCallback, - Handler handler) { + ShellExecutor shellMainExcutor) { mContext = context; - mHandler = handler; + mShellMainExcutor = shellMainExcutor; mPipBoundsState = pipBoundsState; mMotionHelper = motionHelper; mTaskOrganizer = taskOrganizer; mSnapAlgorithm = snapAlgorithm; mUpdateMovementBoundCallback = updateMovementBoundCallback; mCallbacks = callbacks; + mConnectionImpl = new PipAccessibilityInteractionConnectionImpl(); + } + + public void register(AccessibilityManager am) { + am.setPictureInPictureActionReplacingConnection(mConnectionImpl); } - @Override - public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, + private void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) { @@ -94,8 +101,7 @@ public class PipAccessibilityInteractionConnection } } - @Override - public void performAccessibilityAction(long accessibilityNodeId, int action, + private void performAccessibilityAction(long accessibilityNodeId, int action, Bundle arguments, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid) { @@ -115,9 +121,7 @@ public class PipAccessibilityInteractionConnection } else { switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: - mHandler.post(() -> { - mCallbacks.onAccessibilityShowMenu(); - }); + mCallbacks.onAccessibilityShowMenu(); result = true; break; case AccessibilityNodeInfo.ACTION_DISMISS: @@ -172,8 +176,7 @@ public class PipAccessibilityInteractionConnection }); } - @Override - public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, + private void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { @@ -185,8 +188,7 @@ public class PipAccessibilityInteractionConnection } } - @Override - public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, + private void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { @@ -198,8 +200,7 @@ public class PipAccessibilityInteractionConnection } } - @Override - public void findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion, + private void findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { // We have no view that can take focus @@ -210,8 +211,7 @@ public class PipAccessibilityInteractionConnection } } - @Override - public void focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion, + private void focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { // We have no view that can take focus @@ -222,16 +222,6 @@ public class PipAccessibilityInteractionConnection } } - @Override - public void clearAccessibilityFocus() { - // We should not be here. - } - - @Override - public void notifyOutsideTouch() { - // Do nothing. - } - /** * Update the normal and expanded bounds so they can be used for Resize. */ @@ -271,4 +261,95 @@ public class PipAccessibilityInteractionConnection mAccessibilityNodeInfoList.add(info); return mAccessibilityNodeInfoList; } + + @BinderThread + private class PipAccessibilityInteractionConnectionImpl + extends IAccessibilityInteractionConnection.Stub { + @Override + public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, + Region bounds, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec, + Bundle arguments) throws RemoteException { + mShellMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this + .findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, bounds, + interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec, arguments); + }); + } + + @Override + public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, + Region bounds, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mShellMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByViewId( + accessibilityNodeId, viewId, bounds, interactionId, callback, flags, + interrogatingPid, interrogatingTid, spec); + }); + } + + @Override + public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, + Region bounds, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mShellMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByText( + accessibilityNodeId, text, bounds, interactionId, callback, flags, + interrogatingPid, interrogatingTid, spec); + }); + } + + @Override + public void findFocus(long accessibilityNodeId, int focusType, Region bounds, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mShellMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.findFocus(accessibilityNodeId, focusType, + bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec); + }); + } + + @Override + public void focusSearch(long accessibilityNodeId, int direction, Region bounds, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mShellMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.focusSearch(accessibilityNodeId, + direction, + bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec); + }); + } + + @Override + public void performAccessibilityAction(long accessibilityNodeId, int action, + Bundle arguments, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid) throws RemoteException { + mShellMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.performAccessibilityAction( + accessibilityNodeId, action, arguments, interactionId, callback, flags, + interrogatingPid, interrogatingTid); + }); + } + + @Override + public void clearAccessibilityFocus() throws RemoteException { + // Do nothing + } + + @Override + public void notifyOutsideTouch() throws RemoteException { + // Do nothing + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 598b5d9b5d30..4d2760259521 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -33,15 +33,16 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ParceledListSlice; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.util.Pair; +import android.util.Size; import android.util.Slog; import android.view.DisplayInfo; -import android.view.IPinnedStackController; import android.view.WindowManagerGlobal; import android.window.WindowContainerTransaction; @@ -52,7 +53,6 @@ import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.PipInputConsumer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; @@ -87,12 +87,11 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); private final Rect mTmpInsetBounds = new Rect(); - protected final Rect mReentryBounds = new Rect(); private boolean mIsInFixedRotation; private Consumer<Boolean> mPinnedStackAnimationRecentsCallback; - protected PipMenuActivityController mMenuController; + protected PhonePipMenuController mMenuController; protected PipTaskOrganizer mPipTaskOrganizer; protected PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener = new PipControllerPinnedStackListener(); @@ -106,6 +105,9 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac // Skip if we aren't in PIP or haven't actually entered PIP yet. We still need to update // the display layout in the bounds handler in this case. onDisplayRotationChangedNotInPip(mContext, toRotation); + // do not forget to update the movement bounds as well. + updateMovementBounds(mPipBoundsState.getNormalBounds(), true /* fromRotation */, + false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t); return; } // If there is an animation running (ie. from a shelf offset), then ensure that we calculate @@ -138,7 +140,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } }; - private DisplayController.OnDisplaysChangedListener mFixedRotationListener = + private final DisplayController.OnDisplaysChangedListener mFixedRotationListener = new DisplayController.OnDisplaysChangedListener() { @Override public void onFixedRotationStarted(int displayId, int newRotation) { @@ -163,63 +165,38 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac private class PipControllerPinnedStackListener extends PinnedStackListenerForwarder.PinnedStackListener { @Override - public void onListenerRegistered(IPinnedStackController controller) { - mMainExecutor.execute(() -> mTouchHandler.setPinnedStackController(controller)); - } - - @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - mMainExecutor.execute(() -> { - mPipBoundsState.setImeVisibility(imeVisible, imeHeight); - mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); - }); + mPipBoundsState.setImeVisibility(imeVisible, imeHeight); + mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); } @Override public void onMovementBoundsChanged(boolean fromImeAdjustment) { - mMainExecutor.execute(() -> updateMovementBounds(null /* toBounds */, + updateMovementBounds(null /* toBounds */, false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */, - null /* windowContainerTransaction */)); + null /* windowContainerTransaction */); } @Override public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - mMainExecutor.execute(() -> mMenuController.setAppActions(actions)); + mMenuController.setAppActions(actions); } @Override public void onActivityHidden(ComponentName componentName) { - mMainExecutor.execute(() -> { - if (componentName.equals(mPipBoundsState.getLastPipComponentName())) { - // The activity was removed, we don't want to restore to the reentry state - // saved for this component anymore. - mPipBoundsState.setLastPipComponentName(null); - } - }); - } - - @Override - public void onDisplayInfoChanged(DisplayInfo displayInfo) { - mMainExecutor.execute(() -> mPipBoundsState.setDisplayInfo(displayInfo)); - } - - @Override - public void onConfigurationChanged() { - mMainExecutor.execute(() -> { - mPipBoundsAlgorithm.onConfigurationChanged(mContext); - mTouchHandler.onConfigurationChanged(); - mPipBoundsState.onConfigurationChanged(); - }); + if (componentName.equals(mPipBoundsState.getLastPipComponentName())) { + // The activity was removed, we don't want to restore to the reentry state + // saved for this component anymore. + mPipBoundsState.setLastPipComponentName(null); + } } @Override public void onAspectRatioChanged(float aspectRatio) { // TODO(b/169373982): Remove this callback as it is redundant with PipTaskOrg params // change. - mMainExecutor.execute(() -> { - mPipBoundsState.setAspectRatio(aspectRatio); - mTouchHandler.onAspectRatioChanged(); - }); + mPipBoundsState.setAspectRatio(aspectRatio); + mTouchHandler.onAspectRatioChanged(); } } @@ -229,7 +206,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, PipMediaController pipMediaController, - PipMenuActivityController pipMenuActivityController, + PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler, WindowManagerShellWrapper windowManagerShellWrapper, @@ -250,7 +227,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac mPipTaskOrganizer = pipTaskOrganizer; mMainExecutor = mainExecutor; mMediaController = pipMediaController; - mMenuController = pipMenuActivityController; + mMenuController = phonePipMenuController; mTouchHandler = pipTouchHandler; mAppOpsListener = pipAppOpsListener; mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), @@ -349,6 +326,15 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } @Override + public void onConfigurationChanged(Configuration newConfig) { + mMainExecutor.execute(() -> { + mPipBoundsAlgorithm.onConfigurationChanged(mContext); + mTouchHandler.onConfigurationChanged(); + mPipBoundsState.onConfigurationChanged(); + }); + } + + @Override public void onDensityOrFontScaleChanged() { mMainExecutor.execute(() -> { mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext); @@ -452,10 +438,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac @Override public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { if (isOutPipDirection(direction)) { - // Exiting PIP, save the reentry bounds to restore to when re-entering. - updateReentryBounds(pipBounds); - final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(mReentryBounds); - mPipBoundsState.saveReentryState(mReentryBounds, snapFraction); + // Exiting PIP, save the reentry state to restore to when re-entering. + saveReentryState(pipBounds); } // Disable touches while the animation is running mTouchHandler.setTouchEnabled(false); @@ -464,14 +448,16 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } } - /** - * Update the bounds used to save the re-entry size and snap fraction when exiting PIP. - */ - public void updateReentryBounds(Rect bounds) { - final Rect reentryBounds = mTouchHandler.getUserResizeBounds(); - float snapFraction = mPipBoundsAlgorithm.getSnapFraction(bounds); - mPipBoundsAlgorithm.applySnapFraction(reentryBounds, snapFraction); - mReentryBounds.set(reentryBounds); + /** Save the state to restore to on re-entry. */ + public void saveReentryState(Rect pipBounds) { + float snapFraction = mPipBoundsAlgorithm.getSnapFraction(pipBounds); + if (mPipBoundsState.hasUserResizedPip()) { + final Rect reentryBounds = mTouchHandler.getUserResizeBounds(); + final Size reentrySize = new Size(reentryBounds.width(), reentryBounds.height()); + mPipBoundsState.saveReentryState(reentrySize, snapFraction); + } else { + mPipBoundsState.saveReentryState(null /* bounds */, snapFraction); + } } /** @@ -547,7 +533,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac * * @return {@code true} if internal {@link DisplayInfo} is rotated, {@code false} otherwise. */ - public boolean onDisplayRotationChanged(Context context, Rect outBounds, Rect oldBounds, + private boolean onDisplayRotationChanged(Context context, Rect outBounds, Rect oldBounds, Rect outInsetBounds, int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) { // Bail early if the event is not sent to current {@link #mDisplayInfo} @@ -632,7 +618,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac public static PipController create(Context context, DisplayController displayController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipMediaController pipMediaController, - PipMenuActivityController pipMenuActivityController, PipTaskOrganizer pipTaskOrganizer, + PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { @@ -641,7 +627,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, - pipBoundsState, pipMediaController, pipMenuActivityController, pipTaskOrganizer, + pipBoundsState, pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTouchHandler, windowManagerShellWrapper, taskStackListener, mainExecutor); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java index 87ddb181dcce..0c64c8ca9b64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/PipInputConsumer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.wm.shell.common; +package com.android.wm.shell.pip.phone; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.WindowManager.INPUT_CONSUMER_PIP; -import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION; import android.os.Binder; import android.os.IBinder; @@ -30,7 +28,6 @@ import android.view.Choreographer; import android.view.IWindowManager; import android.view.InputChannel; import android.view.InputEvent; -import android.view.WindowManagerGlobal; import java.io.PrintWriter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index 6e3cd8e9aa09..2e10fc93cafb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -23,9 +23,9 @@ import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTR import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; -import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; -import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_FULL; -import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_NONE; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -105,7 +105,7 @@ public class PipMenuView extends FrameLayout { private int mBetweenActionPaddingLand; private AnimatorSet mMenuContainerAnimator; - private PipMenuActivityController mController; + private PhonePipMenuController mController; private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @@ -127,7 +127,7 @@ public class PipMenuView extends FrameLayout { protected View mTopEndContainer; protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; - public PipMenuView(Context context, PipMenuActivityController controller) { + public PipMenuView(Context context, PhonePipMenuController controller) { super(context, null, 0); mContext = context; mController = controller; @@ -182,7 +182,7 @@ public class PipMenuView extends FrameLayout { @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) { - mController.onPipShowMenu(); + mController.showMenu(); } return super.performAccessibilityAction(host, action, args); } @@ -475,7 +475,7 @@ public class PipMenuView extends FrameLayout { final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS, Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null)); settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); - mContext.startActivityAsUser(settingsIntent, UserHandle.CURRENT); + mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index 903f7d773896..e32d3b955081 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -70,7 +70,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private final PipTaskOrganizer mPipTaskOrganizer; private @NonNull PipBoundsState mPipBoundsState; - private PipMenuActivityController mMenuController; + private PhonePipMenuController mMenuController; private PipSnapAlgorithm mSnapAlgorithm; private final Handler mMainHandler = new Handler(Looper.getMainLooper()); @@ -162,7 +162,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, }; public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, - PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController menuController, + PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, FloatingContentCoordinator floatingContentCoordinator) { mContext = context; mPipTaskOrganizer = pipTaskOrganizer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipPinchResizingAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipPinchResizingAlgorithm.java new file mode 100644 index 000000000000..28cbe35745a9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipPinchResizingAlgorithm.java @@ -0,0 +1,123 @@ +/* + * 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.pip.phone; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Helper class to calculate the new size given two-fingers pinch to resize. + */ +public class PipPinchResizingAlgorithm { + private static final Rect TMP_RECT = new Rect(); + /** + * Given inputs and requirements and current PiP bounds, return the new size. + * + * @param x0 x-coordinate of the primary input. + * @param y0 y-coordinate of the primary input. + * @param x1 x-coordinate of the secondary input. + * @param y1 y-coordinate of the secondary input. + * @param downx0 x-coordinate of the original down point of the primary input. + * @param downy0 y-coordinate of the original down ponit of the primary input. + * @param downx1 x-coordinate of the original down point of the secondary input. + * @param downy1 y-coordinate of the original down point of the secondary input. + * @param currentPipBounds current PiP bounds. + * @param minVisibleWidth minimum visible width. + * @param minVisibleHeight minimum visible height. + * @param maxSize max size. + * @return The new resized PiP bounds, sharing the same center. + */ + public static Rect pinchResize(float x0, float y0, float x1, float y1, + float downx0, float downy0, float downx1, float downy1, Rect currentPipBounds, + int minVisibleWidth, int minVisibleHeight, Point maxSize) { + + int width = currentPipBounds.width(); + int height = currentPipBounds.height(); + int left = currentPipBounds.left; + int top = currentPipBounds.top; + int right = currentPipBounds.right; + int bottom = currentPipBounds.bottom; + final float aspect = (float) width / (float) height; + final int widthDelta = Math.round(Math.abs(x0 - x1) - Math.abs(downx0 - downx1)); + final int heightDelta = Math.round(Math.abs(y0 - y1) - Math.abs(downy0 - downy1)); + + width = Math.max(minVisibleWidth, Math.min(width + widthDelta, maxSize.x)); + height = Math.max(minVisibleHeight, Math.min(height + heightDelta, maxSize.y)); + + // Calculate 2 rectangles fulfilling all requirements for either X or Y being the major + // drag axis. What ever is producing the bigger rectangle will be chosen. + int width1; + int width2; + int height1; + int height2; + if (aspect > 1.0f) { + // Assuming that the width is our target we calculate the height. + width1 = Math.max(minVisibleWidth, Math.min(maxSize.x, width)); + height1 = Math.round((float) width1 / aspect); + if (height1 < minVisibleHeight) { + // If the resulting height is too small we adjust to the minimal size. + height1 = minVisibleHeight; + width1 = Math.max(minVisibleWidth, + Math.min(maxSize.x, Math.round((float) height1 * aspect))); + } + // Assuming that the height is our target we calculate the width. + height2 = Math.max(minVisibleHeight, Math.min(maxSize.y, height)); + width2 = Math.round((float) height2 * aspect); + if (width2 < minVisibleWidth) { + // If the resulting width is too small we adjust to the minimal size. + width2 = minVisibleWidth; + height2 = Math.max(minVisibleHeight, + Math.min(maxSize.y, Math.round((float) width2 / aspect))); + } + } else { + // Assuming that the width is our target we calculate the height. + width1 = Math.max(minVisibleWidth, Math.min(maxSize.x, width)); + height1 = Math.round((float) width1 * aspect); + if (height1 < minVisibleHeight) { + // If the resulting height is too small we adjust to the minimal size. + height1 = minVisibleHeight; + width1 = Math.max(minVisibleWidth, + Math.min(maxSize.x, Math.round((float) height1 / aspect))); + } + // Assuming that the height is our target we calculate the width. + height2 = Math.max(minVisibleHeight, Math.min(maxSize.y, height)); + width2 = Math.round((float) height2 / aspect); + if (width2 < minVisibleWidth) { + // If the resulting width is too small we adjust to the minimal size. + width2 = minVisibleWidth; + height2 = Math.max(minVisibleHeight, + Math.min(maxSize.y, Math.round((float) width2 * aspect))); + } + } + + // Use the bigger of the two rectangles if the major change was positive, otherwise + // do the opposite. + final boolean grows = width > (right - left) || height > (bottom - top); + if (grows == (width1 * height1 > width2 * height2)) { + width = width1; + height = height1; + } else { + width = width2; + height = height2; + } + + TMP_RECT.set(currentPipBounds.centerX() - width / 2, + currentPipBounds.centerY() - height / 2, + currentPipBounds.centerX() + width / 2, + currentPipBounds.centerY() + height / 2); + return TMP_RECT; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index e5a49c430d82..02f6231c6ecf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -39,7 +39,6 @@ import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; import android.view.MotionEvent; -import android.view.ScaleGestureDetector; import android.view.ViewConfiguration; import androidx.annotation.VisibleForTesting; @@ -62,19 +61,21 @@ import java.util.function.Function; public class PipResizeGestureHandler { private static final String TAG = "PipResizeGestureHandler"; - private static final float PINCH_THRESHOLD = 0.05f; - private static final float STARTING_SCALE_FACTOR = 1.0f; + private static final int PINCH_RESIZE_SNAP_DURATION = 250; private final Context mContext; private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipMotionHelper mMotionHelper; private final PipBoundsState mPipBoundsState; + private final PipTaskOrganizer mPipTaskOrganizer; + private final PhonePipMenuController mPhonePipMenuController; + private final PipUiEventLogger mPipUiEventLogger; private final int mDisplayId; private final Executor mMainExecutor; - private final ScaleGestureDetector mScaleGestureDetector; private final Region mTmpRegion = new Region(); private final PointF mDownPoint = new PointF(); + private final PointF mDownSecondaryPoint = new PointF(); private final Point mMaxSize = new Point(); private final Point mMinSize = new Point(); private final Rect mLastResizeBounds = new Rect(); @@ -88,23 +89,27 @@ public class PipResizeGestureHandler { private final Rect mDisplayBounds = new Rect(); private final Function<Rect, Rect> mMovementBoundsSupplier; private final Runnable mUpdateMovementBoundsRunnable; + private final Handler mHandler; private int mDelta; private float mTouchSlop; + private boolean mAllowGesture; private boolean mIsAttached; private boolean mIsEnabled; private boolean mEnablePinchResize; private boolean mIsSysUiStateValid; + // For drag-resize private boolean mThresholdCrossed; + // For pinch-resize + private boolean mThresholdCrossed0; + private boolean mThresholdCrossed1; private boolean mUsingPinchToZoom = false; - private float mScaleFactor = STARTING_SCALE_FACTOR; + int mFirstIndex = -1; + int mSecondIndex = -1; private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; - private PipTaskOrganizer mPipTaskOrganizer; - private PipMenuActivityController mPipMenuActivityController; - private PipUiEventLogger mPipUiEventLogger; private int mCtrlType; @@ -112,7 +117,7 @@ public class PipResizeGestureHandler { PipBoundsState pipBoundsState, PipMotionHelper motionHelper, PipTaskOrganizer pipTaskOrganizer, Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, PipUiEventLogger pipUiEventLogger, - PipMenuActivityController menuActivityController) { + PhonePipMenuController menuActivityController) { mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = context.getMainExecutor(); @@ -122,68 +127,13 @@ public class PipResizeGestureHandler { mPipTaskOrganizer = pipTaskOrganizer; mMovementBoundsSupplier = movementBoundsSupplier; mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; - mPipMenuActivityController = menuActivityController; + mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; + mHandler = new Handler(Looper.getMainLooper()); context.getDisplay().getRealSize(mMaxSize); reloadResources(); - mScaleGestureDetector = new ScaleGestureDetector(context, - new ScaleGestureDetector.OnScaleGestureListener() { - @Override - public boolean onScale(ScaleGestureDetector detector) { - mScaleFactor *= detector.getScaleFactor(); - - if (!mThresholdCrossed - && (mScaleFactor > (STARTING_SCALE_FACTOR + PINCH_THRESHOLD) - || mScaleFactor < (STARTING_SCALE_FACTOR - PINCH_THRESHOLD))) { - mThresholdCrossed = true; - mInputMonitor.pilferPointers(); - } - if (mThresholdCrossed) { - int height = Math.min(mMaxSize.y, Math.max(mMinSize.y, - (int) (mScaleFactor * mLastDownBounds.height()))); - int width = Math.min(mMaxSize.x, Math.max(mMinSize.x, - (int) (mScaleFactor * mLastDownBounds.width()))); - int top, bottom, left, right; - - if ((mCtrlType & CTRL_TOP) != 0) { - top = mLastDownBounds.bottom - height; - bottom = mLastDownBounds.bottom; - } else { - top = mLastDownBounds.top; - bottom = mLastDownBounds.top + height; - } - - if ((mCtrlType & CTRL_LEFT) != 0) { - left = mLastDownBounds.right - width; - right = mLastDownBounds.right; - } else { - left = mLastDownBounds.left; - right = mLastDownBounds.left + width; - } - - mLastResizeBounds.set(left, top, right, bottom); - mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, - mLastResizeBounds, - null); - } - return true; - } - - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - setCtrlTypeForPinchToZoom(); - return true; - } - - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - mScaleFactor = STARTING_SCALE_FACTOR; - finishResize(); - } - }); - mEnablePinchResize = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, PIP_PINCH_RESIZE, @@ -274,7 +224,7 @@ public class PipResizeGestureHandler { if (ev instanceof MotionEvent) { if (mUsingPinchToZoom) { - mScaleGestureDetector.onTouchEvent((MotionEvent) ev); + onPinchResize((MotionEvent) ev); } else { onDragCornerResize((MotionEvent) ev); } @@ -282,6 +232,13 @@ public class PipResizeGestureHandler { } /** + * Checks if there is currently an on-going gesture, either drag-resize or pinch-resize. + */ + public boolean hasOngoingGesture() { + return mCtrlType != CTRL_NONE || mUsingPinchToZoom; + } + + /** * Check whether the current x,y coordinate is within the region in which drag-resize should * start. * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which @@ -295,7 +252,7 @@ public class PipResizeGestureHandler { * |_|_|_________|_|_| * |_|_| |_|_| */ - public boolean isWithinTouchRegion(int x, int y) { + public boolean isWithinDragResizeRegion(int x, int y) { final Rect currentPipBounds = mPipBoundsState.getBounds(); if (currentPipBounds == null) { return false; @@ -327,15 +284,14 @@ public class PipResizeGestureHandler { if (isInValidSysUiState()) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: - // Always pass the DOWN event to the ScaleGestureDetector - mScaleGestureDetector.onTouchEvent(ev); - if (isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY())) { + if (isWithinDragResizeRegion((int) ev.getRawX(), (int) ev.getRawY())) { return true; } break; case MotionEvent.ACTION_POINTER_DOWN: if (mEnablePinchResize && ev.getPointerCount() == 2) { + onPinchResize(ev); mUsingPinchToZoom = true; return true; } @@ -348,33 +304,11 @@ public class PipResizeGestureHandler { return false; } - private void setCtrlTypeForPinchToZoom() { - final Rect currentPipBounds = mPipBoundsState.getBounds(); - mLastDownBounds.set(mPipBoundsState.getBounds()); - - Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); - mDisplayBounds.set(movementBounds.left, - movementBounds.top, - movementBounds.right + currentPipBounds.width(), - movementBounds.bottom + currentPipBounds.height()); - - if (currentPipBounds.left == mDisplayBounds.left) { - mCtrlType |= CTRL_RIGHT; - } else { - mCtrlType |= CTRL_LEFT; - } - - if (currentPipBounds.top > mDisplayBounds.top + mDisplayBounds.height()) { - mCtrlType |= CTRL_TOP; - } else { - mCtrlType |= CTRL_BOTTOM; - } - } - private void setCtrlType(int x, int y) { final Rect currentPipBounds = mPipBoundsState.getBounds(); Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); + mDisplayBounds.set(movementBounds.left, movementBounds.top, movementBounds.right + currentPipBounds.width(), @@ -408,6 +342,79 @@ public class PipResizeGestureHandler { return mIsSysUiStateValid; } + private void onPinchResize(MotionEvent ev) { + int action = ev.getActionMasked(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mFirstIndex = -1; + mSecondIndex = -1; + finishResize(); + } + + if (ev.getPointerCount() != 2) { + return; + } + + if (action == MotionEvent.ACTION_POINTER_DOWN) { + if (mFirstIndex == -1 && mSecondIndex == -1) { + mFirstIndex = 0; + mSecondIndex = 1; + mLastResizeBounds.setEmpty(); + mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); + mDownSecondaryPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); + + mLastResizeBounds.setEmpty(); + mLastDownBounds.set(mPipBoundsState.getBounds()); + } + } + + if (action == MotionEvent.ACTION_MOVE) { + if (mFirstIndex == -1 || mSecondIndex == -1) { + return; + } + + float x0 = ev.getRawX(mFirstIndex); + float y0 = ev.getRawY(mFirstIndex); + float x1 = ev.getRawX(mSecondIndex); + float y1 = ev.getRawY(mSecondIndex); + + double hypot0 = Math.hypot(x0 - mDownPoint.x, y0 - mDownPoint.y); + double hypot1 = Math.hypot(x1 - mDownSecondaryPoint.x, y1 - mDownSecondaryPoint.y); + // Capture inputs + if (hypot0 > mTouchSlop && !mThresholdCrossed0) { + mInputMonitor.pilferPointers(); + mThresholdCrossed0 = true; + // Reset the down to begin resizing from this point + mDownPoint.set(x0, y0); + } + if (hypot1 > mTouchSlop && !mThresholdCrossed1) { + mInputMonitor.pilferPointers(); + mThresholdCrossed1 = true; + // Reset the down to begin resizing from this point + mDownSecondaryPoint.set(x1, y1); + } + if (mThresholdCrossed0 || mThresholdCrossed1) { + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + + x0 = mThresholdCrossed0 ? x0 : mDownPoint.x; + y0 = mThresholdCrossed0 ? y0 : mDownPoint.y; + x1 = mThresholdCrossed1 ? x1 : mDownSecondaryPoint.x; + y1 = mThresholdCrossed1 ? y1 : mDownSecondaryPoint.y; + + final Rect currentPipBounds = mPipBoundsState.getBounds(); + mLastResizeBounds.set(PipPinchResizingAlgorithm.pinchResize(x0, y0, x1, y1, + mDownPoint.x, mDownPoint.y, mDownSecondaryPoint.x, mDownSecondaryPoint.y, + currentPipBounds, mMinSize.x, mMinSize.y, mMaxSize)); + + mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds, + null); + mPipBoundsState.setHasUserResizedPip(true); + } + } + } + private void onDragCornerResize(MotionEvent ev) { int action = ev.getActionMasked(); float x = ev.getX(); @@ -415,15 +422,15 @@ public class PipResizeGestureHandler { if (action == MotionEvent.ACTION_DOWN) { final Rect currentPipBounds = mPipBoundsState.getBounds(); mLastResizeBounds.setEmpty(); - mAllowGesture = isInValidSysUiState() && isWithinTouchRegion((int) x, (int) y); + mAllowGesture = isInValidSysUiState() && isWithinDragResizeRegion((int) x, (int) y); if (mAllowGesture) { setCtrlType((int) x, (int) y); mDownPoint.set(x, y); mLastDownBounds.set(mPipBoundsState.getBounds()); } if (!currentPipBounds.contains((int) ev.getX(), (int) ev.getY()) - && mPipMenuActivityController.isMenuVisible()) { - mPipMenuActivityController.hideMenu(); + && mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); } } else if (mAllowGesture) { @@ -442,9 +449,9 @@ public class PipResizeGestureHandler { mInputMonitor.pilferPointers(); } if (mThresholdCrossed) { - if (mPipMenuActivityController.isMenuVisible()) { - mPipMenuActivityController.hideMenuWithoutResize(); - mPipMenuActivityController.hideMenu(); + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenuWithoutResize(); + mPhonePipMenuController.hideMenu(); } final Rect currentPipBounds = mPipBoundsState.getBounds(); mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, @@ -456,6 +463,7 @@ public class PipResizeGestureHandler { true /* useCurrentSize */); mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds, null); + mPipBoundsState.setHasUserResizedPip(true); } break; case MotionEvent.ACTION_UP: @@ -468,15 +476,30 @@ public class PipResizeGestureHandler { private void finishResize() { if (!mLastResizeBounds.isEmpty()) { - mUserResizeBounds.set(mLastResizeBounds); - mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, - (Rect bounds) -> { - new Handler(Looper.getMainLooper()).post(() -> { - mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundsRunnable.run(); - resetState(); + final Runnable callback = () -> { + mUserResizeBounds.set(mLastResizeBounds); + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }; + + // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped + // position correctly. Drag-resize does not need to move, so just finalize resize. + if (mUsingPinchToZoom) { + final Rect startBounds = new Rect(mLastResizeBounds); + mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, + mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds())); + mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, + PINCH_RESIZE_SNAP_DURATION, + (Rect rect) -> { + mHandler.post(callback); }); - }); + } else { + mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, + (Rect bounds) -> { + mHandler.post(callback); + }); + } mPipUiEventLogger.log( PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index a78c4ecdb39f..33439a412b4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -18,9 +18,9 @@ package com.android.wm.shell.pip.phone; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; -import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; -import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_FULL; -import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_NONE; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -31,11 +31,8 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.Handler; -import android.os.RemoteException; import android.provider.DeviceConfig; -import android.util.Log; import android.util.Size; -import android.view.IPinnedStackController; import android.view.InputEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; @@ -47,6 +44,7 @@ import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -75,10 +73,9 @@ public class PipTouchHandler { private final PipDismissTargetHandler mPipDismissTargetHandler; private PipResizeGestureHandler mPipResizeGestureHandler; - private IPinnedStackController mPinnedStackController; private WeakReference<Consumer<Rect>> mPipExclusionBoundsChangeListener; - private final PipMenuActivityController mMenuController; + private final PhonePipMenuController mMenuController; private final AccessibilityManager mAccessibilityManager; private boolean mShowPipMenuOnAnimationEnd = false; @@ -89,7 +86,7 @@ public class PipTouchHandler { private boolean mEnableStash = true; // The reference inset bounds, used to determine the dismiss fraction - private Rect mInsetBounds = new Rect(); + private final Rect mInsetBounds = new Rect(); private int mExpandedShortestEdgeSize; // Used to workaround an issue where the WM rotation happens before we are notified, allowing @@ -97,7 +94,8 @@ public class PipTouchHandler { private int mDeferResizeToNormalBoundsUntilRotation = -1; private int mDisplayRotation; - private Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(); + private final PipAccessibilityInteractionConnection mConnection; // Behaviour states private int mMenuState = MENU_STATE_NONE; @@ -111,7 +109,6 @@ public class PipTouchHandler { private float mSavedSnapFraction = -1f; private boolean mSendingHoverAccessibilityEvents; private boolean mMovementWithinDismiss; - private PipAccessibilityInteractionConnection mConnection; // Touch state private final PipTouchState mTouchState; @@ -125,7 +122,7 @@ public class PipTouchHandler { /** * A listener for the PIP menu activity. */ - private class PipMenuListener implements PipMenuActivityController.Listener { + private class PipMenuListener implements PhonePipMenuController.Listener { @Override public void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback) { setMenuState(menuState, resize, callback); @@ -152,12 +149,13 @@ public class PipTouchHandler { @SuppressLint("InflateParams") public PipTouchHandler(Context context, - PipMenuActivityController menuController, + PhonePipMenuController menuController, PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, FloatingContentCoordinator floatingContentCoordinator, - PipUiEventLogger pipUiEventLogger) { + PipUiEventLogger pipUiEventLogger, + ShellExecutor shellMainExecutor) { // Initialize the Pip input consumer mContext = context; mAccessibilityManager = context.getSystemService(AccessibilityManager.class); @@ -188,7 +186,7 @@ public class PipTouchHandler { mFloatingContentCoordinator = floatingContentCoordinator; mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState, mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), - this::onAccessibilityShowMenu, this::updateMovementBounds, mHandler); + this::onAccessibilityShowMenu, this::updateMovementBounds, shellMainExecutor); mPipUiEventLogger = pipUiEventLogger; @@ -436,8 +434,11 @@ public class PipTouchHandler { * TODO Add appropriate description */ public void onRegistrationChanged(boolean isRegistered) { - mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered - ? mConnection : null); + if (isRegistered) { + mConnection.register(mAccessibilityManager); + } else { + mAccessibilityManager.setPictureInPictureActionReplacingConnection(null); + } if (!isRegistered && mTouchState.isUserInteracting()) { // If the input consumer is unregistered while the user is interacting, then we may not // get the final TOUCH_UP event, so clean up the dismiss target as well @@ -459,10 +460,6 @@ public class PipTouchHandler { if (!(inputEvent instanceof MotionEvent)) { return true; } - // Skip touch handling until we are bound to the controller - if (mPinnedStackController == null) { - return true; - } MotionEvent ev = (MotionEvent) inputEvent; if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) { @@ -473,6 +470,11 @@ public class PipTouchHandler { return true; } + if (mPipResizeGestureHandler.hasOngoingGesture()) { + mPipDismissTargetHandler.hideDismissTargetMaybe(); + return true; + } + if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting()) && mPipDismissTargetHandler.maybeConsumeMotionEvent(ev)) { // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event @@ -586,13 +588,6 @@ public class PipTouchHandler { } /** - * Sets the controller to update the system of changes from user interaction. - */ - void setPinnedStackController(IPinnedStackController controller) { - mPinnedStackController = controller; - } - - /** * Sets the menu visibility. */ private void setMenuState(int menuState, boolean resize, Runnable callback) { @@ -620,13 +615,9 @@ public class PipTouchHandler { // bounds which are now stale. In such a case we defer the animation to the // normal bounds until after the next onMovementBoundsChanged() call to get the // bounds in the new orientation - try { - int displayRotation = mPinnedStackController.getDisplayRotation(); - if (mDisplayRotation != displayRotation) { - mDeferResizeToNormalBoundsUntilRotation = displayRotation; - } - } catch (RemoteException e) { - Log.e(TAG, "Could not get display rotation from controller"); + int displayRotation = mContext.getDisplay().getRotation(); + if (mDisplayRotation != displayRotation) { + mDeferResizeToNormalBoundsUntilRotation = displayRotation; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java index 217150770084..5f2327ce98d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip.phone; import android.graphics.PointF; import android.os.Handler; import android.util.Log; +import android.view.Display; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; @@ -64,6 +65,7 @@ public class PipTouchState { private boolean mStartedDragging = false; private boolean mAllowDraggingOffscreen = false; private int mActivePointerId; + private int mLastTouchDisplayId = Display.INVALID_DISPLAY; public PipTouchState(ViewConfiguration viewConfig, Handler handler, Runnable doubleTapTimeoutCallback, Runnable hoverExitTimeoutCallback) { @@ -81,12 +83,14 @@ public class PipTouchState { mIsDragging = false; mStartedDragging = false; mIsUserInteracting = false; + mLastTouchDisplayId = Display.INVALID_DISPLAY; } /** * Processes a given touch event and updates the state. */ public void onTouchEvent(MotionEvent ev) { + mLastTouchDisplayId = ev.getDisplayId(); switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { if (!mAllowTouches) { @@ -266,6 +270,13 @@ public class PipTouchState { } /** + * @return Display ID of the last touch event. + */ + public int getLastTouchDisplayId() { + return mLastTouchDisplayId; + } + + /** * Sets whether touching is currently allowed. */ public void setAllowTouches(boolean allowTouches) { @@ -378,6 +389,7 @@ public class PipTouchState { pw.println(prefix + TAG); pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId); + pw.println(innerPrefix + "mLastTouchDisplayId=" + mLastTouchDisplayId); pw.println(innerPrefix + "mDownTouch=" + mDownTouch); pw.println(innerPrefix + "mDownDelta=" + mDownDelta); pw.println(innerPrefix + "mLastTouch=" + mLastTouch); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java index 56e97b91c9d2..0955056900f1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java @@ -36,9 +36,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ParceledListSlice; import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Rect; -import android.os.Debug; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -57,15 +55,14 @@ import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipTaskOrganizer; -import java.util.ArrayList; -import java.util.List; +import java.util.Objects; /** * Manages the picture-in-picture (PIP) UI and states. */ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback { - private static final String TAG = "PipController"; - static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String TAG = "TvPipController"; + static final boolean DEBUG = false; /** * Unknown or invalid state @@ -87,44 +84,21 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac private static final int TASK_ID_NO_PIP = -1; private static final int INVALID_RESOURCE_TYPE = -1; - public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1; - - /** - * PIPed activity is playing a media and it can be paused. - */ - static final int PLAYBACK_STATE_PLAYING = 0; - /** - * PIPed activity has a paused media and it can be played. - */ - static final int PLAYBACK_STATE_PAUSED = 1; - /** - * Users are unable to control PIPed activity's media playback. - */ - static final int PLAYBACK_STATE_UNAVAILABLE = 2; - - private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000; - - private int mSuspendPipResizingReason; - private final Context mContext; private final PipBoundsState mPipBoundsState; private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; + private final TvPipMenuController mTvPipMenuController; + private final PipNotification mPipNotification; private IActivityTaskManager mActivityTaskManager; private int mState = STATE_NO_PIP; - private int mResumeResizePinnedStackRunnableState = STATE_NO_PIP; private final Handler mHandler = new Handler(); - private List<Listener> mListeners = new ArrayList<>(); - private Rect mPipBounds; - private Rect mDefaultPipBounds = new Rect(); - private Rect mMenuModePipBounds; private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED; private int mPipTaskId = TASK_ID_NO_PIP; private int mPinnedStackId = INVALID_STACK_ID; private String[] mLastPackagesResourceGranted; - private PipNotification mPipNotification; private ParceledListSlice<RemoteAction> mCustomActions; private WindowManagerShellWrapper mWindowManagerShellWrapper; private int mResizeAnimationDuration; @@ -137,9 +111,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac private boolean mImeVisible; private int mImeHeightAdjustment; - private final Runnable mResizePinnedStackRunnable = - () -> resizePinnedStack(mResumeResizePinnedStackRunnableState); - private final Runnable mClosePipRunnable = () -> closePip(); + private final Runnable mClosePipRunnable = this::closePip; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -181,46 +153,33 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac PinnedStackListenerForwarder.PinnedStackListener { @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - mHandler.post(() -> { - mPipBoundsState.setImeVisibility(imeVisible, imeHeight); - if (mState == STATE_PIP) { - if (mImeVisible != imeVisible) { - if (imeVisible) { - // Save the IME height adjustment, and offset to not occlude the IME - mPipBounds.offset(0, -imeHeight); - mImeHeightAdjustment = imeHeight; - } else { - // Apply the inverse adjustment when the IME is hidden - mPipBounds.offset(0, mImeHeightAdjustment); - } - mImeVisible = imeVisible; - resizePinnedStack(STATE_PIP); + mPipBoundsState.setImeVisibility(imeVisible, imeHeight); + if (mState == STATE_PIP) { + if (mImeVisible != imeVisible) { + if (imeVisible) { + // Save the IME height adjustment, and offset to not occlude the IME + mPipBoundsState.getNormalBounds().offset(0, -imeHeight); + mImeHeightAdjustment = imeHeight; + } else { + // Apply the inverse adjustment when the IME is hidden + mPipBoundsState.getNormalBounds().offset(0, mImeHeightAdjustment); } + mImeVisible = imeVisible; + resizePinnedStack(STATE_PIP); } - }); + } } @Override public void onMovementBoundsChanged(boolean fromImeAdjustment) { - mHandler.post(() -> { - mTmpDisplayInfo.copyFrom(mPipBoundsState.getDisplayInfo()); - - mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds); - mPipBounds.set(mPipBoundsAlgorithm.getNormalBounds()); - if (mDefaultPipBounds.isEmpty()) { - mDefaultPipBounds.set(mPipBoundsAlgorithm.getDefaultBounds()); - } - }); + mTmpDisplayInfo.copyFrom(mPipBoundsState.getDisplayInfo()); + mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds); } @Override public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { mCustomActions = actions; - mHandler.post(() -> { - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onPipMenuActionsChanged(mCustomActions); - } - }); + mTvPipMenuController.setAppActions(mCustomActions); } } @@ -228,6 +187,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, + TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, PipNotification pipNotification, TaskStackListenerImpl taskStackListener, @@ -237,6 +197,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac mPipNotification = pipNotification; mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipMediaController = pipMediaController; + mTvPipMenuController = tvPipMenuController; + mTvPipMenuController.attachPipController(this); // Ensure that we have the display info in case we get calls to update the bounds // before the listener calls back final DisplayInfo displayInfo = new DisplayInfo(); @@ -249,8 +211,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac mPipTaskOrganizer.registerPipTransitionCallback(this); mActivityTaskManager = ActivityTaskManager.getService(); - addListener(mPipNotification); - final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_CLOSE); intentFilter.addAction(ACTION_MENU); @@ -289,9 +249,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac PipController.this.onActivityRestartAttempt(task, clearedTask); } }); - - // TODO(b/169395392) Refactor PipMenuActivity to PipMenuView - PipMenuActivity.setPipController(this); } private void loadConfigurationsAndApply(Configuration newConfig) { @@ -302,20 +259,17 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac return; } - Resources res = mContext.getResources(); - mMenuModePipBounds = Rect.unflattenFromString(res.getString( - R.string.pip_menu_bounds)); + final Rect menuBounds = Rect.unflattenFromString( + mContext.getResources().getString(R.string.pip_menu_bounds)); + mPipBoundsState.setExpandedBounds(menuBounds); - // Reset the PIP bounds and apply. PIP bounds can be changed by two reasons. - // 1. Configuration changed due to the language change (RTL <-> RTL) - // 2. SystemUI restarts after the crash - mPipBounds = mDefaultPipBounds; resizePinnedStack(getPinnedTaskInfo() == null ? STATE_NO_PIP : STATE_PIP); } /** * Updates the PIP per configuration changed. */ + @Override public void onConfigurationChanged(Configuration newConfig) { loadConfigurationsAndApply(newConfig); mPipNotification.onConfigurationChanged(mContext); @@ -359,9 +313,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac mPinnedStackId = INVALID_STACK_ID; } } - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onPipActivityClosed(); - } + mPipNotification.dismiss(); + mTvPipMenuController.hideMenu(); mHandler.removeCallbacks(mClosePipRunnable); } @@ -372,29 +325,33 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac if (DEBUG) Log.d(TAG, "movePipToFullscreen(), current state=" + getStateDescription()); mPipTaskId = TASK_ID_NO_PIP; - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onMoveToFullscreen(); - } + mTvPipMenuController.hideMenu(); + mPipNotification.dismiss(); + resizePinnedStack(STATE_NO_PIP); } private void onActivityPinned(String packageName) { - if (DEBUG) Log.d(TAG, "onActivityPinned()"); - - RootTaskInfo taskInfo = getPinnedTaskInfo(); + final RootTaskInfo taskInfo = getPinnedTaskInfo(); + if (DEBUG) Log.d(TAG, "onActivityPinned, task=" + taskInfo); if (taskInfo == null) { Log.w(TAG, "Cannot find pinned stack"); return; } - if (DEBUG) Log.d(TAG, "PINNED_STACK:" + taskInfo); + + // At this point PipBoundsState knows the correct aspect ratio for this pinned task, so we + // use PipBoundsAlgorithm to calculate the normal bounds for the task (PipBoundsAlgorithm + // will query PipBoundsState for the aspect ratio) and pass the bounds over to the + // PipBoundsState. + mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds()); + mPinnedStackId = taskInfo.taskId; mPipTaskId = taskInfo.childTaskIds[taskInfo.childTaskIds.length - 1]; + // Set state to STATE_PIP so we show it when the pinned stack animation ends. mState = STATE_PIP; mPipMediaController.onActivityPinned(); - for (int i = mListeners.size() - 1; i >= 0; i--) { - mListeners.get(i).onPipEntered(packageName); - } + mPipNotification.show(packageName); } private void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, @@ -434,45 +391,13 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } } if (getState() == STATE_PIP) { - if (mPipBounds != mDefaultPipBounds) { - mPipBounds = mDefaultPipBounds; + if (!Objects.equals(mPipBoundsState.getBounds(), mPipBoundsState.getNormalBounds())) { resizePinnedStack(STATE_PIP); } } } /** - * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called - * - * @param reason The reason for suspending resizing operations on the Pip. - */ - public void suspendPipResizing(int reason) { - if (DEBUG) { - Log.d(TAG, - "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); - } - - mSuspendPipResizingReason |= reason; - } - - /** - * Resumes resizing operation on the Pip that was previously suspended. - * - * @param reason The reason resizing operations on the Pip was suspended. - */ - public void resumePipResizing(int reason) { - if ((mSuspendPipResizingReason & reason) == 0) { - return; - } - if (DEBUG) { - Log.d(TAG, - "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); - } - mSuspendPipResizingReason &= ~reason; - mHandler.post(mResizePinnedStackRunnable); - } - - /** * Resize the Pip to the appropriate size for the input state. * * @param state In Pip state also used to determine the new size for the Pip. @@ -482,21 +407,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac Log.d(TAG, "resizePinnedStack() state=" + stateToName(state) + ", current state=" + getStateDescription(), new Exception()); } - - boolean wasStateNoPip = (mState == STATE_NO_PIP); - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onPipResizeAboutToStart(); - } - if (mSuspendPipResizingReason != 0) { - mResumeResizePinnedStackRunnableState = state; - if (DEBUG) { - Log.d(TAG, "resizePinnedStack() deferring" - + " mSuspendPipResizingReason=" + mSuspendPipResizingReason - + " mResumeResizePinnedStackRunnableState=" - + stateToName(mResumeResizePinnedStackRunnableState)); - } - return; - } + final boolean wasStateNoPip = (mState == STATE_NO_PIP); + mTvPipMenuController.hideMenu(); mState = state; final Rect newBounds; switch (mState) { @@ -509,11 +421,11 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } break; case STATE_PIP_MENU: - newBounds = mMenuModePipBounds; + newBounds = mPipBoundsState.getExpandedBounds(); break; case STATE_PIP: // fallthrough default: - newBounds = mPipBounds; + newBounds = mPipBoundsState.getNormalBounds(); break; } if (newBounds != null) { @@ -524,44 +436,17 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } /** - * @return the current state, or the pending state if the state change was previously suspended. + * @return the current state. */ private int getState() { - if (mSuspendPipResizingReason != 0) { - return mResumeResizePinnedStackRunnableState; - } return mState; } - /** - * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned - * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}. - */ private void showPipMenu() { if (DEBUG) Log.d(TAG, "showPipMenu(), current state=" + getStateDescription()); mState = STATE_PIP_MENU; - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onShowPipMenu(); - } - Intent intent = new Intent(mContext, PipMenuActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(PipMenuActivity.EXTRA_CUSTOM_ACTIONS, mCustomActions); - mContext.startActivity(intent); - } - - /** - * Adds a {@link Listener} to PipController. - */ - void addListener(Listener listener) { - mListeners.add(listener); - } - - /** - * Removes a {@link Listener} from PipController. - */ - void removeListener(Listener listener) { - mListeners.remove(listener); + mTvPipMenuController.showMenu(); } /** @@ -635,35 +520,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } } - /** - * A listener interface to receive notification on changes in PIP. - */ - public interface Listener { - /** - * Invoked when an activity is pinned and PIP manager is set corresponding information. - * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned} - * because there's no guarantee for the PIP manager be return relavent information - * correctly. (e.g. {@link Pip.isPipShown}). - */ - void onPipEntered(String packageName); - /** Invoked when a PIPed activity is closed. */ - void onPipActivityClosed(); - /** Invoked when the PIP menu gets shown. */ - void onShowPipMenu(); - /** Invoked when the PIP menu actions change. */ - void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions); - /** Invoked when the PIPed activity is about to return back to the fullscreen. */ - void onMoveToFullscreen(); - /** Invoked when we are above to start resizing the Pip. */ - void onPipResizeAboutToStart(); - } - private String getStateDescription() { - if (mSuspendPipResizingReason == 0) { - return stateToName(mState); - } - return stateToName(mResumeResizePinnedStackRunnableState) + " (while " + stateToName(mState) - + " is suspended)"; + return stateToName(mState); } private static String stateToName(int state) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java deleted file mode 100644 index d2270c278161..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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.pip.tv; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.app.Activity; -import android.app.RemoteAction; -import android.content.Intent; -import android.content.pm.ParceledListSlice; -import android.os.Bundle; -import android.util.Log; - -import com.android.wm.shell.R; - -import java.util.Collections; - -/** - * Activity to show the PIP menu to control PIP. - * TODO(b/169395392) Refactor PipMenuActivity to PipMenuView - */ -public class PipMenuActivity extends Activity implements PipController.Listener { - private static final String TAG = "PipMenuActivity"; - private static final boolean DEBUG = PipController.DEBUG; - - static final String EXTRA_CUSTOM_ACTIONS = "custom_actions"; - - private static PipController sPipController; - - private Animator mFadeInAnimation; - private Animator mFadeOutAnimation; - private boolean mRestorePipSizeWhenClose; - private PipControlsViewController mPipControlsViewController; - - @Override - protected void onCreate(Bundle bundle) { - if (DEBUG) Log.d(TAG, "onCreate()"); - - super.onCreate(bundle); - if (sPipController == null) { - finish(); - } - setContentView(R.layout.tv_pip_menu); - mPipControlsViewController = new PipControlsViewController( - findViewById(R.id.pip_controls), sPipController); - sPipController.addListener(this); - mRestorePipSizeWhenClose = true; - mFadeInAnimation = AnimatorInflater.loadAnimator( - this, R.anim.tv_pip_menu_fade_in_animation); - mFadeInAnimation.setTarget(mPipControlsViewController.getView()); - mFadeOutAnimation = AnimatorInflater.loadAnimator( - this, R.anim.tv_pip_menu_fade_out_animation); - mFadeOutAnimation.setTarget(mPipControlsViewController.getView()); - - onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS)); - } - - @Override - protected void onNewIntent(Intent intent) { - if (DEBUG) Log.d(TAG, "onNewIntent(), intent=" + intent); - super.onNewIntent(intent); - - onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS)); - } - - private void restorePipAndFinish() { - if (DEBUG) Log.d(TAG, "restorePipAndFinish()"); - - if (mRestorePipSizeWhenClose) { - if (DEBUG) Log.d(TAG, " > restoring to the default position"); - - // When PIP menu activity is closed, restore to the default position. - sPipController.resizePinnedStack(PipController.STATE_PIP); - } - finish(); - } - - @Override - public void onResume() { - if (DEBUG) Log.d(TAG, "onResume()"); - - super.onResume(); - mFadeInAnimation.start(); - } - - @Override - public void onPause() { - if (DEBUG) Log.d(TAG, "onPause()"); - - super.onPause(); - mFadeOutAnimation.start(); - restorePipAndFinish(); - } - - @Override - protected void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy()"); - - super.onDestroy(); - sPipController.removeListener(this); - sPipController.resumePipResizing( - PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH); - } - - @Override - public void onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed()"); - - restorePipAndFinish(); - } - - @Override - public void onPipEntered(String packageName) { - if (DEBUG) Log.d(TAG, "onPipEntered(), packageName=" + packageName); - } - - @Override - public void onPipActivityClosed() { - if (DEBUG) Log.d(TAG, "onPipActivityClosed()"); - - finish(); - } - - @Override - public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { - if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()"); - - boolean hasCustomActions = actions != null && !actions.getList().isEmpty(); - mPipControlsViewController.setCustomActions( - hasCustomActions ? actions.getList() : Collections.emptyList()); - } - - @Override - public void onShowPipMenu() { - if (DEBUG) Log.d(TAG, "onShowPipMenu()"); - } - - @Override - public void onMoveToFullscreen() { - if (DEBUG) Log.d(TAG, "onMoveToFullscreen()"); - - // Moving PIP to fullscreen is implemented by resizing PINNED_STACK with null bounds. - // This conflicts with restoring PIP position, so disable it. - mRestorePipSizeWhenClose = false; - finish(); - } - - @Override - public void onPipResizeAboutToStart() { - if (DEBUG) Log.d(TAG, "onPipResizeAboutToStart()"); - - finish(); - sPipController.suspendPipResizing( - PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH); - } - - @Override - public void finish() { - if (DEBUG) Log.d(TAG, "finish()", new RuntimeException()); - - super.finish(); - } - - /** - * TODO(b/169395392) Refactor PipMenuActivity to PipMenuView - * - * @param pipController The singleton pipController instance for TV - */ - public static void setPipController(PipController pipController) { - if (sPipController != null) { - return; - } - sPipController = pipController; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java new file mode 100644 index 000000000000..83cb7ce8065b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java @@ -0,0 +1,126 @@ +/* + * 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.pip.tv; + +import static android.view.KeyEvent.ACTION_UP; +import static android.view.KeyEvent.KEYCODE_BACK; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.annotation.Nullable; +import android.app.RemoteAction; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.util.Log; +import android.view.KeyEvent; +import android.view.SurfaceControl; +import android.view.ViewRootImpl; +import android.view.WindowManagerGlobal; +import android.widget.FrameLayout; + +import com.android.wm.shell.R; + +import java.util.Collections; + +/** + * The Menu View that shows controls of the PiP. Always fullscreen. + */ +public class PipMenuView extends FrameLayout { + private static final String TAG = "PipMenuView"; + private static final boolean DEBUG = PipController.DEBUG; + + private final Animator mFadeInAnimation; + private final Animator mFadeOutAnimation; + private final PipControlsViewController mPipControlsViewController; + @Nullable + private OnBackPressListener mOnBackPressListener; + + public PipMenuView(Context context, PipController pipController) { + super(context, null, 0); + inflate(context, R.layout.tv_pip_menu, this); + + mPipControlsViewController = new PipControlsViewController( + findViewById(R.id.pip_controls), pipController); + mFadeInAnimation = AnimatorInflater.loadAnimator( + mContext, R.anim.tv_pip_menu_fade_in_animation); + mFadeInAnimation.setTarget(mPipControlsViewController.getView()); + mFadeOutAnimation = AnimatorInflater.loadAnimator( + mContext, R.anim.tv_pip_menu_fade_out_animation); + mFadeOutAnimation.setTarget(mPipControlsViewController.getView()); + } + + @Nullable + SurfaceControl getWindowSurfaceControl() { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { + return null; + } + final SurfaceControl out = root.getSurfaceControl(); + if (out != null && out.isValid()) { + return out; + } + return null; + } + + void showMenu() { + mFadeInAnimation.start(); + setAlpha(1.0f); + grantWindowFocus(true); + } + + void hideMenu() { + mFadeOutAnimation.start(); + setAlpha(0.0f); + grantWindowFocus(false); + } + + private void grantWindowFocus(boolean grantFocus) { + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + getViewRootImpl().getInputToken(), grantFocus); + } catch (Exception e) { + Log.e(TAG, "Unable to update focus as menu disappears", e); + } + } + + void setOnBackPressListener(OnBackPressListener onBackPressListener) { + mOnBackPressListener = onBackPressListener; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KEYCODE_BACK && event.getAction() == ACTION_UP + && mOnBackPressListener != null) { + mOnBackPressListener.onBackPress(); + return true; + } else { + return super.dispatchKeyEvent(event); + } + } + + void setAppActions(ParceledListSlice<RemoteAction> actions) { + if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()"); + + boolean hasCustomActions = actions != null && !actions.getList().isEmpty(); + mPipControlsViewController.setCustomActions( + hasCustomActions ? actions.getList() : Collections.emptyList()); + } + + interface OnBackPressListener { + void onBackPress(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java index b30dee4f331f..4e0ab668be81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java @@ -19,12 +19,10 @@ package com.android.wm.shell.pip.tv; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.RemoteAction; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.graphics.Bitmap; import android.media.MediaMetadata; @@ -41,7 +39,7 @@ import java.util.Objects; * <p>Once it's created, it will manage the PIP notification UI by itself except for handling * configuration changes. */ -public class PipNotification implements PipController.Listener { +public class PipNotification { private static final boolean DEBUG = PipController.DEBUG; private static final String TAG = "PipNotification"; @@ -81,43 +79,21 @@ public class PipNotification implements PipController.Listener { onConfigurationChanged(context); } - @Override - public void onPipEntered(String packageName) { + void show(String packageName) { mPackageName = packageName; - notifyPipNotification(); + update(); } - @Override - public void onPipActivityClosed() { - dismissPipNotification(); - mPackageName = null; - } - - @Override - public void onShowPipMenu() { - // no-op. - } - - @Override - public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { - // no-op. - } - - @Override - public void onMoveToFullscreen() { - dismissPipNotification(); + void dismiss() { + mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); + mNotified = false; mPackageName = null; } - @Override - public void onPipResizeAboutToStart() { - // no-op. - } - private void onMediaMetadataChanged(MediaMetadata metadata) { if (updateMediaControllerMetadata(metadata) && mNotified) { // update notification - notifyPipNotification(); + update(); } } @@ -130,11 +106,11 @@ public class PipNotification implements PipController.Listener { mDefaultIconResId = R.drawable.pip_icon; if (mNotified) { // update notification - notifyPipNotification(); + update(); } } - private void notifyPipNotification() { + private void update() { mNotified = true; mNotificationBuilder .setShowWhen(true) @@ -151,11 +127,6 @@ public class PipNotification implements PipController.Listener { mNotificationBuilder.build()); } - private void dismissPipNotification() { - mNotified = false; - mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); - } - private boolean updateMediaControllerMetadata(MediaMetadata metadata) { String title = null; Bitmap art = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java new file mode 100644 index 000000000000..5d0d761abd93 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -0,0 +1,138 @@ +/* + * 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.pip.tv; + +import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; + +import android.app.RemoteAction; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.util.Log; +import android.view.SurfaceControl; + +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMenuController; + +/** + * Manages the visibility of the PiP Menu as user interacts with PiP. + */ +public class TvPipMenuController implements PipMenuController { + private static final String TAG = "TvPipMenuController"; + private static final boolean DEBUG = PipController.DEBUG; + + private final Context mContext; + private final SystemWindows mSystemWindows; + private final PipBoundsState mPipBoundsState; + private PipMenuView mMenuView; + private PipController mPipController; + private SurfaceControl mLeash; + + public TvPipMenuController(Context context, PipBoundsState pipBoundsState, + SystemWindows systemWindows) { + mContext = context; + mPipBoundsState = pipBoundsState; + mSystemWindows = systemWindows; + } + + void attachPipController(PipController pipController) { + mPipController = pipController; + } + + @Override + public void showMenu() { + if (DEBUG) Log.d(TAG, "showMenu()"); + + if (mMenuView != null) { + mSystemWindows.updateViewLayout(mMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE, + mPipBoundsState.getDisplayBounds().width(), + mPipBoundsState.getDisplayBounds().height())); + mMenuView.showMenu(); + + // By default, SystemWindows views are above everything else. + // Set the relative z-order so the menu is below PiP. + if (mMenuView.getWindowSurfaceControl() != null && mLeash != null) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setRelativeLayer(mMenuView.getWindowSurfaceControl(), mLeash, -1); + t.apply(); + } + } + } + + void hideMenu() { + if (DEBUG) Log.d(TAG, "hideMenu()"); + + if (isMenuVisible()) { + mMenuView.hideMenu(); + mPipController.resizePinnedStack(PipController.STATE_PIP); + } + } + + @Override + public void attach(SurfaceControl leash) { + mLeash = leash; + attachPipMenuView(); + } + + @Override + public void detach() { + hideMenu(); + detachPipMenuView(); + mLeash = null; + } + + private void attachPipMenuView() { + if (DEBUG) Log.d(TAG, "attachPipMenuView()"); + + if (mMenuView != null) { + detachPipMenuView(); + } + + mMenuView = new PipMenuView(mContext, mPipController); + mMenuView.setOnBackPressListener(this::hideMenu); + mSystemWindows.addView(mMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); + } + + private void detachPipMenuView() { + if (DEBUG) Log.d(TAG, "detachPipMenuView()"); + + if (mMenuView == null) { + return; + } + + mSystemWindows.removeView(mMenuView); + mMenuView = null; + } + + @Override + public void setAppActions(ParceledListSlice<RemoteAction> appActions) { + if (DEBUG) Log.d(TAG, "setAppActions(), actions=" + appActions); + + if (mMenuView != null) { + mMenuView.setAppActions(appActions); + } else { + Log.w(TAG, "Cannot set remote actions, there is no View"); + } + } + + @Override + public boolean isMenuVisible() { + return mMenuView != null && mMenuView.getAlpha() == 1.0f; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java index f2becc99eae8..5078371dab64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java @@ -24,6 +24,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.SHELL_ROOT_LAYER_DIVIDER; @@ -59,7 +60,7 @@ final class DividerWindowManager { PixelFormat.TRANSLUCENT); mLp.token = new Binder(); mLp.setTitle(WINDOW_TITLE); - mLp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION; + mLp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index e55f065c1bb2..7c70a4efad91 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -19,6 +19,8 @@ package com.android.wm.shell.splitscreen; import android.graphics.Rect; import android.window.WindowContainerToken; +import com.android.wm.shell.common.annotations.ExternalThread; + import java.io.PrintWriter; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -26,6 +28,7 @@ import java.util.function.Consumer; /** * Interface to engage split screen feature. */ +@ExternalThread public interface SplitScreen { /** Called when keyguard showing state changed. */ void onKeyguardVisibilityChanged(boolean isShowing); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 07af289c4f35..6d6c76139c87 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -506,7 +506,7 @@ public class SplitScreenController implements SplitScreen, // Try fetching the top running task. final List<RunningTaskInfo> runningTasks = - ActivityTaskManager.getService().getTasks(1 /* maxNum */); + ActivityTaskManager.getInstance().getTasks(1 /* maxNum */); if (runningTasks == null || runningTasks.isEmpty()) { return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java index 5b2b38ba8189..af451ae8bc50 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskListener.java @@ -205,12 +205,9 @@ class SplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { private void updateChildTaskSurface( RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) { - final Rect taskBounds = taskInfo.getConfiguration().windowConfiguration.getBounds(); final Point taskPositionInParent = taskInfo.positionInParent; - final Rect corp = new Rect(taskBounds); - corp.offset(-taskBounds.left, -taskBounds.top); mSyncQueue.runInSync(t -> { - t.setWindowCrop(leash, corp); + t.setWindowCrop(leash, null); t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) { t.setAlpha(leash, 1f); diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index d7afa0e166b3..4a498d2ec581 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -29,7 +29,11 @@ android_test { "truth-prebuilt", "app-helpers-core", "launcher-helper-lib", - "launcher-aosp-tapl" + "launcher-aosp-tapl", + "wm-flicker-common-assertions", + "wm-flicker-common-app-helpers", + "platform-test-annotations", + "wmshell-flicker-test-components", ], } @@ -47,6 +51,10 @@ android_test { "truth-prebuilt", "app-helpers-core", "launcher-helper-lib", - "launcher-aosp-tapl" + "launcher-aosp-tapl", + "wm-flicker-common-assertions", + "wm-flicker-common-app-helpers", + "platform-test-annotations", + "wmshell-flicker-test-components", ], } diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml index 9e8330973b40..1054c4345891 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml @@ -21,6 +21,8 @@ <!-- Read and write traces from external storage --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Allow the test to write directly to /sdcard/ --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Write secure settings --> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <!-- Capture screen contents --> @@ -36,7 +38,10 @@ <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> <!-- Control test app's media session --> <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> - <application> + <!-- ATM.removeRootTasksWithActivityTypes() --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Allow the test to write directly to /sdcard/ --> + <application android:requestLegacyExternalStorage="true"> <uses-library android:name="android.test.runner"/> <service android:name=".NotificationListener" diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml index 9dd9f42bdf81..23d7021baffb 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml @@ -34,7 +34,7 @@ <option name="hidden-api-checks" value="false" /> </test> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> - <option name="directory-keys" value="/storage/emulated/0/Android/data/com.android.wm.shell.flicker/files" /> + <option name="directory-keys" value="/sdcard/flicker" /> <option name="collect-on-run-ended-only" value="true" /> <option name="clean-up" value="true" /> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml index afb1166415fc..073860875004 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml @@ -34,7 +34,7 @@ <option name="hidden-api-checks" value="false" /> </test> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> - <option name="directory-keys" value="/storage/emulated/0/Android/data/com.android.wm.shell.flicker/files" /> + <option name="directory-keys" value="/sdcard/flicker" /> <option name="collect-on-run-ended-only" value="true" /> <option name="clean-up" value="true" /> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt index 0fb43e263d05..1638d72f9914 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -18,78 +18,6 @@ package com.android.wm.shell.flicker import com.android.server.wm.flicker.dsl.EventLogAssertion import com.android.server.wm.flicker.dsl.LayersAssertion -import com.android.server.wm.flicker.dsl.WmAssertion -import com.android.server.wm.flicker.helpers.WindowUtils - -@JvmOverloads -fun WmAssertion.statusBarWindowIsAlwaysVisible( - bugId: Int = 0, - enabled: Boolean = bugId == 0 -) { - all("statusBarWindowIsAlwaysVisible", bugId, enabled) { - this.showsAboveAppWindow(FlickerTestBase.STATUS_BAR_WINDOW_TITLE) - } -} - -@JvmOverloads -fun WmAssertion.navBarWindowIsAlwaysVisible( - bugId: Int = 0, - enabled: Boolean = bugId == 0 -) { - all("navBarWindowIsAlwaysVisible", bugId, enabled) { - this.showsAboveAppWindow(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE) - } -} - -@JvmOverloads -fun LayersAssertion.noUncoveredRegions( - beginRotation: Int, - endRotation: Int = beginRotation, - allStates: Boolean = true, - bugId: Int = 0, - enabled: Boolean = bugId == 0 -) { - val startingBounds = WindowUtils.getDisplayBounds(beginRotation) - val endingBounds = WindowUtils.getDisplayBounds(endRotation) - if (allStates) { - all("noUncoveredRegions", bugId, enabled) { - if (startingBounds == endingBounds) { - this.coversAtLeastRegion(startingBounds) - } else { - this.coversAtLeastRegion(startingBounds) - .then() - .coversAtLeastRegion(endingBounds) - } - } - } else { - start("noUncoveredRegions_StartingPos") { - this.coversAtLeastRegion(startingBounds) - } - end("noUncoveredRegions_EndingPos") { - this.coversAtLeastRegion(endingBounds) - } - } -} - -@JvmOverloads -fun LayersAssertion.statusBarLayerIsAlwaysVisible( - bugId: Int = 0, - enabled: Boolean = bugId == 0 -) { - all("statusBarLayerIsAlwaysVisible", bugId, enabled) { - this.showsLayer(FlickerTestBase.STATUS_BAR_WINDOW_TITLE) - } -} - -@JvmOverloads -fun LayersAssertion.navBarLayerIsAlwaysVisible( - bugId: Int = 0, - enabled: Boolean = bugId == 0 -) { - all("navBarLayerIsAlwaysVisible", bugId, enabled) { - this.showsLayer(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE) - } -} @JvmOverloads fun LayersAssertion.appPairsDividerIsVisible( @@ -97,7 +25,7 @@ fun LayersAssertion.appPairsDividerIsVisible( enabled: Boolean = bugId == 0 ) { end("appPairsDividerIsVisible", bugId, enabled) { - this.showsLayer(FlickerTestBase.APP_PAIRS_DIVIDER) + this.showsLayer(FlickerTestBase.SPLIT_DIVIDER) } } @@ -107,7 +35,7 @@ fun LayersAssertion.appPairsDividerIsInvisible( enabled: Boolean = bugId == 0 ) { end("appPairsDividerIsInVisible", bugId, enabled) { - this.hasNotLayer(FlickerTestBase.APP_PAIRS_DIVIDER) + this.hasNotLayer(FlickerTestBase.SPLIT_DIVIDER) } } @@ -131,48 +59,6 @@ fun LayersAssertion.dockedStackDividerIsInvisible( } } -@JvmOverloads -fun LayersAssertion.navBarLayerRotatesAndScales( - beginRotation: Int, - endRotation: Int = beginRotation, - bugId: Int = 0, - enabled: Boolean = bugId == 0 -) { - val startingPos = WindowUtils.getNavigationBarPosition(beginRotation) - val endingPos = WindowUtils.getNavigationBarPosition(endRotation) - - start("navBarLayerRotatesAndScales_StartingPos", bugId, enabled) { - this.hasVisibleRegion(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE, startingPos) - } - end("navBarLayerRotatesAndScales_EndingPost", bugId, enabled) { - this.hasVisibleRegion(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE, endingPos) - } - - if (startingPos == endingPos) { - all("navBarLayerRotatesAndScales", bugId, enabled) { - this.hasVisibleRegion(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE, startingPos) - } - } -} - -@JvmOverloads -fun LayersAssertion.statusBarLayerRotatesScales( - beginRotation: Int, - endRotation: Int = beginRotation, - bugId: Int = 0, - enabled: Boolean = bugId == 0 -) { - val startingPos = WindowUtils.getStatusBarPosition(beginRotation) - val endingPos = WindowUtils.getStatusBarPosition(endRotation) - - start("statusBarLayerRotatesScales_StartingPos", bugId, enabled) { - this.hasVisibleRegion(FlickerTestBase.STATUS_BAR_WINDOW_TITLE, startingPos) - } - end("statusBarLayerRotatesScales_EndingPos", bugId, enabled) { - this.hasVisibleRegion(FlickerTestBase.STATUS_BAR_WINDOW_TITLE, endingPos) - } -} - fun EventLogAssertion.focusChanges( vararg windows: String, bugId: Int = 0, diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt index 3c222e7d8b56..96234fcc8570 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt @@ -16,31 +16,24 @@ package com.android.wm.shell.flicker -import android.content.ComponentName - const val IME_WINDOW_NAME = "InputMethod" -const val PIP_WINDOW_NAME = "PipMenuActivity" -const val SPLITSCREEN_PRIMARY_WINDOW_NAME = "SplitScreenActivity" -const val SPLITSCREEN_SECONDARY_WINDOW_NAME = "SplitScreenSecondaryActivity" +const val PIP_MENU_WINDOW_NAME = "PipMenuActivity" const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" const val TEST_APP_PACKAGE_NAME = "com.android.wm.shell.flicker.testapp" // Test App > Pip Activity -val TEST_APP_PIP_ACTIVITY_COMPONENT_NAME: ComponentName = ComponentName.createRelative( - TEST_APP_PACKAGE_NAME, ".PipActivity") const val TEST_APP_PIP_ACTIVITY_LABEL = "PipApp" -const val TEST_APP_PIP_ACTIVITY_WINDOW_NAME = "PipActivity" +const val TEST_APP_PIP_MENU_ACTION_NO_OP = "No-Op" +const val TEST_APP_PIP_MENU_ACTION_ON = "On" +const val TEST_APP_PIP_MENU_ACTION_OFF = "Off" +const val TEST_APP_PIP_MENU_ACTION_CLEAR = "Clear" // Test App > Ime Activity -val TEST_APP_IME_ACTIVITY_COMPONENT_NAME: ComponentName = ComponentName.createRelative( - TEST_APP_PACKAGE_NAME, ".ImeActivity") const val TEST_APP_IME_ACTIVITY_LABEL = "ImeApp" +// Test App > Test Activity +const val TEST_APP_FIXED_ACTIVITY_LABEL = "FixedApp" // Test App > SplitScreen Activity -val TEST_APP_SPLITSCREEN_PRIMARY_COMPONENT_NAME: ComponentName = ComponentName.createRelative( - TEST_APP_PACKAGE_NAME, ".$SPLITSCREEN_PRIMARY_WINDOW_NAME") -val TEST_APP_SPLITSCREEN_SECONDARY_COMPONENT_NAME: ComponentName = ComponentName.createRelative( - TEST_APP_PACKAGE_NAME, ".$SPLITSCREEN_SECONDARY_WINDOW_NAME") const val TEST_APP_SPLITSCREEN_PRIMARY_LABEL = "SplitScreenPrimaryApp" const val TEST_APP_SPLITSCREEN_SECONDARY_LABEL = "SplitScreenSecondaryApp" diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt index 54b8fdc83a1f..7809be04de96 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt @@ -130,7 +130,7 @@ abstract class FlickerTestBase { const val NAVIGATION_BAR_WINDOW_TITLE = "NavigationBar" const val STATUS_BAR_WINDOW_TITLE = "StatusBar" const val DOCKED_STACK_DIVIDER = "DockedStackDivider" - const val APP_PAIRS_DIVIDER = "AppPairDivider" + const val SPLIT_DIVIDER = "SplitDivider" const val IMAGE_WALLPAPER = "ImageWallpaper" } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt index 2fc6944a3a5f..0f8d30a94ec6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.apppairs +import android.platform.test.annotations.Presubmit import android.os.SystemClock import android.util.Log import android.view.Surface @@ -28,10 +29,10 @@ import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.TEST_REPETITIONS import com.android.wm.shell.flicker.appPairsDividerIsInvisible import com.android.wm.shell.flicker.appPairsDividerIsVisible -import com.android.wm.shell.flicker.navBarLayerIsAlwaysVisible -import com.android.wm.shell.flicker.navBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.statusBarLayerIsAlwaysVisible -import com.android.wm.shell.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -43,6 +44,7 @@ import java.io.IOException * Test AppPairs launch. * To run this test: `atest WMShellFlickerTests:AppPairsTest` */ +@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -106,7 +108,7 @@ class AppPairsTest( end("appsEndingBounds", enabled = false) { val entry = this.trace.entries.firstOrNull() ?: throw IllegalStateException("Trace is empty") - val dividerRegion = entry.getVisibleBounds(APP_PAIRS_DIVIDER) + val dividerRegion = entry.getVisibleBounds(SPLIT_DIVIDER) this.hasVisibleRegion(primaryApp.defaultWindowName, appPairsHelper.getPrimaryBounds(dividerRegion)) .and() @@ -150,7 +152,7 @@ class AppPairsTest( start("appsStartingBounds", enabled = false) { val entry = this.trace.entries.firstOrNull() ?: throw IllegalStateException("Trace is empty") - val dividerRegion = entry.getVisibleBounds(APP_PAIRS_DIVIDER) + val dividerRegion = entry.getVisibleBounds(SPLIT_DIVIDER) this.hasVisibleRegion(primaryApp.defaultWindowName, appPairsHelper.getPrimaryBounds(dividerRegion)) .and() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestBase.kt index 1a4de0a80bec..f32cd8842074 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestBase.kt @@ -17,12 +17,11 @@ package com.android.wm.shell.flicker.apppairs import com.android.wm.shell.flicker.NonRotationTestBase -import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_PRIMARY_COMPONENT_NAME import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_PRIMARY_LABEL -import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_SECONDARY_COMPONENT_NAME import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_SECONDARY_LABEL import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.testapp.Components abstract class AppPairsTestBase( rotationName: String, @@ -30,11 +29,11 @@ abstract class AppPairsTestBase( ) : NonRotationTestBase(rotationName, rotation) { protected val appPairsHelper = AppPairsHelper(instrumentation, TEST_APP_SPLITSCREEN_PRIMARY_LABEL, - TEST_APP_SPLITSCREEN_PRIMARY_COMPONENT_NAME) + Components.SplitScreenActivity()) protected val primaryApp = SplitScreenHelper(instrumentation, TEST_APP_SPLITSCREEN_PRIMARY_LABEL, - TEST_APP_SPLITSCREEN_PRIMARY_COMPONENT_NAME) + Components.SplitScreenActivity()) protected val secondaryApp = SplitScreenHelper(instrumentation, TEST_APP_SPLITSCREEN_SECONDARY_LABEL, - TEST_APP_SPLITSCREEN_SECONDARY_COMPONENT_NAME) + Components.SplitScreenSecondaryActivity()) } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 3b6fcdbee4be..e2cda7ad123d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -17,19 +17,19 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.graphics.Region import android.system.helpers.ActivityHelper import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.wm.shell.flicker.testapp.Components class AppPairsHelper( instrumentation: Instrumentation, activityLabel: String, - componentName: ComponentName + componentsInfo: Components.ComponentsInfo ) : BaseAppHelper( instrumentation, activityLabel, - componentName + componentsInfo ) { val activityHelper = ActivityHelper.getInstance() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt index 22496a506e0d..6fd1df3b3f30 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager.FEATURE_LEANBACK @@ -28,15 +27,15 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.StandardAppHelper -import com.android.wm.shell.flicker.TEST_APP_PACKAGE_NAME +import com.android.wm.shell.flicker.testapp.Components abstract class BaseAppHelper( instrumentation: Instrumentation, launcherName: String, - private val launcherActivityComponent: ComponentName + private val componentsInfo: Components.ComponentsInfo ) : StandardAppHelper( instrumentation, - TEST_APP_PACKAGE_NAME, + Components.PACKAGE_NAME, launcherName, LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy ) { @@ -53,7 +52,7 @@ abstract class BaseAppHelper( } val defaultWindowName: String - get() = launcherActivityComponent.className + get() = componentsInfo.activityName val label: String get() = context.packageManager.run { @@ -63,8 +62,12 @@ abstract class BaseAppHelper( val ui: UiObject2? get() = uiDevice.findObject(appSelector) - fun launchViaIntent() { - context.startActivity(openAppIntent) + fun launchViaIntent(stringExtras: Map<String, String> = mapOf()) { + val intent = openAppIntent + stringExtras.forEach() { + intent.putExtra(it.key, it.value) + } + context.startActivity(intent) uiDevice.wait(Until.hasObject(appSelector), APP_LAUNCH_WAIT_TIME_MS) } @@ -75,7 +78,7 @@ abstract class BaseAppHelper( override fun getOpenAppIntent(): Intent { val intent = Intent() - intent.component = launcherActivityComponent + intent.component = componentsInfo.componentName intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) return intent } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt new file mode 100644 index 000000000000..c7f19a5d2620 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt @@ -0,0 +1,29 @@ +/* + * 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.flicker.helpers + +import android.app.Instrumentation +import com.android.wm.shell.flicker.TEST_APP_FIXED_ACTIVITY_LABEL +import com.android.wm.shell.flicker.testapp.Components + +class FixedAppHelper( + instrumentation: Instrumentation +) : BaseAppHelper( + instrumentation, + TEST_APP_FIXED_ACTIVITY_LABEL, + Components.FixedActivity() +)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index a6650d7f13d1..d580104ade19 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -21,8 +21,8 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.FIND_TIMEOUT import com.android.server.wm.flicker.helpers.waitForIME -import com.android.wm.shell.flicker.TEST_APP_IME_ACTIVITY_COMPONENT_NAME import com.android.wm.shell.flicker.TEST_APP_IME_ACTIVITY_LABEL +import com.android.wm.shell.flicker.testapp.Components import org.junit.Assert open class ImeAppHelper( @@ -30,7 +30,7 @@ open class ImeAppHelper( ) : BaseAppHelper( instrumentation, TEST_APP_IME_ACTIVITY_LABEL, - TEST_APP_IME_ACTIVITY_COMPONENT_NAME + Components.ImeActivity() ) { fun openIME() { val editText = uiDevice.wait( diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index 532b3de6c99e..ed5f8a42258b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -26,8 +26,8 @@ import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.closePipWindow import com.android.server.wm.flicker.helpers.hasPipWindow import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME -import com.android.wm.shell.flicker.TEST_APP_PIP_ACTIVITY_COMPONENT_NAME import com.android.wm.shell.flicker.TEST_APP_PIP_ACTIVITY_LABEL +import com.android.wm.shell.flicker.testapp.Components import org.junit.Assert.assertNotNull import org.junit.Assert.fail @@ -36,7 +36,7 @@ class PipAppHelper( ) : BaseAppHelper( instrumentation, TEST_APP_PIP_ACTIVITY_LABEL, - TEST_APP_PIP_ACTIVITY_COMPONENT_NAME + Components.PipActivity() ) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) @@ -70,6 +70,12 @@ class PipAppHelper( startButton.click() } + fun checkWithCustomActionsCheckbox() = uiDevice + .findObject(By.res(packageName, "with_custom_actions")) + ?.takeIf { it.isCheckable } + ?.apply { if (!isChecked) click() } + ?: error("'With custom actions' checkbox not found") + fun pauseMedia() = mediaController?.transportControls?.pause() ?: error("No active media session found") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt index 10daa675ce36..e67fc97dad2e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt @@ -17,19 +17,19 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.graphics.Region import android.os.SystemClock import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.wm.shell.flicker.testapp.Components class SplitScreenHelper( instrumentation: Instrumentation, activityLabel: String, - componentName: ComponentName + componentsInfo: Components.ComponentsInfo ) : BaseAppHelper( instrumentation, activityLabel, - componentName + componentsInfo ) { /** diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt new file mode 100644 index 000000000000..2015f4941cea --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt @@ -0,0 +1,46 @@ +/* + * 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.flicker.pip + +import android.app.ActivityTaskManager +import android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT +import android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED +import android.os.SystemClock +import com.android.wm.shell.flicker.NonRotationTestBase + +abstract class AppTestBase( + rotationName: String, + rotation: Int +) : NonRotationTestBase(rotationName, rotation) { + companion object { + fun removeAllTasksButHome() { + val ALL_ACTIVITY_TYPE_BUT_HOME = intArrayOf( + ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS, + ACTIVITY_TYPE_UNDEFINED) + val atm = ActivityTaskManager.getService() + atm.removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME) + } + + fun waitForAnimationComplete() { + // TODO: UiDevice doesn't have reliable way to wait for the completion of animation. + // Consider to introduce WindowManagerStateHelper to access Activity state. + SystemClock.sleep(1000) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt new file mode 100644 index 000000000000..2a660747bc1d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt @@ -0,0 +1,19 @@ +/* + * 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.flicker.pip + +internal const val PIP_WINDOW_TITLE = "PipMenuActivity" diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt new file mode 100644 index 000000000000..c9396aa7ea63 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt @@ -0,0 +1,118 @@ +/* + * 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.flicker.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.dsl.runFlicker +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip launch and exit. + * To run this test: `atest WMShellFlickerTests:EnterExitPipTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterExitPipTest( + rotationName: String, + rotation: Int +) : AppTestBase(rotationName, rotation) { + private val pipApp = PipAppHelper(instrumentation) + private val testApp = FixedAppHelper(instrumentation) + + @Test + fun testDisplayMetricsPinUnpin() { + runFlicker(instrumentation) { + withTestName { "testDisplayMetricsPinUnpin" } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(stringExtras = mapOf(EXTRA_ENTER_PIP to "true")) + testApp.launchViaIntent() + waitForAnimationComplete() + } + } + transitions { + // This will bring PipApp to fullscreen + pipApp.launchViaIntent() + waitForAnimationComplete() + } + teardown { + test { + removeAllTasksButHome() + } + } + assertions { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + windowManagerTrace { + all("pipApp must remain inside visible bounds") { + coversAtMostRegion(pipApp.defaultWindowName, displayBounds) + } + all("Initially shows both app windows then pipApp hides testApp") { + showsAppWindow(testApp.defaultWindowName) + .and().showsAppWindowOnTop(pipApp.defaultWindowName) + .then() + .hidesAppWindow(testApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + all("Initially shows both app layers then pipApp hides testApp") { + showsLayer(testApp.defaultWindowName) + .and().showsLayer(pipApp.defaultWindowName) + .then() + .hidesLayer(testApp.defaultWindowName) + } + start("testApp covers the fullscreen, pipApp remains inside display") { + hasVisibleRegion(testApp.defaultWindowName, displayBounds) + coversAtMostRegion(displayBounds, pipApp.defaultWindowName) + } + end("pipApp covers the fullscreen") { + hasVisibleRegion(pipApp.defaultWindowName, displayBounds) + } + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + } + } + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val supportedRotations = intArrayOf(Surface.ROTATION_0) + return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index a1da7c939f60..cb1fe4e2981d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -19,104 +19,113 @@ package com.android.wm.shell.flicker.pip import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.dsl.flicker +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.helpers.buildTestTag import com.android.server.wm.flicker.helpers.closePipWindow import com.android.server.wm.flicker.helpers.expandPipWindow import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.navBarLayerIsAlwaysVisible -import com.android.wm.shell.flicker.navBarLayerRotatesAndScales -import com.android.wm.shell.flicker.navBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.noUncoveredRegions -import com.android.wm.shell.flicker.statusBarLayerIsAlwaysVisible -import com.android.wm.shell.flicker.statusBarLayerRotatesScales -import com.android.wm.shell.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.PIP_WINDOW_NAME +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.wm.shell.flicker.helpers.PipAppHelper import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** * Test Pip launch. - * To run this test: `atest WMShellFlickerTests:PipToAppTest` + * To run this test: `atest WMShellFlickerTests:EnterPipTest` */ @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @FlakyTest(bugId = 152738416) class EnterPipTest( - rotationName: String, - rotation: Int -) : PipTestBase(rotationName, rotation) { - @Test - fun test() { - flicker(instrumentation) { - withTag { buildTestTag("enterPip", testApp, rotation) } - repeat { 1 } - setup { - test { - device.wakeUpAndGoToHomeScreen() - } - eachRun { - device.pressHome() - testApp.open() - this.setRotation(rotation) - } - } - teardown { - eachRun { - if (device.hasPipWindow()) { - device.closePipWindow() + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory(instrumentation, listOf(Surface.ROTATION_0)) + .buildTest { configuration -> + withTestName { buildTestTag("enterPip", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + } + eachRun { + device.pressHome() + testApp.open() + this.setRotation(configuration.startRotation) + } } - testApp.exit() - this.setRotation(Surface.ROTATION_0) - } - test { - if (device.hasPipWindow()) { - device.closePipWindow() + teardown { + eachRun { + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + this.setRotation(Surface.ROTATION_0) + } + test { + if (device.hasPipWindow()) { + device.closePipWindow() + } + } } - } - } - transitions { - testApp.clickEnterPipButton() - device.expandPipWindow() - } - assertions { - windowManagerTrace { - navBarWindowIsAlwaysVisible() - statusBarWindowIsAlwaysVisible() - all("pipWindowBecomesVisible") { - this.showsAppWindow(testApp.`package`) - .then() - .showsAppWindow(PIP_WINDOW_NAME) + transitions { + testApp.clickEnterPipButton() + device.expandPipWindow() } - } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() - layersTrace { - navBarLayerIsAlwaysVisible() - statusBarLayerIsAlwaysVisible() - noUncoveredRegions(rotation, Surface.ROTATION_0, allStates = false) - navBarLayerRotatesAndScales(rotation, Surface.ROTATION_0) - statusBarLayerRotatesScales(rotation, Surface.ROTATION_0) + all("pipWindowBecomesVisible") { + this.showsAppWindow(testApp.`package`) + .then() + .showsAppWindow(PIP_WINDOW_TITLE) + } + } - all("pipLayerBecomesVisible") { - this.showsLayer(testApp.launcherName) - .then() - .showsLayer(PIP_WINDOW_NAME) + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, + enabled = false) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + Surface.ROTATION_0) + } + + layersTrace { + all("pipLayerBecomesVisible") { + this.showsLayer(testApp.launcherName) + .then() + .showsLayer(PIP_WINDOW_TITLE) + } + } } } - } - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<Array<Any>> { - val supportedRotations = intArrayOf(Surface.ROTATION_0) - return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt index abb8fc52abbb..6b44ce6ace0c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -16,33 +16,28 @@ package com.android.wm.shell.flicker.pip -import android.content.ComponentName -import android.graphics.Region -import android.util.Log +import android.platform.test.annotations.Presubmit import android.view.Surface -import android.view.WindowManager import androidx.test.filters.RequiresDevice -import com.android.compatibility.common.util.SystemUtil import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.dsl.runWithFlicker +import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.closePipWindow import com.android.server.wm.flicker.helpers.hasPipWindow import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.TEST_APP_IME_ACTIVITY_COMPONENT_NAME import com.android.wm.shell.flicker.IME_WINDOW_NAME -import com.android.wm.shell.flicker.TEST_APP_PIP_ACTIVITY_WINDOW_NAME import com.android.wm.shell.flicker.helpers.ImeAppHelper import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized -import java.io.IOException /** * Test Pip launch. * To run this test: `atest WMShellFlickerTests:PipKeyboardTest` */ +@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -50,9 +45,6 @@ class PipKeyboardTest( rotationName: String, rotation: Int ) : PipTestBase(rotationName, rotation) { - private val windowManager: WindowManager = - instrumentation.context.getSystemService(WindowManager::class.java) - private val keyboardApp = ImeAppHelper(instrumentation) private val keyboardScenario: FlickerBuilder @@ -71,7 +63,7 @@ class PipKeyboardTest( // open an app with an input field and a keyboard // UiAutomator doesn't support to launch the multiple Activities in a task. // So use launchActivity() for the Keyboard Activity. - launchActivity(TEST_APP_IME_ACTIVITY_COMPONENT_NAME) + keyboardApp.launchViaIntent() } } teardown { @@ -103,10 +95,8 @@ class PipKeyboardTest( assertions { windowManagerTrace { all("PiP window must remain inside visible bounds") { - coversAtMostRegion( - partialWindowTitle = "PipActivity", - region = Region(windowManager.maximumWindowMetrics.bounds) - ) + val displayBounds = WindowUtils.getDisplayBounds(rotation) + coversAtMostRegion(testApp.defaultWindowName, displayBounds) } } } @@ -132,74 +122,13 @@ class PipKeyboardTest( assertions { windowManagerTrace { end { - isAboveWindow(IME_WINDOW_NAME, TEST_APP_PIP_ACTIVITY_WINDOW_NAME) + isAboveWindow(IME_WINDOW_NAME, testApp.defaultWindowName) } } } } } - private fun launchActivity( - activity: ComponentName? = null, - action: String? = null, - flags: Set<Int> = setOf(), - boolExtras: Map<String, Boolean> = mapOf(), - intExtras: Map<String, Int> = mapOf(), - stringExtras: Map<String, String> = mapOf() - ) { - require(activity != null || !action.isNullOrBlank()) { - "Cannot launch an activity with neither activity name nor action!" - } - val command = composeCommand( - "start", activity, action, flags, boolExtras, intExtras, stringExtras) - executeShellCommand(command) - } - - private fun composeCommand( - command: String, - activity: ComponentName?, - action: String?, - flags: Set<Int>, - boolExtras: Map<String, Boolean>, - intExtras: Map<String, Int>, - stringExtras: Map<String, String> - ): String = buildString { - append("am ") - append(command) - activity?.let { - append(" -n ") - append(it.flattenToShortString()) - } - action?.let { - append(" -a ") - append(it) - } - flags.forEach { - append(" -f ") - append(it) - } - boolExtras.forEach { - append(it.withFlag("ez")) - } - intExtras.forEach { - append(it.withFlag("ei")) - } - stringExtras.forEach { - append(it.withFlag("es")) - } - } - - private fun Map.Entry<String, *>.withFlag(flag: String): String = " --$flag $key $value" - - private fun executeShellCommand(cmd: String): String { - try { - return SystemUtil.runShellCommand(instrumentation, cmd) - } catch (e: IOException) { - Log.e("FlickerTests", "Error running shell command: $cmd") - throw e - } - } - companion object { private const val TEST_REPETITIONS = 10 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt new file mode 100644 index 000000000000..76aabc1b83cf --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt @@ -0,0 +1,203 @@ +/* + * 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.flicker.pip + +import android.content.Intent +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.dsl.runFlicker +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP +import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION +import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION +import org.junit.Assert.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip with orientation changes. + * To run this test: `atest WMShellFlickerTests:PipOrientationTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipOrientationTest( + rotationName: String, + rotation: Int +) : AppTestBase(rotationName, rotation) { + // Helper class to process test actions by broadcast. + private inner class BroadcastActionTrigger { + private fun createIntentWithAction(broadcastAction: String): Intent { + return Intent(broadcastAction).setFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + fun doAction(broadcastAction: String) { + instrumentation.getContext().sendBroadcast(createIntentWithAction(broadcastAction)) + } + fun requestOrientationForPip(orientation: Int) { + instrumentation.getContext() + .sendBroadcast(createIntentWithAction(ACTION_SET_REQUESTED_ORIENTATION) + .putExtra(EXTRA_PIP_ORIENTATION, orientation.toString())) + } + } + private val broadcastActionTrigger = BroadcastActionTrigger() + + // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + private val ORIENTATION_LANDSCAPE = 0 + // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + private val ORIENTATION_PORTRAIT = 1 + + private val testApp = FixedAppHelper(instrumentation) + private val pipApp = PipAppHelper(instrumentation) + + @Test + fun testEnterPipToOtherOrientation() { + runFlicker(instrumentation) { + withTestName { "testEnterPipToOtherOrientation" } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + // Launch a portrait only app on the fullscreen stack + testApp.launchViaIntent(stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())) + waitForAnimationComplete() + // Launch the PiP activity fixed as landscape + pipApp.launchViaIntent(stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) + waitForAnimationComplete() + } + } + transitions { + // Enter PiP, and assert that the PiP is within bounds now that the device is back + // in portrait + broadcastActionTrigger.doAction(ACTION_ENTER_PIP) + waitForAnimationComplete() + } + teardown { + test { + removeAllTasksButHome() + } + } + assertions { + windowManagerTrace { + all("pipApp window is always on top") { + showsAppWindowOnTop(pipApp.defaultWindowName) + } + start("pipApp window hides testApp") { + hidesAppWindow(testApp.defaultWindowName) + } + end("testApp windows is shown") { + showsAppWindow(testApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) + val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + start("pipApp layer hides testApp") { + hasVisibleRegion(pipApp.defaultWindowName, startingBounds) + hidesLayer(testApp.defaultWindowName) + } + end("testApp layer covers fullscreen") { + hasVisibleRegion(testApp.defaultWindowName, endingBounds) + } + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + } + } + } + } + + @Test + fun testSetRequestedOrientationWhilePinned() { + runFlicker(instrumentation) { + withTestName { "testSetRequestedOrientationWhilePinned" } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + // Launch the PiP activity fixed as landscape + pipApp.launchViaIntent(stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString(), + EXTRA_ENTER_PIP to "true")) + waitForAnimationComplete() + assertEquals(Surface.ROTATION_0, device.displayRotation) + } + } + transitions { + // Request that the orientation is set to landscape + broadcastActionTrigger.requestOrientationForPip(ORIENTATION_LANDSCAPE) + + // Launch the activity back into fullscreen and ensure that it is now in landscape + pipApp.launchViaIntent() + waitForAnimationComplete() + assertEquals(Surface.ROTATION_90, device.displayRotation) + } + teardown { + test { + removeAllTasksButHome() + } + } + assertions { + val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) + windowManagerTrace { + start("PIP window must remain inside display") { + coversAtMostRegion(pipApp.defaultWindowName, startingBounds) + } + end("pipApp shows on top") { + showsAppWindowOnTop(pipApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + start("PIP layer must remain inside display") { + coversAtMostRegion(startingBounds, pipApp.defaultWindowName) + } + end("pipApp layer covers fullscreen") { + hasVisibleRegion(pipApp.defaultWindowName, endingBounds) + } + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + } + } + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val supportedRotations = intArrayOf(Surface.ROTATION_0) + return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt new file mode 100644 index 000000000000..a67b3b760c49 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -0,0 +1,127 @@ +/* + * 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.flicker.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip Stack in bounds after rotations. + * To run this test: `atest WMShellFlickerTests:PipRotationTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipRotationTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = FixedAppHelper(instrumentation) + val pipApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory(instrumentation, + listOf(Surface.ROTATION_0, Surface.ROTATION_90)) + .buildRotationTest { configuration -> + withTestName { buildTestTag("PipRotationTest", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + AppTestBase.removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(stringExtras = mapOf( + EXTRA_ENTER_PIP to "true")) + testApp.launchViaIntent() + AppTestBase.waitForAnimationComplete() + } + eachRun { + setRotation(configuration.startRotation) + } + } + transitions { + setRotation(configuration.endRotation) + } + teardown { + eachRun { + setRotation(Surface.ROTATION_0) + } + test { + AppTestBase.removeAllTasksButHome() + } + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + noUncoveredRegions(configuration.startRotation, + configuration.endRotation, allStates = false) + navBarLayerRotatesAndScales(configuration.startRotation, + configuration.endRotation) + statusBarLayerRotatesScales(configuration.startRotation, + configuration.endRotation) + } + layersTrace { + val startingBounds = WindowUtils.getDisplayBounds( + configuration.startRotation) + val endingBounds = WindowUtils.getDisplayBounds( + configuration.endRotation) + start("appLayerRotates_StartingBounds") { + hasVisibleRegion(testApp.defaultWindowName, startingBounds) + coversAtMostRegion(startingBounds, pipApp.defaultWindowName) + } + end("appLayerRotates_EndingBounds") { + hasVisibleRegion(testApp.defaultWindowName, endingBounds) + coversAtMostRegion(endingBounds, pipApp.defaultWindowName) + } + } + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipSplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipSplitScreenTest.kt new file mode 100644 index 000000000000..f79b21ff278d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipSplitScreenTest.kt @@ -0,0 +1,127 @@ +/* + * 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.flicker.pip + +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.dsl.runFlicker +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.ImeAppHelper +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip with split-screen. + * To run this test: `atest WMShellFlickerTests:PipSplitScreenTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 161435597) +class PipSplitScreenTest( + rotationName: String, + rotation: Int +) : AppTestBase(rotationName, rotation) { + private val pipApp = PipAppHelper(instrumentation) + private val imeApp = ImeAppHelper(instrumentation) + private val testApp = FixedAppHelper(instrumentation) + + @Test + fun testShowsPipLaunchingToSplitScreen() { + runFlicker(instrumentation) { + withTestName { "testShowsPipLaunchingToSplitScreen" } + repeat { TEST_REPETITIONS } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(stringExtras = mapOf(EXTRA_ENTER_PIP to "true")) + waitForAnimationComplete() + } + } + transitions { + testApp.launchViaIntent() + device.launchSplitScreen() + imeApp.launchViaIntent() + waitForAnimationComplete() + } + teardown { + eachRun { + imeApp.exit() + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + testApp.exit() + } + test { + removeAllTasksButHome() + } + } + assertions { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + windowManagerTrace { + all("PIP window must remain inside visible bounds") { + coversAtMostRegion(pipApp.defaultWindowName, displayBounds) + } + end("Both app windows should be visible") { + showsAppWindow(testApp.defaultWindowName) + showsAppWindow(imeApp.defaultWindowName) + noWindowsOverlap(testApp.defaultWindowName, imeApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + all("PIP layer must remain inside visible bounds") { + coversAtMostRegion(displayBounds, pipApp.defaultWindowName) + } + end("Both app layers should be visible") { + coversAtMostRegion(displayBounds, testApp.defaultWindowName) + coversAtMostRegion(displayBounds, imeApp.defaultWindowName) + } + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + } + } + } + } + + companion object { + const val TEST_REPETITIONS = 2 + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val supportedRotations = intArrayOf(Surface.ROTATION_0) + return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt index c1c34ecfbaec..03a92119fc86 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt @@ -16,12 +16,11 @@ package com.android.wm.shell.flicker.pip -import com.android.wm.shell.flicker.NonRotationTestBase import com.android.wm.shell.flicker.helpers.PipAppHelper abstract class PipTestBase( rotationName: String, rotation: Int -) : NonRotationTestBase(rotationName, rotation) { +) : AppTestBase(rotationName, rotation) { protected val testApp = PipAppHelper(instrumentation) } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt new file mode 100644 index 000000000000..e1fa6578e552 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt @@ -0,0 +1,135 @@ +/* + * 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.flicker.pip + +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.closePipWindow +import com.android.server.wm.flicker.helpers.expandPipWindow +import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.helpers.PipAppHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip launch. + * To run this test: `atest WMShellFlickerTests:PipToAppTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 152738416) +class PipToAppTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory(instrumentation, listOf(Surface.ROTATION_0)) + .buildTest { configuration -> + withTestName { buildTestTag("exitPipModeToApp", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.pressHome() + testApp.open() + } + eachRun { + this.setRotation(configuration.startRotation) + testApp.clickEnterPipButton() + device.hasPipWindow() + } + } + teardown { + eachRun { + this.setRotation(Surface.ROTATION_0) + } + test { + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + } + } + transitions { + device.expandPipWindow() + device.waitForIdle() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + + all("appReplacesPipWindow") { + this.showsAppWindow(PIP_WINDOW_TITLE) + .then() + .showsAppWindowOnTop(testApp.launcherName) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, + enabled = false) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + Surface.ROTATION_0) + + all("appReplacesPipLayer") { + this.showsLayer(PIP_WINDOW_TITLE) + .then() + .showsLayer(testApp.launcherName) + } + } + + eventLog { + focusChanges( + "NexusLauncherActivity", testApp.launcherName, + "NexusLauncherActivity", bugId = 151179149) + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt new file mode 100644 index 000000000000..bf1193786a59 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt @@ -0,0 +1,135 @@ +/* + * 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.flicker.pip + +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.closePipWindow +import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.helpers.PipAppHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip launch. + * To run this test: `atest WMShellFlickerTests:PipToHomeTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 152738416) +class PipToHomeTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory(instrumentation, listOf(Surface.ROTATION_0)) + .buildTest { configuration -> + withTestName { buildTestTag("exitPipModeToApp", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.pressHome() + } + eachRun { + testApp.open() + this.setRotation(configuration.startRotation) + testApp.clickEnterPipButton() + device.hasPipWindow() + } + } + teardown { + eachRun { + this.setRotation(Surface.ROTATION_0) + if (device.hasPipWindow()) { + device.closePipWindow() + } + } + test { + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + } + } + transitions { + testApp.closePipWindow() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + + all("pipWindowBecomesInvisible") { + this.showsAppWindow(PIP_WINDOW_TITLE) + .then() + .hidesAppWindow(PIP_WINDOW_TITLE) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, + enabled = false) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + Surface.ROTATION_0) + + all("pipLayerBecomesInvisible") { + this.showsLayer(PIP_WINDOW_TITLE) + .then() + .hidesLayer(PIP_WINDOW_TITLE) + } + } + + eventLog { + focusChanges(testApp.launcherName, "NexusLauncherActivity", + bugId = 151179149) + } + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 871732cf7460..4cb6447f7d7e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -20,7 +20,12 @@ import android.graphics.Rect import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.UiObject2 import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_CLEAR +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_NO_OP +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_OFF +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_ON import com.android.wm.shell.flicker.wait +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -128,6 +133,7 @@ class TvPipMenuTests : TvPipTestBase() { testApp.clickStartMediaSessionButton() enterPip_openMenu_assertShown() + assertFullscreenAndCloseButtonsAreShown() // PiP menu should contain the Pause button val pauseButton = uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription) @@ -137,6 +143,7 @@ class TvPipMenuTests : TvPipTestBase() { // When we pause media, the button should change from Pause to Play pauseButton.click() + assertFullscreenAndCloseButtonsAreShown() // PiP menu should contain the Play button now uiDevice.waitForTvPipMenuElementWithDescription(playButtonDescription) ?: fail("\"Play\" button should be shown in Pip menu if there is an active " + @@ -145,10 +152,99 @@ class TvPipMenuTests : TvPipTestBase() { testApp.closePipWindow() } + @Test + fun pipMenu_withCustomActions() { + // Enter PiP with custom actions. + testApp.checkWithCustomActionsCheckbox() + enterPip_openMenu_assertShown() + + // PiP menu should contain "No-Op", "Off" and "Clear" buttons... + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP) + ?: fail("\"No-Op\" button should be shown in Pip menu") + val offButton = uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF) + ?: fail("\"Off\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") + // ... and should also contain the "Full screen" and "Close" buttons. + assertFullscreenAndCloseButtonsAreShown() + + offButton.click() + // Invoking the "Off" action should replace it with the "On" action/button and should + // remove the "No-Op" action/button. "Clear" action/button should remain in the menu ... + uiDevice.waitForTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_ON) + ?: fail("\"On\" button should be shown in Pip for a corresponding custom action") + assertNull("\"No-Op\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP)) + val clearButton = + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") + // ... as well as the "Full screen" and "Close" buttons. + assertFullscreenAndCloseButtonsAreShown() + + clearButton.click() + // Invoking the "Clear" action should remove all the custom actions and their corresponding + // buttons, ... + uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(TEST_APP_PIP_MENU_ACTION_ON)?.also { + isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu") + } + assertNull("\"Off\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF)) + assertNull("\"Clear\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR)) + assertNull("\"No-Op\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP)) + // ... but the menu should still contain the "Full screen" and "Close" buttons. + assertFullscreenAndCloseButtonsAreShown() + + testApp.closePipWindow() + } + + @Test + fun pipMenu_customActions_override_mediaControls() { + // Start media session before entering PiP with custom actions. + testApp.clickStartMediaSessionButton() + testApp.checkWithCustomActionsCheckbox() + enterPip_openMenu_assertShown() + + // PiP menu should contain "No-Op", "Off" and "Clear" buttons for the custom actions... + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP) + ?: fail("\"No-Op\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF) + ?: fail("\"Off\" button should be shown in Pip menu") + val clearButton = + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") + // ... should also contain the "Full screen" and "Close" buttons, ... + assertFullscreenAndCloseButtonsAreShown() + // ... but should not contain media buttons. + assertNull("\"Play\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(playButtonDescription)) + assertNull("\"Pause\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)) + + clearButton.click() + // Invoking the "Clear" action should remove all the custom actions, which should bring up + // media buttons... + uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription) + ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " + + "playing media session.") + // ... while the "Full screen" and "Close" buttons should remain in the menu. + assertFullscreenAndCloseButtonsAreShown() + + testApp.closePipWindow() + } + private fun enterPip_openMenu_assertShown(): UiObject2 { testApp.clickEnterPipButton() // Pressing the Window key should bring up Pip menu uiDevice.pressWindowKey() return uiDevice.waitForTvPipMenu() ?: fail("Pip menu should have been shown") } + + private fun assertFullscreenAndCloseButtonsAreShown() { + uiDevice.findTvPipMenuCloseButton() + ?: fail("\"Close PIP\" button should be shown in Pip menu") + uiDevice.findTvPipMenuFullscreenButton() + ?: fail("\"Full screen\" button should be shown in Pip menu") + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt index 8db8bc67da14..0732794903b7 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt @@ -58,6 +58,9 @@ fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? { ?.findObject(buttonSelector) } +fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boolean? = + wait(Until.gone(By.copy(tvPipMenuSelector).hasDescendant(By.desc(desc))), WAIT_TIME_MS) + fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run { height() == uiDevice.displayHeight && width() == uiDevice.displayWidth }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenTest.kt index a0056dfc0948..5570a562a515 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.splitscreen +import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.dsl.FlickerBuilder @@ -24,10 +25,10 @@ import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.wm.shell.flicker.dockedStackDividerIsVisible import com.android.wm.shell.flicker.helpers.SplitScreenHelper.Companion.TEST_REPETITIONS -import com.android.wm.shell.flicker.navBarLayerIsAlwaysVisible -import com.android.wm.shell.flicker.navBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -38,6 +39,7 @@ import org.junit.runners.Parameterized * Test SplitScreen launch. * To run this test: `atest WMShellFlickerTests:EnterSplitScreenTest` */ +@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/ExitSplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/ExitSplitScreenTest.kt index 32e112dbd598..7c47d1f1b1ae 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/ExitSplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/ExitSplitScreenTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.splitscreen +import android.platform.test.annotations.Presubmit import android.util.Rational import android.view.Surface import androidx.test.filters.FlakyTest @@ -28,8 +29,8 @@ import com.android.server.wm.flicker.helpers.resizeSplitScreen import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.wm.shell.flicker.dockedStackDividerIsInvisible import com.android.wm.shell.flicker.helpers.SplitScreenHelper.Companion.TEST_REPETITIONS -import com.android.wm.shell.flicker.navBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -40,6 +41,7 @@ import org.junit.runners.Parameterized * Test exit SplitScreen mode. * To run this test: `atest WMShellFlickerTests:ExitSplitScreenTest` */ +@Presubmit @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/OpenAppToSplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/OpenAppToSplitScreenTest.kt new file mode 100644 index 000000000000..c85561d96091 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/OpenAppToSplitScreenTest.kt @@ -0,0 +1,128 @@ +/* + * 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.flicker.splitscreen + +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:OpenAppToSplitScreenTest` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class OpenAppToSplitScreenTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = StandardAppHelper(instrumentation, + "com.android.wm.shell.flicker.testapp", "SimpleApp") + + // b/161435597 causes the test not to work on 90 degrees + return FlickerTestRunnerFactory(instrumentation, listOf(Surface.ROTATION_0)) + .buildTest { configuration -> + withTestName { + buildTestTag("appToSplitScreen", testApp, configuration) + } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + } + eachRun { + testApp.open() + this.setRotation(configuration.endRotation) + } + } + teardown { + eachRun { + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + } + test { + testApp.exit() + } + } + transitions { + device.launchSplitScreen() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.endRotation, enabled = false) + navBarLayerRotatesAndScales(configuration.endRotation, + bugId = 140855415) + statusBarLayerRotatesScales(configuration.endRotation) + + all("dividerLayerBecomesVisible") { + this.hidesLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) + } + } + + eventLog { + focusChanges(testApp.`package`, + "recents_animation_input_consumer", "NexusLauncherActivity", + bugId = 151179149) + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/ResizeSplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/ResizeSplitScreenTest.kt new file mode 100644 index 000000000000..7c83846621de --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/ResizeSplitScreenTest.kt @@ -0,0 +1,208 @@ +/* + * 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.flicker.splitscreen + +import android.graphics.Region +import android.util.Rational +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.focusDoesNotChange +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.resizeSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test split screen resizing window transitions. + * To run this test: `atest WMShellFlickerTests:ResizeSplitScreenTest` + * + * Currently it runs only in 0 degrees because of b/156100803 + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 159096424) +class ResizeSplitScreenTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + private const val sSimpleActivity = "SimpleActivity" + private const val sImeActivity = "ImeActivity" + private val startRatio = Rational(1, 3) + private val stopRatio = Rational(2, 3) + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testAppTop = StandardAppHelper(instrumentation, + "com.android.wm.shell.flicker.testapp", "SimpleApp") + val testAppBottom = ImeAppHelper(instrumentation) + + return FlickerTestRunnerFactory(instrumentation, listOf(Surface.ROTATION_0)) + .buildTest { configuration -> + withTestName { + val description = (startRatio.toString().replace("/", "-") + "_to_" + + stopRatio.toString().replace("/", "-")) + buildTestTag("resizeSplitScreen", testAppTop.launcherName, + configuration.startRotation, configuration.endRotation, + testAppBottom.launcherName, description) + } + repeat { configuration.repetitions } + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + this.setRotation(configuration.startRotation) + this.launcherStrategy.clearRecentAppsFromOverview() + testAppBottom.open() + device.pressHome() + testAppTop.open() + device.waitForIdle() + device.launchSplitScreen() + val snapshot = + device.findObject(By.res(device.launcherPackageName, "snapshot")) + snapshot.click() + testAppBottom.openIME(device) + device.pressBack() + device.resizeSplitScreen(startRatio) + } + } + teardown { + eachRun { + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + device.pressHome() + testAppTop.exit() + testAppBottom.exit() + } + test { + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + } + } + transitions { + device.resizeSplitScreen(stopRatio) + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + + all("topAppWindowIsAlwaysVisible", bugId = 156223549) { + this.showsAppWindow(sSimpleActivity) + } + + all("bottomAppWindowIsAlwaysVisible", bugId = 156223549) { + this.showsAppWindow(sImeActivity) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.endRotation) + navBarLayerRotatesAndScales(configuration.endRotation) + statusBarLayerRotatesScales(configuration.endRotation) + + all("topAppLayerIsAlwaysVisible") { + this.showsLayer(sSimpleActivity) + } + + all("bottomAppLayerIsAlwaysVisible") { + this.showsLayer(sImeActivity) + } + + all("dividerLayerIsAlwaysVisible") { + this.showsLayer(DOCKED_STACK_DIVIDER) + } + + start("appsStartingBounds", enabled = false) { + val displayBounds = WindowUtils.displayBounds + val entry = this.trace.entries.firstOrNull() + ?: throw IllegalStateException("Trace is empty") + val dividerBounds = + entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + + val topAppBounds = Region(0, 0, dividerBounds.right, + dividerBounds.top + WindowUtils.dockedStackDividerInset) + val bottomAppBounds = Region(0, + dividerBounds.bottom - WindowUtils.dockedStackDividerInset, + displayBounds.right, + displayBounds.bottom - WindowUtils.navigationBarHeight) + this.hasVisibleRegion("SimpleActivity", topAppBounds) + .and() + .hasVisibleRegion("ImeActivity", bottomAppBounds) + } + + end("appsEndingBounds", enabled = false) { + val displayBounds = WindowUtils.displayBounds + val entry = this.trace.entries.lastOrNull() + ?: throw IllegalStateException("Trace is empty") + val dividerBounds = + entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + + val topAppBounds = Region(0, 0, dividerBounds.right, + dividerBounds.top + WindowUtils.dockedStackDividerInset) + val bottomAppBounds = Region(0, + dividerBounds.bottom - WindowUtils.dockedStackDividerInset, + displayBounds.right, + displayBounds.bottom - WindowUtils.navigationBarHeight) + + this.hasVisibleRegion(sSimpleActivity, topAppBounds) + .and() + .hasVisibleRegion(sImeActivity, bottomAppBounds) + } + } + + eventLog { + focusDoesNotChange() + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenRotateOneLaunchedAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenRotateOneLaunchedAppTest.kt new file mode 100644 index 000000000000..d2371bd766f5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenRotateOneLaunchedAppTest.kt @@ -0,0 +1,95 @@ +/* + * 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.flicker.splitscreen + +import androidx.test.filters.FlakyTest +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.repetitions +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:SplitScreenRotateOneLaunchedAppTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest +class SplitScreenRotateOneLaunchedAppTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = StandardAppHelper(instrumentation, + "com.android.wm.shell.flicker.testapp", "SimpleApp") + + return FlickerTestRunnerFactory(instrumentation, repetitions = 3) + .buildTest { configuration -> + withTestName { + buildTestTag("splitScreenRotateOneApp", testApp, configuration) + } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + testApp.open() + device.launchSplitScreen() + device.waitForIdle() + } + eachRun { + this.setRotation(configuration.startRotation) + } + } + teardown { + eachRun { + setRotation(Surface.ROTATION_0) + } + test { + testApp.exit() + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + } + } + transitions { + this.setRotation(configuration.endRotation) + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenRotateTwoLaunchedAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenRotateTwoLaunchedAppTest.kt new file mode 100644 index 000000000000..67346424acd2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenRotateTwoLaunchedAppTest.kt @@ -0,0 +1,104 @@ +/* + * 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.flicker.splitscreen + +import androidx.test.filters.FlakyTest +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.reopenAppFromOverview +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.repetitions +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:SplitScreenRotateTwoLaunchedAppTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest +class SplitScreenRotateTwoLaunchedAppTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = StandardAppHelper(instrumentation, + "com.android.wm.shell.flicker.testapp", "SimpleApp") + val secondaryApp = StandardAppHelper(instrumentation, + "com.android.wm.shell.flicker.testapp", + "SplitScreenSecondaryApp") + + return FlickerTestRunnerFactory(instrumentation, repetitions = 3) + .buildTest { configuration -> + withTestName { + buildTestTag("splitScreenRotateTwoApps", testApp, configuration) + } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + testApp.open() + device.pressHome() + secondaryApp.open() + device.pressHome() + device.launchSplitScreen() + device.reopenAppFromOverview() + device.waitForIdle() + } + eachRun { + this.setRotation(configuration.startRotation) + } + } + teardown { + eachRun { + setRotation(Surface.ROTATION_0) + } + test { + testApp.exit() + secondaryApp.exit() + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + } + } + transitions { + this.setRotation(configuration.endRotation) + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenTestBase.kt index 496fe94ba951..a3440df9ddf8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenTestBase.kt @@ -17,11 +17,10 @@ package com.android.wm.shell.flicker.splitscreen import com.android.wm.shell.flicker.NonRotationTestBase -import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_PRIMARY_COMPONENT_NAME import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_PRIMARY_LABEL -import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_SECONDARY_COMPONENT_NAME import com.android.wm.shell.flicker.TEST_APP_SPLITSCREEN_SECONDARY_LABEL import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.testapp.Components abstract class SplitScreenTestBase( rotationName: String, @@ -29,8 +28,8 @@ abstract class SplitScreenTestBase( ) : NonRotationTestBase(rotationName, rotation) { protected val splitScreenApp = SplitScreenHelper(instrumentation, TEST_APP_SPLITSCREEN_PRIMARY_LABEL, - TEST_APP_SPLITSCREEN_PRIMARY_COMPONENT_NAME) + Components.SplitScreenActivity()) protected val secondaryApp = SplitScreenHelper(instrumentation, TEST_APP_SPLITSCREEN_SECONDARY_LABEL, - TEST_APP_SPLITSCREEN_SECONDARY_COMPONENT_NAME) + Components.SplitScreenSecondaryActivity()) } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenToLauncherTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenToLauncherTest.kt new file mode 100644 index 000000000000..00979fa9fac0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenToLauncherTest.kt @@ -0,0 +1,134 @@ +/* + * 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.flicker.splitscreen + +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.Flicker +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.focusDoesNotChange +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:SplitScreenToLauncherTest` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class SplitScreenToLauncherTest( + testName: String, + flickerSpec: Flicker +) : FlickerTestRunner(testName, flickerSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = StandardAppHelper(instrumentation, + "com.android.wm.shell.flicker.testapp", "SimpleApp") + + // b/161435597 causes the test not to work on 90 degrees + return FlickerTestRunnerFactory(instrumentation, listOf(Surface.ROTATION_0)) + .buildTest { configuration -> + withTestName { + buildTestTag("splitScreenToLauncher", testApp, configuration) + } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + } + eachRun { + testApp.open() + this.setRotation(configuration.endRotation) + device.launchSplitScreen() + device.waitForIdle() + } + } + teardown { + eachRun { + testApp.exit() + } + test { + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + } + } + transitions { + device.exitSplitScreen() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + + layersTrace { + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.endRotation) + navBarLayerRotatesAndScales(configuration.endRotation) + statusBarLayerRotatesScales(configuration.endRotation) + + // b/161435597 causes the test not to work on 90 degrees + all("dividerLayerBecomesInvisible") { + this.showsLayer(DOCKED_STACK_DIVIDER) + .then() + .hidesLayer(DOCKED_STACK_DIVIDER) + } + + all("appLayerBecomesInvisible") { + this.showsLayer(testApp.getPackage()) + .then() + .hidesLayer(testApp.getPackage()) + } + } + + eventLog { + focusDoesNotChange(bugId = 151179149) + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp index d12b49245277..26627a47ee62 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp @@ -18,3 +18,9 @@ android_test { sdk_version: "current", test_suites: ["device-tests"], } + +java_library { + name: "wmshell-flicker-test-components", + srcs: ["src/**/Components.java"], + sdk_version: "test_current", +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml index 2ce120448ac4..a583b725899b 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml @@ -21,13 +21,25 @@ android:targetSdkVersion="29"/> <application android:allowBackup="false" android:supportsRtl="true"> + <activity android:name=".FixedActivity" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:launchMode="singleTop" + android:label="FixedApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> <activity android:name=".PipActivity" - android:resizeableActivity="true" - android:supportsPictureInPicture="true" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" - android:taskAffinity="com.android.wm.shell.flicker.testapp.PipActivity" - android:label="PipApp" - android:exported="true"> + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:taskAffinity="com.android.wm.shell.flicker.testapp.PipActivity" + android:launchMode="singleTop" + android:label="PipApp" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -39,9 +51,9 @@ </activity> <activity android:name=".ImeActivity" - android:taskAffinity="com.android.wm.shell.flicker.testapp.ImeActivity" - android:label="ImeApp" - android:exported="true"> + android:taskAffinity="com.android.wm.shell.flicker.testapp.ImeActivity" + android:label="ImeApp" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -73,5 +85,15 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + + <activity android:name=".SimpleActivity" + android:taskAffinity="com.android.wm.shell.flicker.testapp.SimpleActivity" + android:label="SimpleApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> </application> </manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml index b4a4c165cc7b..e5d2f82080a2 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml @@ -28,6 +28,12 @@ android:text="Enter PIP" android:onClick="enterPip"/> + <CheckBox + android:id="@+id/with_custom_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="With custom actions"/> + <RadioGroup android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml new file mode 100644 index 000000000000..5d94e5177dcc --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/holo_orange_light"> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java new file mode 100644 index 000000000000..8e9b4cb2d53e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java @@ -0,0 +1,82 @@ +/* + * 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.flicker.testapp; + +import android.content.ComponentName; + +public class Components { + public abstract static class ComponentsInfo { + public ComponentName getComponentName() { + return ComponentName.createRelative(PACKAGE_NAME, "." + getActivityName()); + } + public abstract String getActivityName(); + } + + public static final String PACKAGE_NAME = "com.android.wm.shell.flicker.testapp"; + + public static class FixedActivity extends ComponentsInfo { + // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation} + public static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation"; + + @Override + public String getActivityName() { + return FixedActivity.class.getSimpleName(); + } + } + + public static class PipActivity extends ComponentsInfo { + // Intent action that this activity dynamically registers to enter picture-in-picture + public static final String ACTION_ENTER_PIP = PACKAGE_NAME + ".PipActivity.ENTER_PIP"; + // Intent action that this activity dynamically registers to set requested orientation. + // Will apply the oriention to the value set in the EXTRA_FIXED_ORIENTATION extra. + public static final String ACTION_SET_REQUESTED_ORIENTATION = + PACKAGE_NAME + ".PipActivity.SET_REQUESTED_ORIENTATION"; + + // Calls enterPictureInPicture() on creation + public static final String EXTRA_ENTER_PIP = "enter_pip"; + // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation} + public static final String EXTRA_PIP_ORIENTATION = "fixed_orientation"; + // Adds a click listener to finish this activity when it is clicked + public static final String EXTRA_TAP_TO_FINISH = "tap_to_finish"; + + @Override + public String getActivityName() { + return PipActivity.class.getSimpleName(); + } + } + + public static class ImeActivity extends ComponentsInfo { + @Override + public String getActivityName() { + return ImeActivity.class.getSimpleName(); + } + } + + public static class SplitScreenActivity extends ComponentsInfo { + @Override + public String getActivityName() { + return SplitScreenActivity.class.getSimpleName(); + } + } + + public static class SplitScreenSecondaryActivity extends ComponentsInfo { + @Override + public String getActivityName() { + return SplitScreenSecondaryActivity.class.getSimpleName(); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java new file mode 100644 index 000000000000..d4ae6c1313bf --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java @@ -0,0 +1,35 @@ +/* + * 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.flicker.testapp; + +import static com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION; + +import android.os.Bundle; + +public class FixedActivity extends SimpleActivity { + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // Set the fixed orientation if requested + if (getIntent().hasExtra(EXTRA_FIXED_ORIENTATION)) { + final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_FIXED_ORIENTATION)); + setRequestedOrientation(ori); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java index d2fcd0d31558..a6ba7823e22d 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java @@ -24,18 +24,38 @@ import static android.media.session.PlaybackState.STATE_PAUSED; import static android.media.session.PlaybackState.STATE_PLAYING; import static android.media.session.PlaybackState.STATE_STOPPED; +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP; +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP; +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION; + import android.app.Activity; +import android.app.PendingIntent; import android.app.PictureInPictureParams; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.drawable.Icon; import android.media.MediaMetadata; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.os.Bundle; +import android.util.Log; import android.util.Rational; import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.widget.CheckBox; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; public class PipActivity extends Activity { + private static final String TAG = PipActivity.class.getSimpleName(); /** * A media session title for when the session is in {@link STATE_PLAYING}. * TvPipNotificationTests check whether the actual notification title matches this string. @@ -52,7 +72,19 @@ public class PipActivity extends Activity { private static final Rational RATIO_WIDE = new Rational(2, 1); private static final Rational RATIO_TALL = new Rational(1, 2); - private PictureInPictureParams.Builder mPipParamsBuilder; + private static final String PIP_ACTION_NO_OP = "No-Op"; + private static final String PIP_ACTION_OFF = "Off"; + private static final String PIP_ACTION_ON = "On"; + private static final String PIP_ACTION_CLEAR = "Clear"; + private static final String ACTION_NO_OP = "com.android.wm.shell.flicker.testapp.NO_OP"; + private static final String ACTION_SWITCH_OFF = + "com.android.wm.shell.flicker.testapp.SWITCH_OFF"; + private static final String ACTION_SWITCH_ON = "com.android.wm.shell.flicker.testapp.SWITCH_ON"; + private static final String ACTION_CLEAR = "com.android.wm.shell.flicker.testapp.CLEAR"; + + private final PictureInPictureParams.Builder mPipParamsBuilder = + new PictureInPictureParams.Builder() + .setAspectRatio(RATIO_DEFAULT); private MediaSession mMediaSession; private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder() .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP) @@ -60,6 +92,46 @@ public class PipActivity extends Activity { private PlaybackState mPlaybackState = mPlaybackStateBuilder.build(); private final MediaMetadata.Builder mMediaMetadataBuilder = new MediaMetadata.Builder(); + private final List<RemoteAction> mSwitchOffActions = new ArrayList<>(); + private final List<RemoteAction> mSwitchOnActions = new ArrayList<>(); + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isInPictureInPictureMode()) { + switch (intent.getAction()) { + case ACTION_SWITCH_ON: + mPipParamsBuilder.setActions(mSwitchOnActions); + break; + case ACTION_SWITCH_OFF: + mPipParamsBuilder.setActions(mSwitchOffActions); + break; + case ACTION_CLEAR: + mPipParamsBuilder.setActions(Collections.emptyList()); + break; + case ACTION_NO_OP: + return; + default: + Log.w(TAG, "Unhandled action=" + intent.getAction()); + return; + } + setPictureInPictureParams(mPipParamsBuilder.build()); + } else { + switch (intent.getAction()) { + case ACTION_ENTER_PIP: + enterPip(null); + break; + case ACTION_SET_REQUESTED_ORIENTATION: + setRequestedOrientation(Integer.parseInt(intent.getStringExtra( + EXTRA_PIP_ORIENTATION))); + break; + default: + Log.w(TAG, "Unhandled action=" + intent.getAction()); + return; + } + } + } + }; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -72,9 +144,6 @@ public class PipActivity extends Activity { setContentView(R.layout.activity_pip); - mPipParamsBuilder = new PictureInPictureParams.Builder() - .setAspectRatio(RATIO_DEFAULT); - findViewById(R.id.media_session_start) .setOnClickListener(v -> updateMediaSessionState(STATE_PLAYING)); findViewById(R.id.media_session_stop) @@ -98,9 +167,52 @@ public class PipActivity extends Activity { updateMediaSessionState(STATE_STOPPED); } }); + + // Build two sets of the custom actions. We'll replace one with the other when 'On'/'Off' + // action is invoked. + // The first set consists of 3 actions: 1) Off; 2) No-Op; 3) Clear. + // The second set consists of 2 actions: 1) On; 2) Clear. + // Upon invocation 'Clear' action clear-off all the custom actions, including itself. + final Icon icon = Icon.createWithResource(this, android.R.drawable.ic_menu_help); + final RemoteAction noOpAction = buildRemoteAction(icon, PIP_ACTION_NO_OP, ACTION_NO_OP); + final RemoteAction switchOnAction = + buildRemoteAction(icon, PIP_ACTION_ON, ACTION_SWITCH_ON); + final RemoteAction switchOffAction = + buildRemoteAction(icon, PIP_ACTION_OFF, ACTION_SWITCH_OFF); + final RemoteAction clearAllAction = buildRemoteAction(icon, PIP_ACTION_CLEAR, ACTION_CLEAR); + mSwitchOffActions.addAll(Arrays.asList(switchOnAction, clearAllAction)); + mSwitchOnActions.addAll(Arrays.asList(noOpAction, switchOffAction, clearAllAction)); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_NO_OP); + filter.addAction(ACTION_SWITCH_ON); + filter.addAction(ACTION_SWITCH_OFF); + filter.addAction(ACTION_CLEAR); + filter.addAction(ACTION_SET_REQUESTED_ORIENTATION); + filter.addAction(ACTION_ENTER_PIP); + registerReceiver(mBroadcastReceiver, filter); + + handleIntentExtra(getIntent()); + } + + @Override + protected void onDestroy() { + unregisterReceiver(mBroadcastReceiver); + super.onDestroy(); + } + + private RemoteAction buildRemoteAction(Icon icon, String label, String action) { + final Intent intent = new Intent(action); + final PendingIntent pendingIntent = + PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + return new RemoteAction(icon, label, label, pendingIntent); } public void enterPip(View v) { + final boolean withCustomActions = + ((CheckBox) findViewById(R.id.with_custom_actions)).isChecked(); + mPipParamsBuilder.setActions( + withCustomActions ? mSwitchOnActions : Collections.emptyList()); enterPictureInPictureMode(mPipParamsBuilder.build()); } @@ -153,4 +265,17 @@ public class PipActivity extends Activity { mMediaSession.setMetadata(mMediaMetadataBuilder.build()); mMediaSession.setActive(newState != STATE_STOPPED); } + + private void handleIntentExtra(Intent intent) { + // Set the fixed orientation if requested + if (intent.hasExtra(EXTRA_PIP_ORIENTATION)) { + final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION)); + setRequestedOrientation(ori); + } + // Enter picture in picture with the given aspect ratio if provided + if (intent.hasExtra(EXTRA_ENTER_PIP)) { + mPipParamsBuilder.setActions(mSwitchOnActions); + enterPip(null); + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java new file mode 100644 index 000000000000..5343c1893d4e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java @@ -0,0 +1,33 @@ +/* + * 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.wm.shell.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; +import android.view.WindowManager; + +public class SimpleActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WindowManager.LayoutParams p = getWindow().getAttributes(); + p.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(p); + setContentView(R.layout.activity_simple); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index fdf4d31f0281..3ff750af7ec9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -108,8 +108,7 @@ public class ShellTaskOrganizerTests { doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList()) .when(mTaskOrganizerController).registerTaskOrganizer(any()); } catch (RemoteException e) {} - mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mSyncTransactionQueue, - mTransactionPool, mTestExecutor, mTestExecutor, mContext)); + mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java index 754f73246c86..8dbc1d56bcc2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java @@ -34,7 +34,6 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TaskStackListenerImpl; import org.junit.After; import org.junit.Before; @@ -52,19 +51,17 @@ public class AppPairTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private DisplayController mDisplayController; - @Mock private TaskStackListenerImpl mTaskStackListener; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new TestAppPairsController( - mTaskOrganizer, - mSyncQueue, - mDisplayController, - mTaskStackListener); when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); when(mDisplayController.getDisplay(anyInt())).thenReturn( mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); + mController = new TestAppPairsController( + mTaskOrganizer, + mSyncQueue, + mDisplayController); } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java index 6d441ab898ec..fada694a4c07 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java @@ -34,7 +34,6 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TaskStackListenerImpl; import org.junit.After; import org.junit.Before; @@ -52,20 +51,18 @@ public class AppPairsControllerTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private DisplayController mDisplayController; - @Mock private TaskStackListenerImpl mTaskStackListener; @Before public void setUp() { MockitoAnnotations.initMocks(this); + when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); + when(mDisplayController.getDisplay(anyInt())).thenReturn( + mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); mController = new TestAppPairsController( mTaskOrganizer, mSyncQueue, - mDisplayController, - mTaskStackListener); + mDisplayController); mPool = mController.getPool(); - when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); - when(mDisplayController.getDisplay(anyInt())).thenReturn( - mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java index d3dbbfe37985..a3f134ee97ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java @@ -18,13 +18,16 @@ package com.android.wm.shell.apppairs; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TaskStackListenerImpl; import org.junit.After; import org.junit.Before; @@ -36,22 +39,21 @@ import org.mockito.MockitoAnnotations; /** Tests for {@link AppPairsPool} */ @SmallTest @RunWith(AndroidJUnit4.class) -public class AppPairsPoolTests { +public class AppPairsPoolTests extends ShellTestCase { private TestAppPairsController mController; private TestAppPairsPool mPool; @Mock private SyncTransactionQueue mSyncQueue; @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private DisplayController mDisplayController; - @Mock private TaskStackListenerImpl mTaskStackListener; @Before public void setUp() { MockitoAnnotations.initMocks(this); + when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); mController = new TestAppPairsController( mTaskOrganizer, mSyncQueue, - mDisplayController, - mTaskStackListener); + mDisplayController); mPool = mController.getPool(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java index e61cc91c394b..be0963628933 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java @@ -19,14 +19,13 @@ package com.android.wm.shell.apppairs; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TaskStackListenerImpl; public class TestAppPairsController extends AppPairsController { TestAppPairsPool mPool; public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, - DisplayController displayController, TaskStackListenerImpl taskStackListener) { - super(organizer, syncQueue, displayController, taskStackListener); + DisplayController displayController) { + super(organizer, syncQueue, displayController); mPool = new TestAppPairsPool(this); setPairsPool(mPool); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java index 080cddc58a09..5e0d51809d44 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -20,12 +20,15 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.InsetsState.ITYPE_IME; import static android.view.Surface.ROTATION_0; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import android.graphics.Point; +import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.SurfaceControl; @@ -68,19 +71,31 @@ public class DisplayImeControllerTest { @Test public void reappliesVisibilityToChangedLeash() { verifyZeroInteractions(mT); + mPerDisplay.mImeShowing = true; - mPerDisplay.mImeShowing = false; - mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[] { - new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0)) - }); + mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); + assertFalse(mPerDisplay.mImeShowing); verify(mT).hide(any()); mPerDisplay.mImeShowing = true; - mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[] { - new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0)) - }); + mPerDisplay.insetsControlChanged(insetsStateWithIme(true), insetsSourceControl()); + assertTrue(mPerDisplay.mImeShowing); verify(mT).show(any()); } + + private InsetsSourceControl[] insetsSourceControl() { + return new InsetsSourceControl[]{ + new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0)) + }; + } + + private InsetsState insetsStateWithIme(boolean visible) { + InsetsState state = new InsetsState(); + state.addSource(new InsetsSource(ITYPE_IME)); + state.setSourceVisible(ITYPE_IME, visible); + return state; + } + } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java new file mode 100644 index 000000000000..d87f4c60fad4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -0,0 +1,106 @@ +/* + * 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.common.split; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link SplitLayout} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitLayoutTests extends ShellTestCase { + @Mock SplitLayout.LayoutChangeListener mLayoutChangeListener; + @Mock SurfaceControl mRootLeash; + private SplitLayout mSplitLayout; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mSplitLayout = new SplitLayout( + mContext, + getConfiguration(false), + mLayoutChangeListener, + mRootLeash); + } + + @Test + @UiThreadTest + public void testUpdateConfiguration() { + assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse(); + assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue(); + } + + @Test + public void testUpdateDividePosition() { + mSplitLayout.updateDividePosition(anyInt()); + verify(mLayoutChangeListener).onBoundsChanging(any(SplitLayout.class)); + } + + @Test + public void testSetSnapTarget() { + DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0, + DividerSnapAlgorithm.SnapTarget.FLAG_NONE); + mSplitLayout.setSnapTarget(snapTarget); + verify(mLayoutChangeListener).onBoundsChanged(any(SplitLayout.class)); + + // verify it callbacks properly when the snap target indicates dismissing split. + snapTarget = getSnapTarget(0, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START); + mSplitLayout.setSnapTarget(snapTarget); + verify(mLayoutChangeListener).onSnappedToDismiss(eq(false)); + snapTarget = getSnapTarget(0, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END); + mSplitLayout.setSnapTarget(snapTarget); + verify(mLayoutChangeListener).onSnappedToDismiss(eq(true)); + } + + private static Configuration getConfiguration(boolean isLandscape) { + final Configuration configuration = new Configuration(); + configuration.unset(); + configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; + configuration.windowConfiguration.setBounds( + new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160)); + return configuration; + } + + private static DividerSnapAlgorithm.SnapTarget getSnapTarget(int position, int flag) { + return new DividerSnapAlgorithm.SnapTarget( + position /* position */, position /* taskPosition */, flag); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java new file mode 100644 index 000000000000..aa0eb2f95ed8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java @@ -0,0 +1,66 @@ +/* + * 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.common.split; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link SplitWindowManager} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitWindowManagerTests extends ShellTestCase { + @Mock SurfaceControl mSurfaceControl; + @Mock SplitLayout mSplitLayout; + private SplitWindowManager mSplitWindowManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + final Configuration configuration = new Configuration(); + configuration.setToDefaults(); + mSplitWindowManager = new SplitWindowManager(mContext, configuration, mSurfaceControl); + when(mSplitLayout.getDividerBounds()).thenReturn( + new Rect(0, 0, configuration.windowConfiguration.getBounds().width(), + configuration.windowConfiguration.getBounds().height())); + } + + @Test + @UiThreadTest + public void testInitRelease() { + mSplitWindowManager.init(mSplitLayout); + assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull(); + mSplitWindowManager.release(); + assertThat(mSplitWindowManager.getSurfaceControl()).isNull(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index fad1f057267a..92d4bee7cfc2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -42,7 +42,7 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.app.ActivityManager; -import android.app.IActivityTaskManager; +import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; @@ -69,7 +69,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.ArrayList; @@ -88,7 +87,7 @@ public class DragAndDropPolicyTest { private Context mContext; @Mock - private IActivityTaskManager mIActivityTaskManager; + private ActivityTaskManager mActivityTaskManager; @Mock private SplitScreen mSplitScreen; @@ -134,7 +133,7 @@ public class DragAndDropPolicyTest { return null; }).when(mSplitScreen).registerInSplitScreenListener(any()); - mPolicy = new DragAndDropPolicy(mContext, mIActivityTaskManager, mSplitScreen, mStarter); + mPolicy = new DragAndDropPolicy(mContext, mActivityTaskManager, mSplitScreen, mStarter); mActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); mNonResizeableActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); setClipDataResizeable(mNonResizeableActivityClipData, false); @@ -188,9 +187,9 @@ public class DragAndDropPolicyTest { return info; } - private void setRunningTask(ActivityManager.RunningTaskInfo task) throws RemoteException { - doReturn(Collections.singletonList(task)).when(mIActivityTaskManager) - .getFilteredTasks(anyInt(), anyBoolean()); + private void setRunningTask(ActivityManager.RunningTaskInfo task) { + doReturn(Collections.singletonList(task)).when(mActivityTaskManager) + .getTasks(anyInt(), anyBoolean()); } private void setClipDataResizeable(ClipData data, boolean resizeable) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/letterbox/LetterboxTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/letterbox/LetterboxTaskListenerTest.java index 0f719afd08b3..5cbc7d927d61 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/letterbox/LetterboxTaskListenerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/letterbox/LetterboxTaskListenerTest.java @@ -96,9 +96,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 200, 100), /* activityBounds */ new Rect(75, 0, 125, 75), - /* taskBounds */ new Rect(50, 0, 125, 100)), + /* taskBounds */ new Rect(50, 0, 125, 100), + /* activityInsets */ new Rect(0, 0, 0, 0)), mLeash); // Task doesn't need to repositioned @@ -109,10 +111,12 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskInfoChanged( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 200, 100), // Activity is offset by 25 to the left /* activityBounds */ new Rect(50, 0, 100, 75), - /* taskBounds */ new Rect(50, 0, 125, 100))); + /* taskBounds */ new Rect(50, 0, 125, 100), + /* activityInsets */ new Rect(0, 0, 0, 0))); // Task needs to be repositioned by 25 to the left verifySetPosition(75, 0); @@ -130,9 +134,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 200, 100), /* activityBounds */ new Rect(150, 0, 200, 75), - /* taskBounds */ new Rect(125, 0, 200, 100)), + /* taskBounds */ new Rect(125, 0, 200, 100), + /* activityInsets */ new Rect(0, 10, 10, 0)), mLeash); verifySetPosition(-15, 0); @@ -150,9 +156,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 200, 100), /* activityBounds */ new Rect(150, 0, 200, 75), - /* taskBounds */ new Rect(125, 0, 200, 100)), + /* taskBounds */ new Rect(125, 0, 200, 100), + /* activityInsets */ new Rect(0, 10, 10, 0)), mLeash); verifySetPosition(55, 0); @@ -170,9 +178,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 200, 100), /* activityBounds */ new Rect(50, 0, 100, 75), - /* taskBounds */ new Rect(25, 0, 100, 100)), + /* taskBounds */ new Rect(25, 0, 100, 100), + /* activityInsets */ new Rect(0, 10, 10, 0)), mLeash); verifySetPosition(115, 0); @@ -190,9 +200,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 100, 150), /* activityBounds */ new Rect(0, 75, 50, 125), - /* taskBounds */ new Rect(0, 50, 100, 125)), + /* taskBounds */ new Rect(0, 50, 100, 125), + /* activityInsets */ new Rect(10, 0, 0, 0)), mLeash); verifySetPosition(20, -15); @@ -210,9 +222,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 100, 150), /* activityBounds */ new Rect(0, 75, 50, 125), - /* taskBounds */ new Rect(0, 50, 100, 125)), + /* taskBounds */ new Rect(0, 50, 100, 125), + /* activityInsets */ new Rect(10, 0, 0, 0)), mLeash); verifySetPosition(20, 20); @@ -221,6 +235,29 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { } @Test + public void testOnTaskInfoAppeared_portraitWithCenterGravity_visibleLeftInset() { + mLetterboxConfigController.setPortraitGravity(Gravity.CENTER); + setWindowBoundsAndInsets( + /* windowBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds + Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 20)); + + mLetterboxTaskListener.onTaskAppeared( + createTaskInfo( + /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds + /* parentBounds */ new Rect(0, 0, 100, 150), + /* activityBounds */ new Rect(0, 75, 50, 125), + /* taskBounds */ new Rect(0, 50, 100, 125), + // Activity is drawn under the left inset. + /* activityInsets */ new Rect(0, 0, 0, 0)), + mLeash); + + verifySetPosition(20, 20); + // Should return activity coordinates offset by task coordinates + verifySetWindowCrop(new Rect(0, 25, 50, 75)); + } + + @Test public void testOnTaskInfoAppeared_portraitWithBottomGravity() { mLetterboxConfigController.setPortraitGravity(Gravity.BOTTOM); setWindowBoundsAndInsets( @@ -230,9 +267,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 100, 150), /* activityBounds */ new Rect(0, 75, 50, 125), - /* taskBounds */ new Rect(0, 50, 100, 125)), + /* taskBounds */ new Rect(0, 50, 100, 125), + /* activityInsets */ new Rect(10, 0, 0, 0)), mLeash); verifySetPosition(20, 55); @@ -250,16 +289,17 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 200, 125), // equal to parent bounds /* parentBounds */ new Rect(0, 0, 200, 125), /* activityBounds */ new Rect(15, 0, 175, 120), - /* taskBounds */ new Rect(0, 0, 100, 125)), // equal to parent bounds + /* taskBounds */ new Rect(0, 0, 200, 125), + /* activityInsets */ new Rect(10, 25, 10, 10)), // equal to parent bounds mLeash); // Activity fully covers parent bounds with insets so doesn't need to be moved. verifySetPosition(0, 0); - // Should return activity coordinates offset by task coordinates minus all insets - // except top one (keep status bar decor visible). - verifySetWindowCrop(new Rect(25, 0, 165, 110)); + // Should return activity coordinates offset by task coordinates + verifySetWindowCrop(new Rect(15, 0, 175, 120)); } @Test @@ -272,9 +312,11 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { mLetterboxTaskListener.onTaskAppeared( createTaskInfo( /* taskId */ 1, + /* maxBounds= */ new Rect(0, 0, 100, 150), /* parentBounds */ new Rect(0, 75, 100, 225), /* activityBounds */ new Rect(25, 75, 75, 125), - /* taskBounds */ new Rect(0, 75, 100, 125)), + /* taskBounds */ new Rect(0, 75, 100, 125), + /* activityInsets */ new Rect(10, 0, 0, 0)), mLeash); verifySetPosition(0, 0); @@ -285,7 +327,8 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { public void testOnTaskAppeared_calledSecondTimeWithSameTaskId_throwsException() { setWindowBoundsAndInsets(new Rect(), Insets.NONE); RunningTaskInfo taskInfo = - createTaskInfo(/* taskId */ 1, new Rect(), new Rect(), new Rect()); + createTaskInfo(/* taskId */ 1, new Rect(), new Rect(), new Rect(), new Rect(), + new Rect()); mLetterboxTaskListener.onTaskAppeared(taskInfo, mLeash); mLetterboxTaskListener.onTaskAppeared(taskInfo, mLeash); } @@ -306,14 +349,18 @@ public final class LetterboxTaskListenerTest extends ShellTestCase { private static RunningTaskInfo createTaskInfo( int taskId, + final Rect maxBounds, final Rect parentBounds, final Rect activityBounds, - final Rect taskBounds) { + final Rect taskBounds, + final Rect activityInsets) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setMaxBounds(maxBounds); taskInfo.parentBounds = parentBounds; taskInfo.configuration.windowConfiguration.setBounds(taskBounds); taskInfo.letterboxActivityBounds = Rect.copyOrNull(activityBounds); + taskInfo.letterboxActivityInsets = Rect.copyOrNull(activityInsets); return taskInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java index 55e7a354f4cd..7f280cd124d2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java @@ -79,7 +79,8 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void getAnimator_withBounds_returnBoundsAnimator() { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, new Rect(), new Rect(), null, TRANSITION_DIRECTION_TO_PIP); + .getAnimator(mLeash, new Rect(), new Rect(), new Rect(), null, + TRANSITION_DIRECTION_TO_PIP); assertEquals("Expect ANIM_TYPE_BOUNDS animation", animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); @@ -87,16 +88,19 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void getAnimator_whenSameTypeRunning_updateExistingAnimator() { + final Rect baseValue = new Rect(0, 0, 100, 100); final Rect startValue = new Rect(0, 0, 100, 100); final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue1, null, TRANSITION_DIRECTION_TO_PIP); + .getAnimator(mLeash, baseValue, startValue, endValue1, null, + TRANSITION_DIRECTION_TO_PIP); oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue2, null, TRANSITION_DIRECTION_TO_PIP); + .getAnimator(mLeash, baseValue, startValue, endValue2, null, + TRANSITION_DIRECTION_TO_PIP); assertEquals("getAnimator with same type returns same animator", oldAnimator, newAnimator); @@ -122,11 +126,13 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test @SuppressWarnings("unchecked") public void pipTransitionAnimator_updateEndValue() { + final Rect baseValue = new Rect(0, 0, 100, 100); final Rect startValue = new Rect(0, 0, 100, 100); final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue1, null, TRANSITION_DIRECTION_TO_PIP); + .getAnimator(mLeash, baseValue, startValue, endValue1, null, + TRANSITION_DIRECTION_TO_PIP); animator.updateEndValue(endValue2); @@ -135,10 +141,12 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void pipTransitionAnimator_setPipAnimationCallback() { + final Rect baseValue = new Rect(0, 0, 100, 100); final Rect startValue = new Rect(0, 0, 100, 100); final Rect endValue = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue, null, TRANSITION_DIRECTION_TO_PIP); + .getAnimator(mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP); animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); animator.setPipAnimationCallback(mPipAnimationCallback); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java index a65d832359d2..ef9923550fc5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java @@ -337,7 +337,8 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { reentryBounds.scale(1.25f); final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds); - mPipBoundsState.saveReentryState(reentryBounds, reentrySnapFraction); + mPipBoundsState.saveReentryState( + new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); assertEquals(reentryBounds.width(), destinationBounds.width()); @@ -351,7 +352,8 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { reentryBounds.offset(0, -100); final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds); - mPipBoundsState.saveReentryState(reentryBounds, reentrySnapFraction); + mPipBoundsState.saveReentryState( + new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java index 4bcca06b592f..8ba301a9ebfa 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java @@ -47,7 +47,7 @@ import java.util.function.BiConsumer; @SmallTest public class PipBoundsStateTest extends ShellTestCase { - private static final Rect DEFAULT_BOUNDS = new Rect(0, 0, 10, 10); + private static final Size DEFAULT_SIZE = new Size(10, 10); private static final float DEFAULT_SNAP_FRACTION = 1.0f; private PipBoundsState mPipBoundsState; @@ -71,22 +71,22 @@ public class PipBoundsStateTest extends ShellTestCase { @Test public void testSetReentryState() { - final Rect bounds = new Rect(0, 0, 100, 100); + final Size size = new Size(100, 100); final float snapFraction = 0.5f; - mPipBoundsState.saveReentryState(bounds, snapFraction); + mPipBoundsState.saveReentryState(size, snapFraction); final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); - assertEquals(new Size(100, 100), state.getSize()); + assertEquals(size, state.getSize()); assertEquals(snapFraction, state.getSnapFraction(), 0.01); } @Test public void testClearReentryState() { - final Rect bounds = new Rect(0, 0, 100, 100); + final Size size = new Size(100, 100); final float snapFraction = 0.5f; - mPipBoundsState.saveReentryState(bounds, snapFraction); + mPipBoundsState.saveReentryState(size, snapFraction); mPipBoundsState.clearReentryState(); assertNull(mPipBoundsState.getReentryState()); @@ -95,20 +95,20 @@ public class PipBoundsStateTest extends ShellTestCase { @Test public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() { mPipBoundsState.setLastPipComponentName(mTestComponentName1); - mPipBoundsState.saveReentryState(DEFAULT_BOUNDS, DEFAULT_SNAP_FRACTION); + mPipBoundsState.saveReentryState(DEFAULT_SIZE, DEFAULT_SNAP_FRACTION); mPipBoundsState.setLastPipComponentName(mTestComponentName1); final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); assertNotNull(state); - assertEquals(new Size(DEFAULT_BOUNDS.width(), DEFAULT_BOUNDS.height()), state.getSize()); + assertEquals(DEFAULT_SIZE, state.getSize()); assertEquals(DEFAULT_SNAP_FRACTION, state.getSnapFraction(), 0.01); } @Test public void testSetLastPipComponentName_changed_clearReentryState() { mPipBoundsState.setLastPipComponentName(mTestComponentName1); - mPipBoundsState.saveReentryState(DEFAULT_BOUNDS, DEFAULT_SNAP_FRACTION); + mPipBoundsState.saveReentryState(DEFAULT_SIZE, DEFAULT_SNAP_FRACTION); mPipBoundsState.setLastPipComponentName(mTestComponentName2); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 8d3774cee1e0..45e4241d5bc6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -44,7 +44,7 @@ import android.window.WindowContainerToken; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.pip.phone.PipMenuActivityController; +import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreen; import org.junit.Before; @@ -66,7 +66,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private DisplayController mMockdDisplayController; @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; - @Mock private PipMenuActivityController mMenuActivityController; + @Mock private PhonePipMenuController mMockPhonePipMenuController; @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; @Mock private PipUiEventLogger mMockPipUiEventLogger; @Mock private Optional<SplitScreen> mMockOptionalSplitScreen; @@ -83,9 +83,9 @@ public class PipTaskOrganizerTest extends ShellTestCase { mComponent2 = new ComponentName(mContext, "component2"); mPipBoundsState = new PipBoundsState(mContext); mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, mPipBoundsState, - mMockPipBoundsAlgorithm, mMenuActivityController, mMockPipSurfaceTransactionHelper, - mMockOptionalSplitScreen, mMockdDisplayController, mMockPipUiEventLogger, - mMockShellTaskOrganizer)); + mMockPipBoundsAlgorithm, mMockPhonePipMenuController, + mMockPipSurfaceTransactionHelper, mMockOptionalSplitScreen, mMockdDisplayController, + mMockPipUiEventLogger, mMockShellTaskOrganizer)); preparePipTaskOrg(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 88c8eb902a6f..62ffac4fbd3f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -30,10 +30,12 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.Rect; import android.os.RemoteException; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Size; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.WindowManagerShellWrapper; @@ -61,7 +63,7 @@ public class PipControllerTest extends ShellTestCase { private PipController mPipController; @Mock private DisplayController mMockDisplayController; - @Mock private PipMenuActivityController mMockPipMenuActivityController; + @Mock private PhonePipMenuController mMockPhonePipMenuController; @Mock private PipAppOpsListener mMockPipAppOpsListener; @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; @Mock private PipMediaController mMockPipMediaController; @@ -77,7 +79,7 @@ public class PipControllerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mPipController = new PipController(mContext, mMockDisplayController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, - mMockPipMediaController, mMockPipMenuActivityController, mMockPipTaskOrganizer, + mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockExecutor); doAnswer(invocation -> { @@ -110,7 +112,7 @@ public class PipControllerTest extends ShellTestCase { assertNull(PipController.create(spyContext, mMockDisplayController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, - mMockPipMediaController, mMockPipMenuActivityController, mMockPipTaskOrganizer, + mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockExecutor)); } @@ -135,4 +137,28 @@ public class PipControllerTest extends ShellTestCase { verify(mMockPipBoundsState, never()).setLastPipComponentName(null); } + + @Test + public void saveReentryState_noUserResize_doesNotSaveSize() { + final Rect bounds = new Rect(0, 0, 10, 10); + when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); + when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(false); + + mPipController.saveReentryState(bounds); + + verify(mMockPipBoundsState).saveReentryState(null, 1.0f); + } + + @Test + public void saveReentryState_userHasResized_savesSize() { + final Rect bounds = new Rect(0, 0, 10, 10); + final Rect resizedBounds = new Rect(0, 0, 30, 30); + when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); + when(mMockPipTouchHandler.getUserResizeBounds()).thenReturn(resizedBounds); + when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(true); + + mPipController.saveReentryState(bounds); + + verify(mMockPipBoundsState).saveReentryState(new Size(30, 30), 1.0f); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index abbc681f53fe..4efaebf96c2b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -60,7 +61,7 @@ public class PipTouchHandlerTest extends ShellTestCase { private PipTouchHandler mPipTouchHandler; @Mock - private PipMenuActivityController mPipMenuActivityController; + private PhonePipMenuController mPhonePipMenuController; @Mock private PipTaskOrganizer mPipTaskOrganizer; @@ -71,6 +72,9 @@ public class PipTouchHandlerTest extends ShellTestCase { @Mock private PipUiEventLogger mPipUiEventLogger; + @Mock + private ShellExecutor mShellMainExecutor; + private PipBoundsState mPipBoundsState; private PipBoundsAlgorithm mPipBoundsAlgorithm; private PipSnapAlgorithm mPipSnapAlgorithm; @@ -92,9 +96,9 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState); mPipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); mPipSnapAlgorithm = new PipSnapAlgorithm(); - mPipTouchHandler = new PipTouchHandler(mContext, mPipMenuActivityController, + mPipTouchHandler = new PipTouchHandler(mContext, mPhonePipMenuController, mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, - mFloatingContentCoordinator, mPipUiEventLogger); + mFloatingContentCoordinator, mPipUiEventLogger, mShellMainExecutor); mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper()); mPipResizeGestureHandler = Mockito.spy(mPipTouchHandler.getPipResizeGestureHandler()); mPipTouchHandler.setPipMotionHelper(mMotionHelper); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 4ed5457a2a7f..0533aa6ad45d 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -333,6 +333,7 @@ cc_defaults { "jni/YuvToJpegEncoder.cpp", "jni/fonts/Font.cpp", "jni/fonts/FontFamily.cpp", + "jni/fonts/NativeFont.cpp", "jni/text/LineBreaker.cpp", "jni/text/MeasuredText.cpp", "jni/text/TextShaper.cpp", @@ -429,6 +430,7 @@ cc_defaults { whole_static_libs: ["libskia"], srcs: [ + "canvas/CanvasFrontend.cpp", "canvas/CanvasOpBuffer.cpp", "canvas/CanvasOpRasterizer.cpp", "pipeline/skia/SkiaDisplayList.cpp", @@ -607,6 +609,7 @@ cc_test { "tests/unit/CacheManagerTests.cpp", "tests/unit/CanvasContextTests.cpp", "tests/unit/CanvasOpTests.cpp", + "tests/unit/CanvasFrontendTests.cpp", "tests/unit/CommonPoolTests.cpp", "tests/unit/DamageAccumulatorTests.cpp", "tests/unit/DeferredLayerUpdaterTests.cpp", @@ -669,6 +672,7 @@ cc_benchmark { srcs: [ "tests/microbench/main.cpp", + "tests/microbench/CanvasOpBench.cpp", "tests/microbench/DisplayListCanvasBench.cpp", "tests/microbench/LinearAllocatorBench.cpp", "tests/microbench/PathParserBench.cpp", diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h index dc63e5db4a70..dd2476313b19 100644 --- a/libs/hwui/DisplayList.h +++ b/libs/hwui/DisplayList.h @@ -16,14 +16,15 @@ #pragma once -#include "pipeline/skia/SkiaDisplayList.h" - namespace android { namespace uirenderer { namespace VectorDrawable { class Tree; }; +namespace skiapipeline { +class SkiaDisplayList; +} typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot; /** diff --git a/libs/hwui/DisplayListOps.in b/libs/hwui/DisplayListOps.in index 49817925d9b4..c6c4ba8a6493 100644 --- a/libs/hwui/DisplayListOps.in +++ b/libs/hwui/DisplayListOps.in @@ -19,7 +19,6 @@ X(Save) X(Restore) X(SaveLayer) X(SaveBehind) -X(Concat44) X(Concat) X(SetMatrix) X(Scale) diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index fd18d2f9192d..8b20492543f7 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -20,7 +20,7 @@ namespace android { namespace uirenderer { -const std::string FrameInfoNames[] = { +const std::array<std::string, static_cast<int>(FrameInfoIndex::NumIndexes)> FrameInfoNames = { "Flags", "FrameTimelineVsyncId", "IntendedVsync", @@ -42,10 +42,6 @@ const std::string FrameInfoNames[] = { "GpuCompleted", }; -static_assert((sizeof(FrameInfoNames) / sizeof(FrameInfoNames[0])) == - static_cast<int>(FrameInfoIndex::NumIndexes), - "size mismatch: FrameInfoNames doesn't match the enum!"); - static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 19, "Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)"); diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index bb875e35f6f7..ee7d15a2fbce 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -21,6 +21,7 @@ #include <cutils/compiler.h> #include <utils/Timers.h> +#include <array> #include <memory.h> #include <string> @@ -60,7 +61,7 @@ enum class FrameInfoIndex { NumIndexes }; -extern const std::string FrameInfoNames[]; +extern const std::array<std::string, static_cast<int>(FrameInfoIndex::NumIndexes)> FrameInfoNames; namespace FrameInfoFlags { enum { @@ -158,7 +159,7 @@ public: // GPU start time is approximated to the moment before swapBuffer is invoked. // We could add an EGLSyncKHR fence at the beginning of the frame, but that is an overhead. int64_t endTime = get(FrameInfoIndex::GpuCompleted); - return endTime > 0 ? endTime - get(FrameInfoIndex::SwapBuffers) : 0; + return endTime > 0 ? endTime - get(FrameInfoIndex::SwapBuffers) : -1; } inline int64_t& set(FrameInfoIndex index) { return mFrameInfo[static_cast<int>(index)]; } diff --git a/libs/hwui/Layer.cpp b/libs/hwui/Layer.cpp index c174c240ff22..f4c633fbe58f 100644 --- a/libs/hwui/Layer.cpp +++ b/libs/hwui/Layer.cpp @@ -18,6 +18,7 @@ #include "renderstate/RenderState.h" #include "utils/Color.h" +#include "utils/MathUtils.h" namespace android { namespace uirenderer { @@ -52,5 +53,90 @@ SkBlendMode Layer::getMode() const { } } +static inline SkScalar isIntegerAligned(SkScalar x) { + return fabsf(roundf(x) - x) <= NON_ZERO_EPSILON; +} + +// Disable filtering when there is no scaling in screen coordinates and the corners have the same +// fraction (for translate) or zero fraction (for any other rect-to-rect transform). +static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, const SkRect& dstRect) { + if (!matrix.rectStaysRect()) return true; + SkRect dstDevRect = matrix.mapRect(dstRect); + float dstW, dstH; + if (MathUtils::isZero(matrix.getScaleX()) && MathUtils::isZero(matrix.getScaleY())) { + // Has a 90 or 270 degree rotation, although total matrix may also have scale factors + // in m10 and m01. Those scalings are automatically handled by mapRect so comparing + // dimensions is sufficient, but swap width and height comparison. + dstW = dstDevRect.height(); + dstH = dstDevRect.width(); + } else { + // Handle H/V flips or 180 rotation matrices. Axes may have been mirrored, but + // dimensions are still safe to compare directly. + dstW = dstDevRect.width(); + dstH = dstDevRect.height(); + } + if (!(MathUtils::areEqual(dstW, srcRect.width()) && + MathUtils::areEqual(dstH, srcRect.height()))) { + return true; + } + // Device rect and source rect should be integer aligned to ensure there's no difference + // in how nearest-neighbor sampling is resolved. + return !(isIntegerAligned(srcRect.x()) && + isIntegerAligned(srcRect.y()) && + isIntegerAligned(dstDevRect.x()) && + isIntegerAligned(dstDevRect.y())); +} + +void Layer::draw(SkCanvas* canvas) { + GrRecordingContext* context = canvas->recordingContext(); + if (context == nullptr) { + SkDEBUGF(("Attempting to draw LayerDrawable into an unsupported surface")); + return; + } + SkMatrix layerTransform = getTransform(); + //sk_sp<SkImage> layerImage = getImage(); + const int layerWidth = getWidth(); + const int layerHeight = getHeight(); + if (layerImage) { + SkMatrix textureMatrixInv; + textureMatrixInv = getTexTransform(); + // TODO: after skia bug https://bugs.chromium.org/p/skia/issues/detail?id=7075 is fixed + // use bottom left origin and remove flipV and invert transformations. + SkMatrix flipV; + flipV.setAll(1, 0, 0, 0, -1, 1, 0, 0, 1); + textureMatrixInv.preConcat(flipV); + textureMatrixInv.preScale(1.0f / layerWidth, 1.0f / layerHeight); + textureMatrixInv.postScale(layerImage->width(), layerImage->height()); + SkMatrix textureMatrix; + if (!textureMatrixInv.invert(&textureMatrix)) { + textureMatrix = textureMatrixInv; + } + + SkMatrix matrix; + matrix = SkMatrix::Concat(layerTransform, textureMatrix); + + SkPaint paint; + paint.setAlpha(getAlpha()); + paint.setBlendMode(getMode()); + paint.setColorFilter(getColorFilter()); + const bool nonIdentityMatrix = !matrix.isIdentity(); + if (nonIdentityMatrix) { + canvas->save(); + canvas->concat(matrix); + } + const SkMatrix& totalMatrix = canvas->getTotalMatrix(); + + SkRect imageRect = SkRect::MakeIWH(layerImage->width(), layerImage->height()); + if (getForceFilter() || shouldFilterRect(totalMatrix, imageRect, imageRect)) { + paint.setFilterQuality(kLow_SkFilterQuality); + } + canvas->drawImage(layerImage.get(), 0, 0, &paint); + // restore the original matrix + if (nonIdentityMatrix) { + canvas->restore(); + } + } +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/Layer.h b/libs/hwui/Layer.h index ea3bfc9e80cb..e99e76299317 100644 --- a/libs/hwui/Layer.h +++ b/libs/hwui/Layer.h @@ -21,6 +21,7 @@ #include <SkBlendMode.h> #include <SkColorFilter.h> #include <SkColorSpace.h> +#include <SkCanvas.h> #include <SkPaint.h> #include <SkImage.h> #include <SkMatrix.h> @@ -87,6 +88,8 @@ public: inline sk_sp<SkImage> getImage() const { return this->layerImage; } + void draw(SkCanvas* canvas); + protected: RenderState& mRenderState; diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 473dc53dc4bf..a495ec4ac411 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -125,24 +125,18 @@ struct SaveBehind final : Op { } }; -struct Concat44 final : Op { - static const auto kType = Type::Concat44; - Concat44(const SkM44& m) : matrix(m) {} - SkM44 matrix; - void draw(SkCanvas* c, const SkMatrix&) const { c->concat(matrix); } -}; struct Concat final : Op { static const auto kType = Type::Concat; - Concat(const SkMatrix& matrix) : matrix(matrix) {} - SkMatrix matrix; + Concat(const SkM44& matrix) : matrix(matrix) {} + SkM44 matrix; void draw(SkCanvas* c, const SkMatrix&) const { c->concat(matrix); } }; struct SetMatrix final : Op { static const auto kType = Type::SetMatrix; - SetMatrix(const SkMatrix& matrix) : matrix(matrix) {} - SkMatrix matrix; + SetMatrix(const SkM44& matrix) : matrix(matrix) {} + SkM44 matrix; void draw(SkCanvas* c, const SkMatrix& original) const { - c->setMatrix(SkMatrix::Concat(original, matrix)); + c->setMatrix(SkM44(original) * matrix); } }; struct Scale final : Op { @@ -569,12 +563,9 @@ void DisplayListData::saveBehind(const SkRect* subset) { } void DisplayListData::concat(const SkM44& m) { - this->push<Concat44>(0, m); -} -void DisplayListData::concat(const SkMatrix& matrix) { - this->push<Concat>(0, matrix); + this->push<Concat>(0, m); } -void DisplayListData::setMatrix(const SkMatrix& matrix) { +void DisplayListData::setMatrix(const SkM44& matrix) { this->push<SetMatrix>(0, matrix); } void DisplayListData::scale(SkScalar sx, SkScalar sy) { @@ -834,10 +825,7 @@ bool RecordingCanvas::onDoSaveBehind(const SkRect* subset) { void RecordingCanvas::didConcat44(const SkM44& m) { fDL->concat(m); } -void RecordingCanvas::didConcat(const SkMatrix& matrix) { - fDL->concat(matrix); -} -void RecordingCanvas::didSetMatrix(const SkMatrix& matrix) { +void RecordingCanvas::didSetM44(const SkM44& matrix) { fDL->setMatrix(matrix); } void RecordingCanvas::didScale(SkScalar sx, SkScalar sy) { diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 63d120c4ca19..4851148cd4d8 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -82,8 +82,7 @@ private: void restore(); void concat(const SkM44&); - void concat(const SkMatrix&); - void setMatrix(const SkMatrix&); + void setMatrix(const SkM44&); void scale(SkScalar, SkScalar); void translate(SkScalar, SkScalar); void translateZ(SkScalar); @@ -154,8 +153,7 @@ public: void onFlush() override; void didConcat44(const SkM44&) override; - void didConcat(const SkMatrix&) override; - void didSetMatrix(const SkMatrix&) override; + void didSetM44(const SkM44&) override; void didScale(SkScalar, SkScalar) override; void didTranslate(SkScalar, SkScalar) override; diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 31e45558139d..74c70c8969f2 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -380,7 +380,7 @@ void RenderNode::deleteDisplayList(TreeObserver& observer, TreeInfo* info) { if (mDisplayList) { mDisplayList->updateChildren( [&observer, info](RenderNode* child) { child->decParentRefCount(observer, info); }); - if (!mDisplayList->reuseDisplayList(this, info ? &info->canvasContext : nullptr)) { + if (!mDisplayList->reuseDisplayList(this)) { delete mDisplayList; } } diff --git a/libs/hwui/SaveFlags.h b/libs/hwui/SaveFlags.h new file mode 100644 index 000000000000..f3579a8e9f19 --- /dev/null +++ b/libs/hwui/SaveFlags.h @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#pragma once + +#include <inttypes.h> + +// TODO: Move this to an enum class +namespace android::SaveFlags { + +// These must match the corresponding Canvas API constants. +enum { + Matrix = 0x01, + Clip = 0x02, + HasAlphaLayer = 0x04, + ClipToLayer = 0x10, + + // Helper constant + MatrixClip = Matrix | Clip, +}; +typedef uint32_t Flags; + +} // namespace android::SaveFlags diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp index cd908354aea5..6030c36add7a 100644 --- a/libs/hwui/VectorDrawable.cpp +++ b/libs/hwui/VectorDrawable.cpp @@ -505,13 +505,11 @@ void Tree::draw(SkCanvas* canvas, const SkRect& bounds, const SkPaint& inPaint) SkPaint paint = inPaint; paint.setAlpha(mProperties.getRootAlpha() * 255); - Bitmap& bitmap = getBitmapUpdateIfDirty(); - SkBitmap skiaBitmap; - bitmap.getSkBitmap(&skiaBitmap); + sk_sp<SkImage> cachedBitmap = getBitmapUpdateIfDirty().makeImage(); int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth()); int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight()); - canvas->drawBitmapRect(skiaBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds, + canvas->drawImageRect(cachedBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds, &paint, SkCanvas::kFast_SrcRectConstraint); } diff --git a/libs/hwui/WebViewFunctorManager.cpp b/libs/hwui/WebViewFunctorManager.cpp index 68541b4b31f0..671c66f11e32 100644 --- a/libs/hwui/WebViewFunctorManager.cpp +++ b/libs/hwui/WebViewFunctorManager.cpp @@ -26,6 +26,35 @@ namespace android::uirenderer { +namespace { +class ScopedCurrentFunctor { +public: + ScopedCurrentFunctor(WebViewFunctor* functor) { + ALOG_ASSERT(!sCurrentFunctor); + ALOG_ASSERT(functor); + sCurrentFunctor = functor; + } + ~ScopedCurrentFunctor() { + ALOG_ASSERT(sCurrentFunctor); + sCurrentFunctor = nullptr; + } + + static ASurfaceControl* getSurfaceControl() { + ALOG_ASSERT(sCurrentFunctor); + return sCurrentFunctor->getSurfaceControl(); + } + static void mergeTransaction(ASurfaceTransaction* transaction) { + ALOG_ASSERT(sCurrentFunctor); + sCurrentFunctor->mergeTransaction(transaction); + } + +private: + static WebViewFunctor* sCurrentFunctor; +}; + +WebViewFunctor* ScopedCurrentFunctor::sCurrentFunctor = nullptr; +} // namespace + RenderMode WebViewFunctor_queryPlatformRenderMode() { auto pipelineType = Properties::getRenderPipelineType(); switch (pipelineType) { @@ -83,7 +112,15 @@ void WebViewFunctor::drawGl(const DrawGlInfo& drawInfo) { if (!mHasContext) { mHasContext = true; } - mCallbacks.gles.draw(mFunctor, mData, drawInfo); + ScopedCurrentFunctor currentFunctor(this); + + WebViewOverlayData overlayParams = { + // TODO: + .overlaysMode = OverlaysMode::Disabled, + .getSurfaceControl = currentFunctor.getSurfaceControl, + .mergeTransaction = currentFunctor.mergeTransaction, + }; + mCallbacks.gles.draw(mFunctor, mData, drawInfo, overlayParams); } void WebViewFunctor::initVk(const VkFunctorInitParams& params) { @@ -98,7 +135,15 @@ void WebViewFunctor::initVk(const VkFunctorInitParams& params) { void WebViewFunctor::drawVk(const VkFunctorDrawParams& params) { ATRACE_NAME("WebViewFunctor::drawVk"); - mCallbacks.vk.draw(mFunctor, mData, params); + ScopedCurrentFunctor currentFunctor(this); + + WebViewOverlayData overlayParams = { + // TODO + .overlaysMode = OverlaysMode::Disabled, + .getSurfaceControl = currentFunctor.getSurfaceControl, + .mergeTransaction = currentFunctor.mergeTransaction, + }; + mCallbacks.vk.draw(mFunctor, mData, params, overlayParams); } void WebViewFunctor::postDrawVk() { @@ -118,6 +163,20 @@ void WebViewFunctor::destroyContext() { } } +void WebViewFunctor::removeOverlays() { + ScopedCurrentFunctor currentFunctor(this); + mCallbacks.removeOverlays(mFunctor, mData, currentFunctor.mergeTransaction); +} + +ASurfaceControl* WebViewFunctor::getSurfaceControl() { + // TODO + return nullptr; +} + +void WebViewFunctor::mergeTransaction(ASurfaceTransaction* transaction) { + // TODO +} + WebViewFunctorManager& WebViewFunctorManager::instance() { static WebViewFunctorManager sInstance; return sInstance; diff --git a/libs/hwui/WebViewFunctorManager.h b/libs/hwui/WebViewFunctorManager.h index 675b738c6406..737d60525aa9 100644 --- a/libs/hwui/WebViewFunctorManager.h +++ b/libs/hwui/WebViewFunctorManager.h @@ -56,6 +56,8 @@ public: void postDrawVk() { mReference.postDrawVk(); } + void removeOverlays() { mReference.removeOverlays(); } + private: friend class WebViewFunctor; @@ -71,6 +73,10 @@ public: void drawVk(const VkFunctorDrawParams& params); void postDrawVk(); void destroyContext(); + void removeOverlays(); + + ASurfaceControl* getSurfaceControl(); + void mergeTransaction(ASurfaceTransaction* transaction); sp<Handle> createHandle() { LOG_ALWAYS_FATAL_IF(mCreatedHandle); diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index e1f5abd786bf..0fad2d58cc8a 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -69,6 +69,7 @@ extern int register_android_graphics_drawable_AnimatedVectorDrawable(JNIEnv* env extern int register_android_graphics_drawable_VectorDrawable(JNIEnv* env); extern int register_android_graphics_fonts_Font(JNIEnv* env); extern int register_android_graphics_fonts_FontFamily(JNIEnv* env); +extern int register_android_graphics_fonts_NativeFont(JNIEnv* env); extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env); extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env); extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); @@ -135,6 +136,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_drawable_VectorDrawable), REG_JNI(register_android_graphics_fonts_Font), REG_JNI(register_android_graphics_fonts_FontFamily), + REG_JNI(register_android_graphics_fonts_NativeFont), REG_JNI(register_android_graphics_pdf_PdfDocument), REG_JNI(register_android_graphics_pdf_PdfEditor), REG_JNI(register_android_graphics_pdf_PdfRenderer), diff --git a/libs/hwui/canvas/CanvasFrontend.cpp b/libs/hwui/canvas/CanvasFrontend.cpp new file mode 100644 index 000000000000..8f261c83b8d3 --- /dev/null +++ b/libs/hwui/canvas/CanvasFrontend.cpp @@ -0,0 +1,124 @@ +/* + * 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. + */ + +#include "CanvasFrontend.h" +#include "CanvasOps.h" +#include "CanvasOpBuffer.h" + +namespace android::uirenderer { + +CanvasStateHelper::CanvasStateHelper(int width, int height) { + resetState(width, height); +} + +void CanvasStateHelper::resetState(int width, int height) { + mInitialBounds = SkIRect::MakeWH(width, height); + mSaveStack.clear(); + mClipStack.clear(); + mTransformStack.clear(); + mSaveStack.emplace_back(); + mClipStack.emplace_back().setRect(mInitialBounds); + mTransformStack.emplace_back(); + mCurrentClipIndex = 0; + mCurrentTransformIndex = 0; +} + +bool CanvasStateHelper::internalSave(SaveEntry saveEntry) { + mSaveStack.push_back(saveEntry); + if (saveEntry.matrix) { + // We need to push before accessing transform() to ensure the reference doesn't move + // across vector resizes + mTransformStack.emplace_back() = transform(); + mCurrentTransformIndex += 1; + } + if (saveEntry.clip) { + // We need to push before accessing clip() to ensure the reference doesn't move + // across vector resizes + mClipStack.emplace_back() = clip(); + mCurrentClipIndex += 1; + return true; + } + return false; +} + +// Assert that the cast from SkClipOp to SkRegion::Op is valid +static_assert(static_cast<int>(SkClipOp::kDifference) == SkRegion::Op::kDifference_Op); +static_assert(static_cast<int>(SkClipOp::kIntersect) == SkRegion::Op::kIntersect_Op); +static_assert(static_cast<int>(SkClipOp::kUnion_deprecated) == SkRegion::Op::kUnion_Op); +static_assert(static_cast<int>(SkClipOp::kXOR_deprecated) == SkRegion::Op::kXOR_Op); +static_assert(static_cast<int>(SkClipOp::kReverseDifference_deprecated) == SkRegion::Op::kReverseDifference_Op); +static_assert(static_cast<int>(SkClipOp::kReplace_deprecated) == SkRegion::Op::kReplace_Op); + +void CanvasStateHelper::internalClipRect(const SkRect& rect, SkClipOp op) { + clip().opRect(rect, transform(), mInitialBounds, (SkRegion::Op)op, false); +} + +void CanvasStateHelper::internalClipPath(const SkPath& path, SkClipOp op) { + clip().opPath(path, transform(), mInitialBounds, (SkRegion::Op)op, true); +} + +bool CanvasStateHelper::internalRestore() { + // Prevent underflows + if (saveCount() <= 1) { + return false; + } + + SaveEntry entry = mSaveStack[mSaveStack.size() - 1]; + mSaveStack.pop_back(); + bool needsRestorePropagation = entry.layer; + if (entry.matrix) { + mTransformStack.pop_back(); + mCurrentTransformIndex -= 1; + } + if (entry.clip) { + // We need to push before accessing clip() to ensure the reference doesn't move + // across vector resizes + mClipStack.pop_back(); + mCurrentClipIndex -= 1; + needsRestorePropagation = true; + } + return needsRestorePropagation; +} + +SkRect CanvasStateHelper::getClipBounds() const { + SkIRect ibounds = clip().getBounds(); + + if (ibounds.isEmpty()) { + return SkRect::MakeEmpty(); + } + + SkMatrix inverse; + // if we can't invert the CTM, we can't return local clip bounds + if (!transform().invert(&inverse)) { + return SkRect::MakeEmpty(); + } + + SkRect ret = SkRect::MakeEmpty(); + inverse.mapRect(&ret, SkRect::Make(ibounds)); + return ret; +} + +bool CanvasStateHelper::quickRejectRect(float left, float top, float right, float bottom) const { + // TODO: Implement + return false; +} + +bool CanvasStateHelper::quickRejectPath(const SkPath& path) const { + // TODO: Implement + return false; +} + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasFrontend.h b/libs/hwui/canvas/CanvasFrontend.h new file mode 100644 index 000000000000..d749d2f2596b --- /dev/null +++ b/libs/hwui/canvas/CanvasFrontend.h @@ -0,0 +1,212 @@ +/* + * 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. + */ + +#pragma once + +// TODO: Can we get the dependencies scoped down more? +#include "CanvasOps.h" +#include "CanvasOpBuffer.h" +#include <SaveFlags.h> + +#include <SkRasterClip.h> +#include <ui/FatVector.h> + +#include <optional> + +namespace android::uirenderer { + +// Exists to avoid forcing all this common logic into the templated class +class CanvasStateHelper { +protected: + CanvasStateHelper(int width, int height); + ~CanvasStateHelper() = default; + + struct SaveEntry { + bool clip : 1 = false; + bool matrix : 1 = false; + bool layer : 1 = false; + }; + + constexpr SaveEntry saveEntryForLayer() { + return { + .clip = true, + .matrix = true, + .layer = true, + }; + } + + constexpr SaveEntry flagsToSaveEntry(SaveFlags::Flags flags) { + return SaveEntry { + .clip = static_cast<bool>(flags & SaveFlags::Clip), + .matrix = static_cast<bool>(flags & SaveFlags::Matrix), + .layer = false + }; + } + + bool internalSave(SaveEntry saveEntry); + + void internalSaveLayer(const SkCanvas::SaveLayerRec& layerRec) { + internalSave({ + .clip = true, + .matrix = true, + .layer = true + }); + internalClipRect(*layerRec.fBounds, SkClipOp::kIntersect); + } + + bool internalRestore(); + + void internalClipRect(const SkRect& rect, SkClipOp op); + void internalClipPath(const SkPath& path, SkClipOp op); + + SkIRect mInitialBounds; + FatVector<SaveEntry, 6> mSaveStack; + FatVector<SkMatrix, 6> mTransformStack; + FatVector<SkConservativeClip, 6> mClipStack; + + size_t mCurrentTransformIndex; + size_t mCurrentClipIndex; + + const SkConservativeClip& clip() const { + return mClipStack[mCurrentClipIndex]; + } + + SkConservativeClip& clip() { + return mClipStack[mCurrentClipIndex]; + } + + void resetState(int width, int height); + +public: + int saveCount() const { return mSaveStack.size(); } + + SkRect getClipBounds() const; + bool quickRejectRect(float left, float top, float right, float bottom) const; + bool quickRejectPath(const SkPath& path) const; + + const SkMatrix& transform() const { + return mTransformStack[mCurrentTransformIndex]; + } + + SkMatrix& transform() { + return mTransformStack[mCurrentTransformIndex]; + } + + // For compat with existing HWUI Canvas interface + void getMatrix(SkMatrix* outMatrix) const { + *outMatrix = transform(); + } + + void setMatrix(const SkMatrix& matrix) { + transform() = matrix; + } + + void concat(const SkMatrix& matrix) { + transform().preConcat(matrix); + } + + void rotate(float degrees) { + SkMatrix m; + m.setRotate(degrees); + concat(m); + } + + void scale(float sx, float sy) { + SkMatrix m; + m.setScale(sx, sy); + concat(m); + } + + void skew(float sx, float sy) { + SkMatrix m; + m.setSkew(sx, sy); + concat(m); + } + + void translate(float dx, float dy) { + transform().preTranslate(dx, dy); + } +}; + +// Front-end canvas that handles queries, up-front state, and produces CanvasOp<> output downstream +template <typename CanvasOpReceiver> +class CanvasFrontend final : public CanvasStateHelper { +public: + template<class... Args> + CanvasFrontend(int width, int height, Args&&... args) : CanvasStateHelper(width, height), + mReceiver(std::forward<Args>(args)...) { } + ~CanvasFrontend() = default; + + void save(SaveFlags::Flags flags = SaveFlags::MatrixClip) { + if (internalSave(flagsToSaveEntry(flags))) { + submit<CanvasOpType::Save>({}); + } + } + + void restore() { + if (internalRestore()) { + submit<CanvasOpType::Restore>({}); + } + } + + template <CanvasOpType T> + void draw(CanvasOp<T>&& op) { + // The front-end requires going through certain front-doors, which these aren't. + static_assert(T != CanvasOpType::Save, "Must use CanvasFrontend::save() call instead"); + static_assert(T != CanvasOpType::Restore, "Must use CanvasFrontend::restore() call instead"); + + if constexpr (T == CanvasOpType::SaveLayer) { + internalSaveLayer(op.saveLayerRec); + } + if constexpr (T == CanvasOpType::SaveBehind) { + // Don't use internalSaveLayer as this doesn't apply clipping, it's a "regular" save + // But we do want to flag it as a layer, such that restore is Definitely Required + internalSave(saveEntryForLayer()); + } + if constexpr (T == CanvasOpType::ClipRect) { + internalClipRect(op.rect, op.op); + } + if constexpr (T == CanvasOpType::ClipPath) { + internalClipPath(op.path, op.op); + } + + submit(std::move(op)); + } + + const CanvasOpReceiver& receiver() const { return *mReceiver; } + + CanvasOpReceiver finish() { + auto ret = std::move(mReceiver.value()); + mReceiver.reset(); + return std::move(ret); + } + + template<class... Args> + void reset(int newWidth, int newHeight, Args&&... args) { + resetState(newWidth, newHeight); + mReceiver.emplace(std::forward<Args>(args)...); + } + +private: + std::optional<CanvasOpReceiver> mReceiver; + + template <CanvasOpType T> + void submit(CanvasOp<T>&& op) { + mReceiver->push_container(CanvasOpContainer(std::move(op), transform())); + } +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpRasterizer.cpp b/libs/hwui/canvas/CanvasOpRasterizer.cpp index 25129f641c00..0093c38cf8a8 100644 --- a/libs/hwui/canvas/CanvasOpRasterizer.cpp +++ b/libs/hwui/canvas/CanvasOpRasterizer.cpp @@ -33,7 +33,11 @@ void rasterizeCanvasBuffer(const CanvasOpBuffer& source, SkCanvas* destination) SkMatrix& currentGlobalTransform = globalMatrixStack.emplace_back(SkMatrix::I()); source.for_each([&]<CanvasOpType T>(const CanvasOpContainer<T> * op) { - if constexpr (T == CanvasOpType::BeginZ || T == CanvasOpType::EndZ) { + if constexpr ( + T == CanvasOpType::BeginZ || + T == CanvasOpType::EndZ || + T == CanvasOpType::DrawLayer + ) { // Do beginZ or endZ LOG_ALWAYS_FATAL("TODO"); return; diff --git a/libs/hwui/canvas/CanvasOpRecorder.cpp b/libs/hwui/canvas/CanvasOpRecorder.cpp new file mode 100644 index 000000000000..bb968ee84670 --- /dev/null +++ b/libs/hwui/canvas/CanvasOpRecorder.cpp @@ -0,0 +1,22 @@ +/* + * 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. + */ + +#include "CanvasOpRecorder.h" + +#include "CanvasOpBuffer.h" +#include "CanvasOps.h" + +namespace android::uirenderer {} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpRecorder.h b/libs/hwui/canvas/CanvasOpRecorder.h new file mode 100644 index 000000000000..7d95bc4785ea --- /dev/null +++ b/libs/hwui/canvas/CanvasOpRecorder.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#pragma once + +#include "hwui/Canvas.h" +#include "CanvasOpBuffer.h" + +#include <vector> + +namespace android::uirenderer { + +// Interop with existing HWUI Canvas +class CanvasOpRecorder final : /* todo: public Canvas */ { +public: + // Transform ops +private: + struct SaveEntry { + + }; + + std::vector<SaveEntry> mSaveStack; +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpTypes.h b/libs/hwui/canvas/CanvasOpTypes.h index f9df2f7aa5ba..f0aa7774a6cc 100644 --- a/libs/hwui/canvas/CanvasOpTypes.h +++ b/libs/hwui/canvas/CanvasOpTypes.h @@ -47,14 +47,17 @@ enum class CanvasOpType : int8_t { DrawArc, DrawPaint, DrawPoint, + DrawPoints, DrawPath, DrawLine, + DrawLines, DrawVertices, DrawImage, DrawImageRect, // DrawImageLattice also used to draw 9 patches DrawImageLattice, DrawPicture, + DrawLayer, // TODO: Rest diff --git a/libs/hwui/canvas/CanvasOps.h b/libs/hwui/canvas/CanvasOps.h index 8c7113d5d075..62c26c7b6f6a 100644 --- a/libs/hwui/canvas/CanvasOps.h +++ b/libs/hwui/canvas/CanvasOps.h @@ -26,8 +26,10 @@ #include <hwui/Bitmap.h> #include <log/log.h> #include "CanvasProperty.h" +#include "Points.h" #include "CanvasOpTypes.h" +#include "Layer.h" #include <experimental/type_traits> #include <utility> @@ -165,6 +167,22 @@ struct CanvasOp<CanvasOpType::DrawPoint> { }; template <> +struct CanvasOp<CanvasOpType::DrawPoints> { + size_t count; + SkPaint paint; + sk_sp<Points> points; + void draw(SkCanvas* canvas) const { + canvas->drawPoints( + SkCanvas::kPoints_PointMode, + count, + points->data(), + paint + ); + } + ASSERT_DRAWABLE() +}; + +template <> struct CanvasOp<CanvasOpType::DrawRect> { SkRect rect; SkPaint paint; @@ -263,6 +281,22 @@ struct CanvasOp<CanvasOpType::DrawLine> { }; template<> +struct CanvasOp<CanvasOpType::DrawLines> { + size_t count; + SkPaint paint; + sk_sp<Points> points; + void draw(SkCanvas* canvas) const { + canvas->drawPoints( + SkCanvas::kLines_PointMode, + count, + points->data(), + paint + ); + } + ASSERT_DRAWABLE() +}; + +template<> struct CanvasOp<CanvasOpType::DrawVertices> { sk_sp<SkVertices> vertices; SkBlendMode mode; @@ -276,7 +310,7 @@ struct CanvasOp<CanvasOpType::DrawVertices> { template<> struct CanvasOp<CanvasOpType::DrawImage> { - CanvasOp<CanvasOpType::DrawImageRect>( + CanvasOp( const sk_sp<Bitmap>& bitmap, float left, float top, @@ -302,7 +336,7 @@ struct CanvasOp<CanvasOpType::DrawImage> { template<> struct CanvasOp<CanvasOpType::DrawImageRect> { - CanvasOp<CanvasOpType::DrawImageRect>( + CanvasOp( const sk_sp<Bitmap>& bitmap, SkRect src, SkRect dst, @@ -333,7 +367,7 @@ struct CanvasOp<CanvasOpType::DrawImageRect> { template<> struct CanvasOp<CanvasOpType::DrawImageLattice> { - CanvasOp<CanvasOpType::DrawImageLattice>( + CanvasOp( const sk_sp<Bitmap>& bitmap, SkRect dst, SkCanvas::Lattice lattice, @@ -364,6 +398,11 @@ struct CanvasOp<CanvasOpType::DrawPicture> { } }; +template<> +struct CanvasOp<CanvasOpType::DrawLayer> { + sp<Layer> layer; +}; + // cleanup our macros #undef ASSERT_DRAWABLE diff --git a/libs/hwui/canvas/OpBuffer.h b/libs/hwui/canvas/OpBuffer.h index 98e385f37a6e..6dc29d9a4bfe 100644 --- a/libs/hwui/canvas/OpBuffer.h +++ b/libs/hwui/canvas/OpBuffer.h @@ -60,9 +60,8 @@ class OpBuffer { return (size + (Alignment - 1)) & -Alignment; } - static constexpr auto STARTING_SIZE = PadAlign(sizeof(BufferHeader)); - public: + static constexpr auto STARTING_SIZE = PadAlign(sizeof(BufferHeader)); using ItemHeader = OpBufferItemHeader<ItemTypes>; OpBuffer() = default; diff --git a/libs/hwui/canvas/Points.h b/libs/hwui/canvas/Points.h new file mode 100644 index 000000000000..05e6a7dd5884 --- /dev/null +++ b/libs/hwui/canvas/Points.h @@ -0,0 +1,48 @@ +/* + * 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. + */ +#include <ui/FatVector.h> +#include "SkPoint.h" +#include "SkRefCnt.h" + +/** + * Collection of points that are ref counted and to be used with + * various drawing calls that consume SkPoint as inputs like + * drawLines/drawPoints + */ +class Points: public SkNVRefCnt<SkPoint> { +public: + Points(int size){ + skPoints.resize(size); + } + + Points(std::initializer_list<SkPoint> init): skPoints(init) { } + + SkPoint& operator[](int index) { + return skPoints[index]; + } + + const SkPoint* data() const { + return skPoints.data(); + } + + size_t size() const { + return skPoints.size(); + } +private: + // Initialize the size to contain 2 SkPoints on the stack for optimized + // drawLine calls that require 2 SkPoints for start/end points of the line + android::FatVector<SkPoint, 2> skPoints; +}; diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index f94bae2746d9..11fa3223a9c8 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -18,8 +18,10 @@ #include <cutils/compiler.h> #include <utils/Functor.h> +#include <SaveFlags.h> #include <androidfw/ResourceTypes.h> +#include "DisplayList.h" #include "Properties.h" #include "utils/Macros.h" @@ -46,34 +48,6 @@ class CanvasPropertyPaint; class CanvasPropertyPrimitive; class DeferredLayerUpdater; class RenderNode; - -namespace skiapipeline { -class SkiaDisplayList; -} - -/** - * Data structure that holds the list of commands used in display list stream - */ -using DisplayList = skiapipeline::SkiaDisplayList; -} - -namespace SaveFlags { - -// These must match the corresponding Canvas API constants. -enum { - Matrix = 0x01, - Clip = 0x02, - HasAlphaLayer = 0x04, - ClipToLayer = 0x10, - - // Helper constant - MatrixClip = Matrix | Clip, -}; -typedef uint32_t Flags; - -} // namespace SaveFlags - -namespace uirenderer { namespace VectorDrawable { class Tree; } diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index f0c77930cbe3..f612bce748ff 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -187,38 +187,6 @@ static jfloat Font_getFontMetrics(JNIEnv* env, jobject, jlong fontHandle, jlong } // Critical Native -static jlong Font_getFontInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { - const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); - MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); - - uint64_t result = font->style().weight(); - result |= font->style().slant() == minikin::FontStyle::Slant::ITALIC ? 0x10000 : 0x00000; - result |= ((static_cast<uint64_t>(minikinSkia->GetFontIndex())) << 32); - result |= ((static_cast<uint64_t>(minikinSkia->GetAxes().size())) << 48); - return result; -} - -// Critical Native -static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle, jint index) { - const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); - MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); - const minikin::FontVariation& var = minikinSkia->GetAxes().at(index); - uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); - return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); -} - -// FastNative -static jstring Font_getFontPath(JNIEnv* env, jobject, jlong fontHandle) { - const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); - MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); - const std::string& filePath = minikinSkia->getFilePath(); - if (filePath.empty()) { - return nullptr; - } - return env->NewStringUTF(filePath.c_str()); -} - -// Critical Native static jlong Font_getNativeFontPtr(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); return reinterpret_cast<jlong>(font->font.get()); @@ -276,9 +244,6 @@ static const JNINativeMethod gFontBuilderMethods[] = { static const JNINativeMethod gFontMethods[] = { { "nGetGlyphBounds", "(JIJLandroid/graphics/RectF;)F", (void*) Font_getGlyphBounds }, { "nGetFontMetrics", "(JJLandroid/graphics/Paint$FontMetrics;)F", (void*) Font_getFontMetrics }, - { "nGetFontInfo", "(J)J", (void*) Font_getFontInfo }, - { "nGetAxisInfo", "(JI)J", (void*) Font_getAxisInfo }, - { "nGetFontPath", "(J)Ljava/lang/String;", (void*) Font_getFontPath }, { "nGetNativeFontPtr", "(J)J", (void*) Font_getNativeFontPtr }, { "nGetFontBufferAddress", "(J)J", (void*) Font_GetBufferAddress }, }; diff --git a/libs/hwui/jni/fonts/NativeFont.cpp b/libs/hwui/jni/fonts/NativeFont.cpp new file mode 100644 index 000000000000..c5c5d464ccac --- /dev/null +++ b/libs/hwui/jni/fonts/NativeFont.cpp @@ -0,0 +1,125 @@ +/* + * 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. + */ + +#undef LOG_TAG +#define LOG_TAG "Minikin" + +#include "Font.h" +#include "SkData.h" +#include "SkFont.h" +#include "SkFontMetrics.h" +#include "SkFontMgr.h" +#include "SkRefCnt.h" +#include "SkTypeface.h" +#include "GraphicsJNI.h" +#include <nativehelper/ScopedUtfChars.h> +#include "Utils.h" +#include "FontUtils.h" + +#include <hwui/MinikinSkia.h> +#include <hwui/Paint.h> +#include <hwui/Typeface.h> +#include <minikin/FontFamily.h> +#include <minikin/LocaleList.h> +#include <ui/FatVector.h> + +#include <memory> + +namespace android { + +// Critical Native +static jint NativeFont_getFamilyCount(CRITICAL_JNI_PARAMS_COMMA jlong typefaceHandle) { + Typeface* tf = reinterpret_cast<Typeface*>(typefaceHandle); + return tf->fFontCollection->getFamilies().size(); +} + +// Critical Native +static jlong NativeFont_getFamily(CRITICAL_JNI_PARAMS_COMMA jlong typefaceHandle, jint index) { + Typeface* tf = reinterpret_cast<Typeface*>(typefaceHandle); + return reinterpret_cast<jlong>(tf->fFontCollection->getFamilies()[index].get()); + +} + +// Fast Native +static jstring NativeFont_getLocaleList(JNIEnv* env, jobject, jlong familyHandle) { + minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); + uint32_t localeListId = family->localeListId(); + return env->NewStringUTF(minikin::getLocaleString(localeListId).c_str()); +} + +// Critical Native +static jint NativeFont_getFontCount(CRITICAL_JNI_PARAMS_COMMA jlong familyHandle) { + minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); + return family->getNumFonts(); +} + +// Critical Native +static jlong NativeFont_getFont(CRITICAL_JNI_PARAMS_COMMA jlong familyHandle, jint index) { + minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); + return reinterpret_cast<jlong>(family->getFont(index)); +} + +// Critical Native +static jlong NativeFont_getFontInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); + + uint64_t result = font->style().weight(); + result |= font->style().slant() == minikin::FontStyle::Slant::ITALIC ? 0x10000 : 0x00000; + result |= ((static_cast<uint64_t>(minikinSkia->GetFontIndex())) << 32); + result |= ((static_cast<uint64_t>(minikinSkia->GetAxes().size())) << 48); + return result; +} + +// Critical Native +static jlong NativeFont_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle, jint index) { + const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); + const minikin::FontVariation& var = minikinSkia->GetAxes().at(index); + uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); + return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); +} + +// FastNative +static jstring NativeFont_getFontPath(JNIEnv* env, jobject, jlong fontHandle) { + const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); + const std::string& filePath = minikinSkia->getFilePath(); + if (filePath.empty()) { + return nullptr; + } + return env->NewStringUTF(filePath.c_str()); +} + +/////////////////////////////////////////////////////////////////////////////// + +static const JNINativeMethod gNativeFontMethods[] = { + { "nGetFamilyCount", "(J)I", (void*) NativeFont_getFamilyCount }, + { "nGetFamily", "(JI)J", (void*) NativeFont_getFamily }, + { "nGetLocaleList", "(J)Ljava/lang/String;", (void*) NativeFont_getLocaleList }, + { "nGetFontCount", "(J)I", (void*) NativeFont_getFontCount }, + { "nGetFont", "(JI)J", (void*) NativeFont_getFont }, + { "nGetFontInfo", "(J)J", (void*) NativeFont_getFontInfo }, + { "nGetAxisInfo", "(JI)J", (void*) NativeFont_getAxisInfo }, + { "nGetFontPath", "(J)Ljava/lang/String;", (void*) NativeFont_getFontPath }, +}; + +int register_android_graphics_fonts_NativeFont(JNIEnv* env) { + return RegisterMethodsOrDie(env, "android/graphics/fonts/NativeFont", gNativeFontMethods, + NELEM(gNativeFontMethods)); +} + +} // namespace android diff --git a/libs/hwui/jni/pdf/PdfEditor.cpp b/libs/hwui/jni/pdf/PdfEditor.cpp index e65921ac8e0a..427bafa1bd83 100644 --- a/libs/hwui/jni/pdf/PdfEditor.cpp +++ b/libs/hwui/jni/pdf/PdfEditor.cpp @@ -129,8 +129,8 @@ static void nativeSetTransformAndClip(JNIEnv* env, jclass thiz, jlong documentPt // PDF's coordinate system origin is left-bottom while in graphics it // is the top-left. So, translate the PDF coordinates to ours. - SkMatrix reflectOnX = SkMatrix::MakeScale(1, -1); - SkMatrix moveUp = SkMatrix::MakeTrans(0, FPDF_GetPageHeight(page)); + SkMatrix reflectOnX = SkMatrix::Scale(1, -1); + SkMatrix moveUp = SkMatrix::Translate(0, FPDF_GetPageHeight(page)); SkMatrix coordinateChange = SkMatrix::Concat(moveUp, reflectOnX); // Apply the transformation what was created in our coordinates. diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index 158c3493a90c..c63f5d349311 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -47,7 +47,7 @@ void SkiaDisplayList::syncContents(const WebViewSyncData& data) { } } -bool SkiaDisplayList::reuseDisplayList(RenderNode* node, renderthread::CanvasContext* context) { +bool SkiaDisplayList::reuseDisplayList(RenderNode* node) { reset(); node->attachAvailableList(this); return true; diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index cdd00db9afdc..f2f19ba2975e 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -98,7 +98,7 @@ public: * * @return true if the displayList will be reused and therefore should not be deleted */ - bool reuseDisplayList(RenderNode* node, renderthread::CanvasContext* context); + bool reuseDisplayList(RenderNode* node); /** * ONLY to be called by RenderNode::syncDisplayList so that we can notify any diff --git a/libs/hwui/private/hwui/WebViewFunctor.h b/libs/hwui/private/hwui/WebViewFunctor.h index 96da947ace08..22ae59e5137b 100644 --- a/libs/hwui/private/hwui/WebViewFunctor.h +++ b/libs/hwui/private/hwui/WebViewFunctor.h @@ -17,6 +17,15 @@ #ifndef FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H #define FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H +#ifdef __ANDROID__ // Layoutlib does not support surface control +#include <android/surface_control.h> +#else +// To avoid ifdefs around overlay implementation all over the place we typedef these to void *. They +// won't be used. +typedef void* ASurfaceControl; +typedef void* ASurfaceTransaction; +#endif + #include <cutils/compiler.h> #include <private/hwui/DrawGlInfo.h> #include <private/hwui/DrawVkInfo.h> @@ -28,6 +37,14 @@ enum class RenderMode { Vulkan, }; +enum class OverlaysMode { + // Indicated that webview should not promote anything to overlays this draw + // and remove all visible overlays. + Disabled, + // Indicates that webview can use overlays. + Enabled +}; + // Static for the lifetime of the process ANDROID_API RenderMode WebViewFunctor_queryPlatformRenderMode(); @@ -35,6 +52,23 @@ struct WebViewSyncData { bool applyForceDark; }; +struct WebViewOverlayData { + // Desired overlay mode for this draw. + OverlaysMode overlaysMode; + + // Returns parent ASurfaceControl for WebView overlays. It will be have same + // geometry as the surface we draw into and positioned below it (underlay). + // This does not pass ownership to webview, but guaranteed to be alive until + // transaction from next removeOverlays call or functor destruction will be + // finished. + ASurfaceControl* (*getSurfaceControl)(); + + // Merges WebView transaction to be applied synchronously with current draw. + // This doesn't pass ownership of the transaction, changes will be copied and + // webview can free transaction right after the call. + void (*mergeTransaction)(ASurfaceTransaction*); +}; + struct WebViewFunctorCallbacks { // kModeSync, called on RenderThread void (*onSync)(int functor, void* data, const WebViewSyncData& syncData); @@ -48,16 +82,23 @@ struct WebViewFunctorCallbacks { // this functor had ever been drawn. void (*onDestroyed)(int functor, void* data); + // Called on render thread to force webview hide all overlays and stop updating them. + // Should happen during hwui draw (e.g can be called instead of draw if webview + // isn't visible and won't receive draw) and support MergeTransaction call. + void (*removeOverlays)(int functor, void* data, void (*mergeTransaction)(ASurfaceTransaction*)); + union { struct { // Called on RenderThread. initialize is guaranteed to happen before this call - void (*draw)(int functor, void* data, const DrawGlInfo& params); + void (*draw)(int functor, void* data, const DrawGlInfo& params, + const WebViewOverlayData& overlayParams); } gles; struct { // Called either the first time the functor is used or the first time it's used after // a call to onContextDestroyed. void (*initialize)(int functor, void* data, const VkFunctorInitParams& params); - void (*draw)(int functor, void* data, const VkFunctorDrawParams& params); + void (*draw)(int functor, void* data, const VkFunctorDrawParams& params, + const WebViewOverlayData& overlayParams); void (*postDraw)(int functor, void*); } vk; }; diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 36c5a8c1b3de..c1d8b761514b 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -323,7 +323,8 @@ public: }; switch (mode) { case RenderMode::OpenGL_ES: - callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params) { + callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params, + const WebViewOverlayData& overlay_params) { expectOnRenderThread("draw"); sMockFunctorCounts[functor].glesDraw++; }; diff --git a/libs/hwui/tests/microbench/CanvasOpBench.cpp b/libs/hwui/tests/microbench/CanvasOpBench.cpp new file mode 100644 index 000000000000..ef5749e6b79b --- /dev/null +++ b/libs/hwui/tests/microbench/CanvasOpBench.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2015 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. + */ + +#include <benchmark/benchmark.h> + +#include "DisplayList.h" +#include "hwui/Paint.h" +#include "canvas/CanvasOpBuffer.h" +#include "canvas/CanvasFrontend.h" +#include "tests/common/TestUtils.h" + +using namespace android; +using namespace android::uirenderer; + +void BM_CanvasOpBuffer_alloc(benchmark::State& benchState) { + while (benchState.KeepRunning()) { + auto displayList = new CanvasOpBuffer(); + benchmark::DoNotOptimize(displayList); + delete displayList; + } +} +BENCHMARK(BM_CanvasOpBuffer_alloc); + +void BM_CanvasOpBuffer_record_saverestore(benchmark::State& benchState) { + CanvasFrontend<CanvasOpBuffer> canvas(100, 100); + while (benchState.KeepRunning()) { + canvas.reset(100, 100); + canvas.save(SaveFlags::MatrixClip); + canvas.save(SaveFlags::MatrixClip); + benchmark::DoNotOptimize(&canvas); + canvas.restore(); + canvas.restore(); + canvas.finish(); + } +} +BENCHMARK(BM_CanvasOpBuffer_record_saverestore); + +void BM_CanvasOpBuffer_record_saverestoreWithReuse(benchmark::State& benchState) { + CanvasFrontend<CanvasOpBuffer> canvas(100, 100); + + while (benchState.KeepRunning()) { + canvas.reset(100, 100); + canvas.save(SaveFlags::MatrixClip); + canvas.save(SaveFlags::MatrixClip); + benchmark::DoNotOptimize(&canvas); + canvas.restore(); + canvas.restore(); + } +} +BENCHMARK(BM_CanvasOpBuffer_record_saverestoreWithReuse); + +void BM_CanvasOpBuffer_record_simpleBitmapView(benchmark::State& benchState) { + CanvasFrontend<CanvasOpBuffer> canvas(100, 100); + + Paint rectPaint; + sk_sp<Bitmap> iconBitmap(TestUtils::createBitmap(80, 80)); + + while (benchState.KeepRunning()) { + canvas.reset(100, 100); + { + canvas.save(SaveFlags::MatrixClip); + canvas.draw(CanvasOp<CanvasOpType::DrawRect> { + .rect = SkRect::MakeWH(100, 100), + .paint = rectPaint, + }); + canvas.restore(); + } + { + canvas.save(SaveFlags::MatrixClip); + canvas.translate(10, 10); + canvas.draw(CanvasOp<CanvasOpType::DrawImage> { + iconBitmap, + 0, + 0, + SkPaint{} + }); + canvas.restore(); + } + benchmark::DoNotOptimize(&canvas); + canvas.finish(); + } +} +BENCHMARK(BM_CanvasOpBuffer_record_simpleBitmapView); diff --git a/libs/hwui/tests/microbench/RenderNodeBench.cpp b/libs/hwui/tests/microbench/RenderNodeBench.cpp index 206dcd58d785..011939a6e4b2 100644 --- a/libs/hwui/tests/microbench/RenderNodeBench.cpp +++ b/libs/hwui/tests/microbench/RenderNodeBench.cpp @@ -30,3 +30,29 @@ void BM_RenderNode_create(benchmark::State& state) { } } BENCHMARK(BM_RenderNode_create); + +void BM_RenderNode_recordSimple(benchmark::State& state) { + sp<RenderNode> node = new RenderNode(); + std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); + delete canvas->finishRecording(); + + while (state.KeepRunning()) { + canvas->resetRecording(100, 100, node.get()); + canvas->drawColor(0x00000000, SkBlendMode::kSrcOver); + node->setStagingDisplayList(canvas->finishRecording()); + } +} +BENCHMARK(BM_RenderNode_recordSimple); + +void BM_RenderNode_recordSimpleWithReuse(benchmark::State& state) { + sp<RenderNode> node = new RenderNode(); + std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); + delete canvas->finishRecording(); + + while (state.KeepRunning()) { + canvas->resetRecording(100, 100, node.get()); + canvas->drawColor(0x00000000, SkBlendMode::kSrcOver); + canvas->finishRecording()->reuseDisplayList(node.get()); + } +} +BENCHMARK(BM_RenderNode_recordSimpleWithReuse);
\ No newline at end of file diff --git a/libs/hwui/tests/unit/CanvasFrontendTests.cpp b/libs/hwui/tests/unit/CanvasFrontendTests.cpp new file mode 100644 index 000000000000..05b11795d90d --- /dev/null +++ b/libs/hwui/tests/unit/CanvasFrontendTests.cpp @@ -0,0 +1,213 @@ +/* + * 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. + */ + +#include <gtest/gtest.h> + +#include <canvas/CanvasFrontend.h> +#include <canvas/CanvasOpBuffer.h> +#include <canvas/CanvasOps.h> +#include <canvas/CanvasOpRasterizer.h> + +#include <tests/common/CallCountingCanvas.h> + +#include "SkPictureRecorder.h" +#include "SkColor.h" +#include "SkLatticeIter.h" +#include "pipeline/skia/AnimatedDrawables.h" +#include <SkNoDrawCanvas.h> + +using namespace android; +using namespace android::uirenderer; +using namespace android::uirenderer::test; + +class CanvasOpCountingReceiver { +public: + template <CanvasOpType T> + void push_container(CanvasOpContainer<T>&& op) { + mOpCounts[static_cast<size_t>(T)] += 1; + } + + int operator[](CanvasOpType op) const { + return mOpCounts[static_cast<size_t>(op)]; + } + +private: + std::array<int, static_cast<size_t>(CanvasOpType::COUNT)> mOpCounts; +}; + +TEST(CanvasFrontend, saveCount) { + SkNoDrawCanvas skiaCanvas(100, 100); + CanvasFrontend<CanvasOpCountingReceiver> opCanvas(100, 100); + const auto& receiver = opCanvas.receiver(); + + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.save(); + opCanvas.save(SaveFlags::MatrixClip); + EXPECT_EQ(2, skiaCanvas.getSaveCount()); + EXPECT_EQ(2, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + EXPECT_EQ(1, receiver[CanvasOpType::Save]); + EXPECT_EQ(1, receiver[CanvasOpType::Restore]); +} + +TEST(CanvasFrontend, transform) { + SkNoDrawCanvas skiaCanvas(100, 100); + CanvasFrontend<CanvasOpCountingReceiver> opCanvas(100, 100); + + skiaCanvas.translate(10, 10); + opCanvas.translate(10, 10); + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + + { + skiaCanvas.save(); + opCanvas.save(SaveFlags::Matrix); + skiaCanvas.scale(2.0f, 1.125f); + opCanvas.scale(2.0f, 1.125f); + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + skiaCanvas.restore(); + opCanvas.restore(); + } + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + + { + skiaCanvas.save(); + opCanvas.save(SaveFlags::Matrix); + skiaCanvas.rotate(90.f); + opCanvas.rotate(90.f); + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + + { + skiaCanvas.save(); + opCanvas.save(SaveFlags::Matrix); + skiaCanvas.skew(5.0f, 2.25f); + opCanvas.skew(5.0f, 2.25f); + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + skiaCanvas.restore(); + opCanvas.restore(); + } + + skiaCanvas.restore(); + opCanvas.restore(); + } + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); +} + +TEST(CanvasFrontend, drawOpTransform) { + CanvasFrontend<CanvasOpBuffer> opCanvas(100, 100); + const auto& receiver = opCanvas.receiver(); + + auto makeDrawRect = [] { + return CanvasOp<CanvasOpType::DrawRect>{ + .rect = SkRect::MakeWH(50, 50), + .paint = SkPaint(SkColors::kBlack), + }; + }; + + opCanvas.draw(makeDrawRect()); + + opCanvas.translate(10, 10); + opCanvas.draw(makeDrawRect()); + + opCanvas.save(); + opCanvas.scale(2.0f, 4.0f); + opCanvas.draw(makeDrawRect()); + opCanvas.restore(); + + opCanvas.save(); + opCanvas.translate(20, 15); + opCanvas.draw(makeDrawRect()); + opCanvas.save(); + opCanvas.rotate(90.f); + opCanvas.draw(makeDrawRect()); + opCanvas.restore(); + opCanvas.restore(); + + // Validate the results + std::vector<SkMatrix> transforms; + transforms.reserve(5); + receiver.for_each([&](auto op) { + // Filter for the DrawRect calls; ignore the save & restores + // (TODO: Add a filtered for_each variant to OpBuffer?) + if (op->type() == CanvasOpType::DrawRect) { + transforms.push_back(op->transform()); + } + }); + + EXPECT_EQ(transforms.size(), 5); + + { + // First result should be identity + const auto& result = transforms[0]; + EXPECT_EQ(SkMatrix::kIdentity_Mask, result.getType()); + EXPECT_EQ(SkMatrix::I(), result); + } + + { + // Should be translate 10, 10 + const auto& result = transforms[1]; + EXPECT_EQ(SkMatrix::kTranslate_Mask, result.getType()); + SkMatrix m; + m.setTranslate(10, 10); + EXPECT_EQ(m, result); + } + + { + // Should be translate 10, 10 + scale 2, 4 + const auto& result = transforms[2]; + EXPECT_EQ(SkMatrix::kTranslate_Mask | SkMatrix::kScale_Mask, result.getType()); + SkMatrix m; + m.setTranslate(10, 10); + m.preScale(2.0f, 4.0f); + EXPECT_EQ(m, result); + } + + { + // Should be translate 10, 10 + translate 20, 15 + const auto& result = transforms[3]; + EXPECT_EQ(SkMatrix::kTranslate_Mask, result.getType()); + SkMatrix m; + m.setTranslate(30, 25); + EXPECT_EQ(m, result); + } + + { + // Should be translate 10, 10 + translate 20, 15 + rotate 90 + const auto& result = transforms[4]; + EXPECT_EQ(SkMatrix::kTranslate_Mask | SkMatrix::kAffine_Mask | SkMatrix::kScale_Mask, + result.getType()); + SkMatrix m; + m.setTranslate(30, 25); + m.preRotate(90.f); + EXPECT_EQ(m, result); + } +}
\ No newline at end of file diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp index b15c3221dd60..033a5872ead3 100644 --- a/libs/hwui/tests/unit/CanvasOpTests.cpp +++ b/libs/hwui/tests/unit/CanvasOpTests.cpp @@ -16,6 +16,7 @@ #include <gtest/gtest.h> +#include <canvas/CanvasFrontend.h> #include <canvas/CanvasOpBuffer.h> #include <canvas/CanvasOps.h> #include <canvas/CanvasOpRasterizer.h> @@ -26,6 +27,7 @@ #include "SkColor.h" #include "SkLatticeIter.h" #include "pipeline/skia/AnimatedDrawables.h" +#include <SkNoDrawCanvas.h> using namespace android; using namespace android::uirenderer; @@ -78,6 +80,21 @@ struct MockOp<MockTypes::Lifecycle> { using MockBuffer = OpBuffer<MockTypes, MockOpContainer>; +class CanvasOpCountingReceiver { +public: + template <CanvasOpType T> + void push_container(CanvasOpContainer<T>&& op) { + mOpCounts[static_cast<size_t>(T)] += 1; + } + + int operator[](CanvasOpType op) const { + return mOpCounts[static_cast<size_t>(op)]; + } + +private: + std::array<int, static_cast<size_t>(CanvasOpType::COUNT)> mOpCounts; +}; + template<typename T> static int countItems(const T& t) { int count = 0; @@ -228,6 +245,31 @@ TEST(CanvasOp, simpleDrawPoint) { EXPECT_EQ(1, canvas.sumTotalDrawCalls()); } +TEST(CanvasOp, simpleDrawPoints) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + size_t numPts = 3; + auto pts = sk_ref_sp( + new Points({ + {32, 16}, + {48, 48}, + {16, 32} + }) + ); + + buffer.push(CanvasOp<Op::DrawPoints> { + .count = numPts, + .paint = SkPaint{}, + .points = pts + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPoints); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + TEST(CanvasOp, simpleDrawLine) { CanvasOpBuffer buffer; EXPECT_EQ(buffer.size(), 0); @@ -246,6 +288,30 @@ TEST(CanvasOp, simpleDrawLine) { EXPECT_EQ(1, canvas.sumTotalDrawCalls()); } +TEST(CanvasOp, simpleDrawLines) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + size_t numPts = 3; + auto pts = sk_ref_sp( + new Points({ + {32, 16}, + {48, 48}, + {16, 32} + }) + ); + buffer.push(CanvasOp<Op::DrawLines> { + .count = numPts, + .paint = SkPaint{}, + .points = pts + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPoints); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + TEST(CanvasOp, simpleDrawRect) { CanvasOpBuffer buffer; EXPECT_EQ(buffer.size(), 0); @@ -614,4 +680,35 @@ TEST(CanvasOp, immediateRendering) { rasterizer.draw(op); EXPECT_EQ(1, canvas->drawRectCount); EXPECT_EQ(1, canvas->sumTotalDrawCalls()); +} + +TEST(CanvasOp, frontendSaveCount) { + SkNoDrawCanvas skiaCanvas(100, 100); + CanvasFrontend<CanvasOpCountingReceiver> opCanvas(100, 100); + const auto& receiver = opCanvas.receiver(); + + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.save(); + opCanvas.save(SaveFlags::MatrixClip); + EXPECT_EQ(2, skiaCanvas.getSaveCount()); + EXPECT_EQ(2, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + EXPECT_EQ(1, receiver[Op::Save]); + EXPECT_EQ(1, receiver[Op::Restore]); +} + +TEST(CanvasOp, frontendTransform) { + }
\ No newline at end of file diff --git a/libs/hwui/tests/unit/CommonPoolTests.cpp b/libs/hwui/tests/unit/CommonPoolTests.cpp index da6a2604a4b6..bffdeca4db54 100644 --- a/libs/hwui/tests/unit/CommonPoolTests.cpp +++ b/libs/hwui/tests/unit/CommonPoolTests.cpp @@ -54,7 +54,9 @@ TEST(DISABLED_CommonPool, threadCount) { EXPECT_EQ(0, threads.count(gettid())); } -TEST(CommonPool, singleThread) { +// Disabled since this is flaky. This isn't a necessarily useful functional test, so being +// disabled isn't that significant. However it may be good to resurrect this somehow. +TEST(CommonPool, DISABLED_singleThread) { std::mutex mutex; std::condition_variable fence; bool isProcessing = false; diff --git a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp index f4c3e13b0ea6..955a5e7d8b3a 100644 --- a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp +++ b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp @@ -39,7 +39,7 @@ RENDERTHREAD_TEST(DeferredLayerUpdater, updateLayer) { EXPECT_EQ(Matrix4::identity(), layerUpdater->backingLayer()->getTexTransform()); // push the deferred updates to the layer - SkMatrix scaledMatrix = SkMatrix::MakeScale(0.5, 0.5); + SkMatrix scaledMatrix = SkMatrix::Scale(0.5, 0.5); SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); sk_sp<SkImage> layerImage = SkImage::MakeFromBitmap(bitmap); diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index abdf9d587189..26bc65915c7a 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -206,7 +206,7 @@ TEST(RenderNodeDrawable, saveLayerClipAndMatrixRestore) { ASSERT_EQ(SkRect::MakeLTRB(50, 50, 350, 350), getRecorderClipBounds(recorder)); recorder.translate(300.0f, 400.0f); - EXPECT_EQ(SkMatrix::MakeTrans(300.0f, 400.0f), getRecorderMatrix(recorder)); + EXPECT_EQ(SkMatrix::Translate(300.0f, 400.0f), getRecorderMatrix(recorder)); recorder.restore(); ASSERT_EQ(SkRect::MakeLTRB(0, 0, 400, 800), getRecorderClipBounds(recorder)); @@ -1107,27 +1107,27 @@ TEST(ReorderBarrierDrawable, testShadowMatrix) { EXPECT_EQ(dy, TRANSLATE_Y); } - virtual void didSetMatrix(const SkMatrix& matrix) override { + virtual void didSetM44(const SkM44& matrix) override { mDrawCounter++; // First invocation is EndReorderBarrierDrawable::drawShadow to apply shadow matrix. // Second invocation is preparing the matrix for an elevated RenderNodeDrawable. - EXPECT_TRUE(matrix.isIdentity()); + EXPECT_TRUE(matrix == SkM44()); EXPECT_TRUE(getTotalMatrix().isIdentity()); } - virtual void didConcat(const SkMatrix& matrix) override { + virtual void didConcat44(const SkM44& matrix) override { mDrawCounter++; if (mFirstDidConcat) { // First invocation is EndReorderBarrierDrawable::drawShadow to apply shadow matrix. mFirstDidConcat = false; - EXPECT_EQ(SkMatrix::MakeTrans(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), + EXPECT_EQ(SkM44::Translate(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), matrix); - EXPECT_EQ(SkMatrix::MakeTrans(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), + EXPECT_EQ(SkMatrix::Translate(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), getTotalMatrix()); } else { // Second invocation is preparing the matrix for an elevated RenderNodeDrawable. - EXPECT_EQ(SkMatrix::MakeTrans(TRANSLATE_X, TRANSLATE_Y), matrix); - EXPECT_EQ(SkMatrix::MakeTrans(TRANSLATE_X, TRANSLATE_Y), getTotalMatrix()); + EXPECT_EQ(SkM44::Translate(TRANSLATE_X, TRANSLATE_Y), matrix); + EXPECT_EQ(SkMatrix::Translate(TRANSLATE_X, TRANSLATE_Y), getTotalMatrix()); } } diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp index c19e1ed6ce75..4659a929a9eb 100644 --- a/libs/hwui/tests/unit/RenderNodeTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -264,6 +264,8 @@ TEST(RenderNode, releasedCallback) { TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { TestUtils::syncHierarchyPropertiesAndDisplayList(node); }); + // Fence on any remaining post'd work + TestUtils::runOnRenderThreadUnmanaged([] (RenderThread&) {}); EXPECT_EQ(2, counts.sync); EXPECT_EQ(1, counts.destroyed); } diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp index 74a565439f85..c63f008c4aed 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -84,7 +84,7 @@ TEST(SkiaDisplayList, reuseDisplayList) { // attach a displayList for reuse SkiaDisplayList skiaDL; - ASSERT_TRUE(skiaDL.reuseDisplayList(renderNode.get(), nullptr)); + ASSERT_TRUE(skiaDL.reuseDisplayList(renderNode.get())); // detach the list that you just attempted to reuse availableList = renderNode->detachAvailableList(); @@ -263,10 +263,10 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscr } // Another way to be offscreen: a matrix from the draw call. - for (const SkMatrix translate : { SkMatrix::MakeTrans(width, 0), - SkMatrix::MakeTrans(0, height), - SkMatrix::MakeTrans(-width, 0), - SkMatrix::MakeTrans(0, -height)}) { + for (const SkMatrix translate : { SkMatrix::Translate(width, 0), + SkMatrix::Translate(0, height), + SkMatrix::Translate(-width, 0), + SkMatrix::Translate(0, -height)}) { SkiaDisplayList skiaDL; VectorDrawableRoot dirtyVD(new VectorDrawable::Group()); dirtyVD.mutateProperties()->setBounds(bounds); @@ -291,7 +291,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscr SkiaDisplayList skiaDL; VectorDrawableRoot dirtyVD(new VectorDrawable::Group()); dirtyVD.mutateProperties()->setBounds(bounds); - SkMatrix translate = SkMatrix::MakeTrans(50, 50); + SkMatrix translate = SkMatrix::Translate(50, 50); skiaDL.appendVD(&dirtyVD, translate); ASSERT_TRUE(dirtyVD.isDirty()); diff --git a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp index eec25c6bd40d..15ecf5831f3a 100644 --- a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp +++ b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp @@ -111,11 +111,11 @@ TEST(RenderNodeDrawable, renderPropTransform) { [](RenderProperties& properties) { properties.setLeftTopRightBottom(10, 10, 110, 110); - SkMatrix staticMatrix = SkMatrix::MakeScale(1.2f, 1.2f); + SkMatrix staticMatrix = SkMatrix::Scale(1.2f, 1.2f); properties.setStaticMatrix(&staticMatrix); // ignored, since static overrides animation - SkMatrix animationMatrix = SkMatrix::MakeTrans(15, 15); + SkMatrix animationMatrix = SkMatrix::Translate(15, 15); properties.setAnimationMatrix(&animationMatrix); properties.setTranslationX(10); diff --git a/libs/incident/OWNERS b/libs/incident/OWNERS new file mode 100644 index 000000000000..f76611555dbb --- /dev/null +++ b/libs/incident/OWNERS @@ -0,0 +1 @@ +include /cmds/incidentd/OWNERS diff --git a/libs/input/OWNERS b/libs/input/OWNERS new file mode 100644 index 000000000000..d701f23cb9b8 --- /dev/null +++ b/libs/input/OWNERS @@ -0,0 +1 @@ +include /core/java/android/hardware/input/OWNERS diff --git a/libs/storage/OWNERS b/libs/storage/OWNERS new file mode 100644 index 000000000000..6f9dbea36b06 --- /dev/null +++ b/libs/storage/OWNERS @@ -0,0 +1 @@ +include /core/java/android/os/storage/OWNERS diff --git a/libs/usb/OWNERS b/libs/usb/OWNERS new file mode 100644 index 000000000000..f7b2a37a297a --- /dev/null +++ b/libs/usb/OWNERS @@ -0,0 +1 @@ +include /services/usb/OWNERS |