diff options
Diffstat (limited to 'libs')
96 files changed, 2775 insertions, 1388 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..94555de4f05c 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; @@ -56,6 +57,7 @@ public class ShellInit { mFullscreenTaskListener = fullscreenTaskListener; } + @ExternalThread public void init() { // Start listening for display changes mDisplayImeController.startMonitorDisplays(); 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..174c16afc75f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -31,12 +31,14 @@ import android.os.Binder; import android.os.IBinder; import android.util.ArrayMap; import android.util.Log; +import android.util.Slog; import android.util.SparseArray; import android.view.SurfaceControl; import android.window.ITaskOrganizerController; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; +import androidx.annotation.BinderThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -45,6 +47,7 @@ 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.common.annotations.ShellMainThread; import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; import java.io.PrintWriter; @@ -119,7 +122,7 @@ public class ShellTaskOrganizer extends TaskOrganizer { ShellExecutor mainExecutor, ShellExecutor animExecutor, Context context) { super(taskOrganizerController, mainExecutor); mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor); - if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions); + if (Transitions.ENABLE_SHELL_TRANSITIONS) mTransitions.register(this); // 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. 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 120039de1240..10195b6a26b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java @@ -23,9 +23,9 @@ import static android.window.TransitionInfo.TRANSIT_SHOW; 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,15 +35,18 @@ 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; +import com.android.wm.shell.common.annotations.ShellMainThread; 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,6 +57,7 @@ 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<>(); @@ -64,6 +68,11 @@ public class Transitions extends ITransitionPlayer.Stub { mTransactionPool = pool; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mPlayerImpl = new TransitionPlayerImpl(); + } + + public void register(ShellTaskOrganizer taskOrganizer) { + taskOrganizer.registerTransitionPlayer(mPlayerImpl); } // TODO(shell-transitions): real animations @@ -115,77 +124,73 @@ public class Transitions extends ITransitionPlayer.Stub { || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; } - @Override - public void onTransitionReady(@NonNull IBinder transitionToken, @NonNull 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<>()); - 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_SHOW) { - t.show(leash); - t.setMatrix(leash, 1, 0, 0, 1); - } - continue; - } - - 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 (!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_SHOW) { 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_HIDE) { - 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 */); - } + } + continue; + } + + 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_SHOW) { + 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_HIDE) { + 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); - }); + } + t.apply(); + onFinish(transitionToken); } - @MainThread private void onFinish(IBinder transition) { if (!mActiveTransitions.get(transition).isEmpty()) return; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, @@ -194,16 +199,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/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/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..5593268588fd 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,16 @@ package com.android.wm.shell.pip; import android.app.RemoteAction; import android.content.ComponentName; import android.content.pm.ParceledListSlice; +import android.os.RemoteException; import android.view.DisplayInfo; -import android.view.IPinnedStackController; 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 +36,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,69 +56,110 @@ 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 onDisplayInfoChanged(DisplayInfo displayInfo) { for (PinnedStackListener listener : mListeners) { listener.onDisplayInfoChanged(displayInfo); } } - @Override - public void onConfigurationChanged() { + private void onConfigurationChanged() { for (PinnedStackListener listener : mListeners) { listener.onConfigurationChanged(); } } - @Override - public void onAspectRatioChanged(float aspectRatio) { + private void onAspectRatioChanged(float aspectRatio) { for (PinnedStackListener listener : mListeners) { listener.onAspectRatioChanged(aspectRatio); } } + @BinderThread + private class PinnedStackListenerImpl extends IPinnedStackListener.Stub { + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onMovementBoundsChanged(fromImeAdjustment); + }); + } + + @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 onDisplayInfoChanged(DisplayInfo displayInfo) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onDisplayInfoChanged(displayInfo); + }); + } + + @Override + public void onConfigurationChanged() { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onConfigurationChanged(); + }); + } + + @Override + public void onAspectRatioChanged(float aspectRatio) { + mShellMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onAspectRatioChanged(aspectRatio); + }); + } + } + /** * A counterpart of {@link IPinnedStackListener} with empty implementations. * 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) {} 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..da9ce0aacedc 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,12 @@ 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.graphics.Rect; +import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.pip.phone.PipTouchHandler; import java.io.PrintWriter; @@ -31,6 +31,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). 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/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..167b9f9975f3 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,8 @@ 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 +672,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 +815,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 +966,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 +992,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 +1011,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 +1024,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 +1088,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 d4217553ef2d..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. */ @@ -250,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) { @@ -290,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) { @@ -391,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(); @@ -406,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. */ @@ -421,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() { @@ -521,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..3234ef6ccf66 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 @@ -41,7 +41,6 @@ import android.util.Log; import android.util.Pair; import android.util.Slog; import android.view.DisplayInfo; -import android.view.IPinnedStackController; import android.view.WindowManagerGlobal; import android.window.WindowContainerTransaction; @@ -52,7 +51,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; @@ -92,7 +90,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac private boolean mIsInFixedRotation; private Consumer<Boolean> mPinnedStackAnimationRecentsCallback; - protected PipMenuActivityController mMenuController; + protected PhonePipMenuController mMenuController; protected PipTaskOrganizer mPipTaskOrganizer; protected PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener = new PipControllerPinnedStackListener(); @@ -163,63 +161,50 @@ 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); - } - }); + 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)); + mPipBoundsState.setDisplayInfo(displayInfo); } @Override public void onConfigurationChanged() { - mMainExecutor.execute(() -> { - mPipBoundsAlgorithm.onConfigurationChanged(mContext); - mTouchHandler.onConfigurationChanged(); - mPipBoundsState.onConfigurationChanged(); - }); + mPipBoundsAlgorithm.onConfigurationChanged(mContext); + mTouchHandler.onConfigurationChanged(); + mPipBoundsState.onConfigurationChanged(); } @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 +214,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 +235,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(), @@ -632,7 +617,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 +626,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..88a11689b90b 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,6 +89,7 @@ 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; @@ -96,15 +98,17 @@ public class PipResizeGestureHandler { 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 +116,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 +126,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 +223,7 @@ public class PipResizeGestureHandler { if (ev instanceof MotionEvent) { if (mUsingPinchToZoom) { - mScaleGestureDetector.onTouchEvent((MotionEvent) ev); + onPinchResize((MotionEvent) ev); } else { onDragCornerResize((MotionEvent) ev); } @@ -282,6 +231,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 +251,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 +283,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 +303,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 +341,78 @@ 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); + } + } + } + private void onDragCornerResize(MotionEvent ev) { int action = ev.getActionMasked(); float x = ev.getX(); @@ -415,15 +420,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 +447,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, @@ -468,15 +473,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..9281f58f522f 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; @@ -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..763370bec1c9 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,7 +36,6 @@ 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; @@ -59,13 +58,14 @@ 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 @@ -111,15 +111,13 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; + private final TvPipMenuController mTvPipMenuController; 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; @@ -181,46 +179,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 +213,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, + TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, PipNotification pipNotification, TaskStackListenerImpl taskStackListener, @@ -237,6 +223,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(); @@ -289,9 +277,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,14 +287,10 @@ 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); } @@ -379,16 +360,22 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac } 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(); @@ -434,8 +421,7 @@ 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); } } @@ -451,7 +437,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac Log.d(TAG, "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); } - mSuspendPipResizingReason |= reason; } @@ -478,6 +463,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac * @param state In Pip state also used to determine the new size for the Pip. */ public void resizePinnedStack(int state) { + if (DEBUG) { Log.d(TAG, "resizePinnedStack() state=" + stateToName(state) + ", current state=" + getStateDescription(), new Exception()); @@ -509,11 +495,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) { @@ -544,10 +530,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac 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); + + mTvPipMenuController.showMenu(); } /** @@ -650,8 +634,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac 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. */ 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/PipMenuView.java index d2270c278161..689c3ede9efa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java @@ -18,109 +18,110 @@ package com.android.wm.shell.pip.tv; import android.animation.Animator; import android.animation.AnimatorInflater; -import android.app.Activity; +import android.annotation.Nullable; import android.app.RemoteAction; -import android.content.Intent; +import android.content.Context; import android.content.pm.ParceledListSlice; -import android.os.Bundle; 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; /** - * Activity to show the PIP menu to control PIP. - * TODO(b/169395392) Refactor PipMenuActivity to PipMenuView + * The Menu View that shows controls of the PiP. Always fullscreen. */ -public class PipMenuActivity extends Activity implements PipController.Listener { - private static final String TAG = "PipMenuActivity"; +public class PipMenuView extends FrameLayout implements PipController.Listener { + private static final String TAG = "PipMenuView"; 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 final PipController mPipController; + private final Animator mFadeInAnimation; + private final Animator mFadeOutAnimation; + private final PipControlsViewController mPipControlsViewController; private boolean mRestorePipSizeWhenClose; - private PipControlsViewController mPipControlsViewController; - @Override - protected void onCreate(Bundle bundle) { - if (DEBUG) Log.d(TAG, "onCreate()"); + public PipMenuView(Context context, PipController pipController) { + super(context, null, 0); + mPipController = pipController; + + inflate(context, R.layout.tv_pip_menu, this); - 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); + findViewById(R.id.pip_controls), mPipController); mRestorePipSizeWhenClose = true; mFadeInAnimation = AnimatorInflater.loadAnimator( - this, R.anim.tv_pip_menu_fade_in_animation); + mContext, R.anim.tv_pip_menu_fade_in_animation); mFadeInAnimation.setTarget(mPipControlsViewController.getView()); mFadeOutAnimation = AnimatorInflater.loadAnimator( - this, R.anim.tv_pip_menu_fade_out_animation); + mContext, 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)); + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK + && event.getAction() == KeyEvent.ACTION_UP) { + restorePipAndFinish(); + return true; + } + return super.dispatchKeyEvent(event); } - 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); + @Nullable + SurfaceControl getWindowSurfaceControl() { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { + return null; + } + final SurfaceControl out = root.getSurfaceControl(); + if (out != null && out.isValid()) { + return out; } - finish(); + return null; } - @Override - public void onResume() { - if (DEBUG) Log.d(TAG, "onResume()"); - - super.onResume(); + void showMenu() { + mPipController.addListener(this); mFadeInAnimation.start(); + setAlpha(1.0f); + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + getViewRootImpl().getInputToken(), true /* grantFocus */); + } catch (Exception e) { + Log.e(TAG, "Unable to update focus as menu appears", e); + } } - @Override - public void onPause() { - if (DEBUG) Log.d(TAG, "onPause()"); - - super.onPause(); + void hideMenu() { + mPipController.removeListener(this); + mPipController.resumePipResizing( + PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH); mFadeOutAnimation.start(); - restorePipAndFinish(); + setAlpha(0.0f); + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + getViewRootImpl().getInputToken(), false /* grantFocus */); + } catch (Exception e) { + Log.e(TAG, "Unable to update focus as menu disappears", e); + } } - @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); - } + private void restorePipAndFinish() { + if (DEBUG) Log.d(TAG, "restorePipAndFinish()"); - @Override - public void onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed()"); + if (mRestorePipSizeWhenClose) { + if (DEBUG) Log.d(TAG, " > restoring to the default position"); - restorePipAndFinish(); + // When PIP menu activity is closed, restore to the default position. + mPipController.resizePinnedStack(PipController.STATE_PIP); + } + hideMenu(); } @Override @@ -132,11 +133,10 @@ public class PipMenuActivity extends Activity implements PipController.Listener public void onPipActivityClosed() { if (DEBUG) Log.d(TAG, "onPipActivityClosed()"); - finish(); + hideMenu(); } - @Override - public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { + void setAppActions(ParceledListSlice<RemoteAction> actions) { if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()"); boolean hasCustomActions = actions != null && !actions.getList().isEmpty(); @@ -156,34 +156,15 @@ public class PipMenuActivity extends Activity implements PipController.Listener // 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(); + hideMenu(); } @Override public void onPipResizeAboutToStart() { if (DEBUG) Log.d(TAG, "onPipResizeAboutToStart()"); - finish(); - sPipController.suspendPipResizing( + hideMenu(); + mPipController.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/PipNotification.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java index b30dee4f331f..d56a88874420 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; @@ -99,11 +97,6 @@ public class PipNotification implements PipController.Listener { } @Override - public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { - // no-op. - } - - @Override public void onMoveToFullscreen() { dismissPipNotification(); mPackageName = 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..91aef670b946 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -0,0 +1,98 @@ +/* + * 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.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 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 (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(); + } + } + } + + @Override + public void attach(SurfaceControl leash) { + if (mMenuView == null) { + mMenuView = new PipMenuView(mContext, mPipController); + mSystemWindows.addView(mMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); + mLeash = leash; + } + } + + @Override + public void detach() { + mSystemWindows.removeView(mMenuView); + mMenuView = null; + mLeash = null; + } + + @Override + public void setAppActions(ParceledListSlice<RemoteAction> appActions) { + mMenuView.setAppActions(appActions); + } + + @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/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml index 101b5bf27c77..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 --> @@ -38,7 +40,8 @@ <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> <!-- ATM.removeRootTasksWithActivityTypes() --> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> - <application> + <!-- 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..3ed53fb221a7 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( @@ -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/apppairs/AppPairsTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt index ced99de21a46..7ac91b065fca 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 @@ -29,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 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 index 0663eb344f46..c9396aa7ea63 100644 --- 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 @@ -23,10 +23,10 @@ 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.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 com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP import org.junit.FixMethodOrder import org.junit.Test 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 index 322034ce7688..76aabc1b83cf 100644 --- 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 @@ -24,10 +24,10 @@ 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.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 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 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 index 96d98d56e069..a67b3b760c49 100644 --- 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 @@ -31,13 +31,13 @@ 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.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.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 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 index d20552f0739d..f79b21ff278d 100644 --- 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 @@ -28,10 +28,10 @@ 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.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 com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP import org.junit.FixMethodOrder import org.junit.Test 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 c61a0f171714..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 @@ -25,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 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 bf9286980b9a..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 @@ -29,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 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 index 1e328a8dae40..c85561d96091 100644 --- 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 @@ -17,7 +17,7 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.Presubmit -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.DOCKED_STACK_DIVIDER @@ -54,7 +54,6 @@ import org.junit.runners.Parameterized @RequiresDevice @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 161435597) class OpenAppToSplitScreenTest( testName: String, flickerSpec: Flicker @@ -67,7 +66,8 @@ class OpenAppToSplitScreenTest( val testApp = StandardAppHelper(instrumentation, "com.android.wm.shell.flicker.testapp", "SimpleApp") - return FlickerTestRunnerFactory(instrumentation) + // 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) 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/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/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/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..4687d2d9667c 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 @@ -61,7 +61,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 +77,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 +110,7 @@ public class PipControllerTest extends ShellTestCase { assertNull(PipController.create(spyContext, mMockDisplayController, mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, - mMockPipMediaController, mMockPipMenuActivityController, mMockPipTaskOrganizer, + mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockExecutor)); } 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 cd53217d2924..1ff1978044b9 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", 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..738246d56d0d 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 { 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/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/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/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..242dbdb27362 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; @@ -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/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/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/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp index f186e55ec2e3..033a5872ead3 100644 --- a/libs/hwui/tests/unit/CanvasOpTests.cpp +++ b/libs/hwui/tests/unit/CanvasOpTests.cpp @@ -245,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); @@ -263,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); 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 |