summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java17
-rw-r--r--libs/WindowManager/Shell/Android.bp111
-rw-r--r--libs/WindowManager/Shell/proto/wm_shell_trace.proto27
-rw-r--r--libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml21
-rw-r--r--libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml22
-rw-r--r--libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml20
-rw-r--r--libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml20
-rw-r--r--libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml20
-rw-r--r--libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml20
-rw-r--r--libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.pngbin0 -> 1766 bytes
-rw-r--r--libs/WindowManager/Shell/res/drawable/dismiss_circle_background.xml28
-rw-r--r--libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient_transition.xml20
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_expand.xml28
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_close_white.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_fullscreen_white.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_pause_white.xml26
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_play_arrow_white.xml26
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_settings.xml28
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_skip_next_white.xml28
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_skip_previous_white.xml28
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_icon.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_resize_handle.xml29
-rw-r--r--libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml18
-rw-r--r--libs/WindowManager/Shell/res/layout/divider.xml21
-rw-r--r--libs/WindowManager/Shell/res/layout/docked_stack_divider.xml38
-rw-r--r--libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml26
-rw-r--r--libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml67
-rw-r--r--libs/WindowManager/Shell/res/layout/pip_menu.xml100
-rw-r--r--libs/WindowManager/Shell/res/layout/pip_menu_action.xml23
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml50
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_controls.xml43
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_custom_control.xml21
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_menu.xml31
-rw-r--r--libs/WindowManager/Shell/res/raw/wm_shell_protolog.json97
-rw-r--r--libs/WindowManager/Shell/res/values-land/dimens.xml21
-rw-r--r--libs/WindowManager/Shell/res/values-land/styles.xml35
-rw-r--r--libs/WindowManager/Shell/res/values-sw600dp/config.xml25
-rw-r--r--libs/WindowManager/Shell/res/values-tvdpi/dimen.xml22
-rw-r--r--libs/WindowManager/Shell/res/values/colors.xml25
-rw-r--r--libs/WindowManager/Shell/res/values/config.xml52
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml72
-rw-r--r--libs/WindowManager/Shell/res/values/ids.xml26
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml100
-rw-r--r--libs/WindowManager/Shell/res/values/strings_tv.xml34
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java123
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java409
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java181
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java63
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java423
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/FloatProperties.kt141
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt1071
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt485
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java61
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java109
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java273
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java540
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java511
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt368
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java176
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java365
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt699
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java75
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java302
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java431
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java377
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java276
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java269
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java144
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java88
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java159
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java173
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java221
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java125
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java262
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java462
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsHandler.java507
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java139
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java204
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java147
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java1200
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java114
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java274
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java97
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java486
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java295
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMediaController.java261
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActivityController.java395
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java86
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java474
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java662
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java523
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java42
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java893
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java391
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUpdateThread.java60
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUtils.java59
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java207
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java750
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java254
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java190
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java253
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java96
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java135
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java146
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java415
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java (renamed from libs/WindowManager/Shell/tests/src/com/android/wm/shell/tests/WindowManagerShellTest.java)27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java1339
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java115
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java108
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java148
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java99
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java313
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java86
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java567
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java249
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java366
-rw-r--r--libs/WindowManager/Shell/tests/README.md15
-rw-r--r--libs/WindowManager/Shell/tests/flicker/Android.bp52
-rw-r--r--libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml43
-rw-r--r--libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml41
-rw-r--r--libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml41
-rw-r--r--libs/WindowManager/Shell/tests/flicker/README.md10
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt153
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt130
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NonRotationTestBase.kt36
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FlickerAppHelper.kt31
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt53
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt45
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt121
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt217
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt31
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/Android.bp0
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp20
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml54
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java33
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java45
-rw-r--r--libs/WindowManager/Shell/tests/unittest/Android.bp (renamed from libs/WindowManager/Shell/tests/Android.bp)14
-rw-r--r--libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml (renamed from libs/WindowManager/Shell/tests/AndroidManifest.xml)0
-rw-r--r--libs/WindowManager/Shell/tests/unittest/AndroidTest.xml (renamed from libs/WindowManager/Shell/tests/AndroidTest.xml)4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/res/values/config.xml (renamed from libs/WindowManager/Shell/tests/res/values/config.xml)0
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java251
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt627
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java86
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java140
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt454
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java71
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java193
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java321
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java105
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java93
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java111
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java104
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java105
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java69
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java70
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java196
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsHandlerTest.java309
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java110
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java74
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTestCase.java53
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java103
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java161
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java161
-rw-r--r--libs/androidfw/Android.bp23
-rw-r--r--libs/androidfw/BackupHelpers.cpp2
-rw-r--r--libs/androidfw/CursorWindow.cpp476
-rw-r--r--libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp31
-rw-r--r--libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.binbin0 -> 292 bytes
-rw-r--r--libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp78
-rw-r--r--libs/androidfw/include/androidfw/BackupHelpers.h2
-rw-r--r--libs/androidfw/include/androidfw/CursorWindow.h139
-rw-r--r--libs/androidfw/tests/BackupHelpers_test.cpp63
-rw-r--r--libs/androidfw/tests/CursorWindow_bench.cpp86
-rw-r--r--libs/androidfw/tests/CursorWindow_test.cpp367
-rw-r--r--libs/hostgraphics/Android.bp6
-rw-r--r--libs/hwui/Android.bp56
-rw-r--r--libs/hwui/AnimationContext.h16
-rw-r--r--libs/hwui/Animator.h54
-rw-r--r--libs/hwui/AnimatorManager.h2
-rw-r--r--libs/hwui/AutoBackendTextureRelease.cpp10
-rw-r--r--libs/hwui/AutoBackendTextureRelease.h9
-rw-r--r--libs/hwui/CanvasTransform.cpp1
-rw-r--r--libs/hwui/ColorMode.h34
-rw-r--r--libs/hwui/DamageAccumulator.h2
-rw-r--r--libs/hwui/DeferredLayerUpdater.cpp2
-rw-r--r--libs/hwui/DeferredLayerUpdater.h18
-rw-r--r--libs/hwui/DeviceInfo.cpp129
-rw-r--r--libs/hwui/DeviceInfo.h53
-rw-r--r--libs/hwui/FrameInfo.cpp4
-rw-r--r--libs/hwui/FrameInfo.h17
-rw-r--r--libs/hwui/HardwareBitmapUploader.cpp192
-rw-r--r--libs/hwui/HardwareBitmapUploader.h2
-rw-r--r--libs/hwui/Interpolator.h22
-rw-r--r--libs/hwui/Matrix.h2
-rw-r--r--libs/hwui/PathParser.h10
-rw-r--r--libs/hwui/Properties.cpp1
-rw-r--r--libs/hwui/Properties.h16
-rw-r--r--libs/hwui/PropertyValuesAnimatorSet.h2
-rw-r--r--libs/hwui/PropertyValuesHolder.h18
-rw-r--r--libs/hwui/Readback.cpp7
-rw-r--r--libs/hwui/RecordingCanvas.cpp67
-rw-r--r--libs/hwui/RecordingCanvas.h20
-rw-r--r--libs/hwui/RenderNode.h22
-rw-r--r--libs/hwui/RenderProperties.cpp7
-rw-r--r--libs/hwui/RenderProperties.h11
-rw-r--r--libs/hwui/RootRenderNode.h19
-rw-r--r--libs/hwui/SkiaCanvas.cpp9
-rw-r--r--libs/hwui/SkiaCanvas.h9
-rw-r--r--libs/hwui/VectorDrawable.h14
-rw-r--r--libs/hwui/apex/LayoutlibLoader.cpp2
-rw-r--r--libs/hwui/apex/java/android/graphics/ColorMatrix.java288
-rw-r--r--libs/hwui/apex/jni_runtime.cpp23
-rw-r--r--libs/hwui/api/current.txt23
-rw-r--r--libs/hwui/api/module-lib-current.txt1
-rw-r--r--libs/hwui/api/module-lib-removed.txt1
-rw-r--r--libs/hwui/api/removed.txt1
-rw-r--r--libs/hwui/api/system-current.txt1
-rw-r--r--libs/hwui/api/system-removed.txt1
-rw-r--r--libs/hwui/hwui/AnimatedImageDrawable.h2
-rw-r--r--libs/hwui/hwui/Bitmap.cpp40
-rw-r--r--libs/hwui/hwui/Bitmap.h16
-rw-r--r--libs/hwui/hwui/Canvas.cpp38
-rw-r--r--libs/hwui/hwui/Canvas.h13
-rw-r--r--libs/hwui/hwui/MinikinSkia.cpp19
-rw-r--r--libs/hwui/hwui/MinikinSkia.h2
-rw-r--r--libs/hwui/hwui/MinikinUtils.cpp13
-rw-r--r--libs/hwui/hwui/MinikinUtils.h19
-rw-r--r--libs/hwui/hwui/Paint.h2
-rw-r--r--libs/hwui/hwui/Typeface.cpp2
-rwxr-xr-xlibs/hwui/jni/Bitmap.cpp447
-rw-r--r--libs/hwui/jni/BitmapFactory.cpp11
-rw-r--r--libs/hwui/jni/BitmapRegionDecoder.cpp82
-rw-r--r--libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp21
-rw-r--r--libs/hwui/jni/CreateJavaOutputStreamAdaptor.h8
-rw-r--r--libs/hwui/jni/FontFamily.cpp14
-rw-r--r--libs/hwui/jni/FontUtils.h5
-rw-r--r--libs/hwui/jni/Graphics.cpp61
-rw-r--r--libs/hwui/jni/GraphicsJNI.h27
-rw-r--r--libs/hwui/jni/ImageDecoder.cpp5
-rw-r--r--libs/hwui/jni/Movie.cpp8
-rw-r--r--libs/hwui/jni/Paint.cpp68
-rw-r--r--libs/hwui/jni/Picture.cpp2
-rw-r--r--libs/hwui/jni/RenderEffect.cpp69
-rw-r--r--libs/hwui/jni/Shader.cpp63
-rw-r--r--libs/hwui/jni/Utils.cpp4
-rw-r--r--libs/hwui/jni/Utils.h5
-rw-r--r--libs/hwui/jni/android_graphics_Canvas.cpp17
-rw-r--r--libs/hwui/jni/android_graphics_DisplayListCanvas.cpp44
-rw-r--r--libs/hwui/jni/android_graphics_HardwareRenderer.cpp187
-rw-r--r--libs/hwui/jni/android_graphics_RenderNode.cpp7
-rw-r--r--libs/hwui/jni/android_graphics_TextureLayer.cpp4
-rw-r--r--libs/hwui/jni/fonts/Font.cpp192
-rw-r--r--libs/hwui/jni/fonts/FontFamily.cpp2
-rw-r--r--libs/hwui/jni/text/TextShaper.cpp206
-rw-r--r--libs/hwui/libhwui.map.txt70
-rw-r--r--libs/hwui/pipeline/skia/FunctorDrawable.h31
-rw-r--r--libs/hwui/pipeline/skia/GLFunctorDrawable.cpp30
-rw-r--r--libs/hwui/pipeline/skia/GLFunctorDrawable.h2
-rw-r--r--libs/hwui/pipeline/skia/LayerDrawable.cpp10
-rw-r--r--libs/hwui/pipeline/skia/LayerDrawable.h8
-rw-r--r--libs/hwui/pipeline/skia/RenderNodeDrawable.cpp4
-rw-r--r--libs/hwui/pipeline/skia/ShaderCache.cpp4
-rw-r--r--libs/hwui/pipeline/skia/ShaderCache.h4
-rw-r--r--libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp2
-rw-r--r--libs/hwui/pipeline/skia/SkiaPipeline.cpp39
-rw-r--r--libs/hwui/pipeline/skia/SkiaPipeline.h4
-rw-r--r--libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp25
-rw-r--r--libs/hwui/pipeline/skia/SkiaRecordingCanvas.h5
-rw-r--r--libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp2
-rw-r--r--libs/hwui/pipeline/skia/VkFunctorDrawable.cpp11
-rw-r--r--libs/hwui/pipeline/skia/VkFunctorDrawable.h1
-rw-r--r--libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp73
-rw-r--r--libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h11
-rw-r--r--libs/hwui/renderthread/CacheManager.cpp9
-rw-r--r--libs/hwui/renderthread/CacheManager.h6
-rw-r--r--libs/hwui/renderthread/CanvasContext.cpp26
-rw-r--r--libs/hwui/renderthread/CanvasContext.h10
-rw-r--r--libs/hwui/renderthread/DrawFrameTask.cpp5
-rw-r--r--libs/hwui/renderthread/EglManager.cpp116
-rw-r--r--libs/hwui/renderthread/EglManager.h5
-rw-r--r--libs/hwui/renderthread/IRenderPipeline.h13
-rw-r--r--libs/hwui/renderthread/ReliableSurface.cpp28
-rw-r--r--libs/hwui/renderthread/ReliableSurface.h4
-rw-r--r--libs/hwui/renderthread/RenderProxy.cpp22
-rw-r--r--libs/hwui/renderthread/RenderProxy.h107
-rw-r--r--libs/hwui/renderthread/RenderTask.h8
-rw-r--r--libs/hwui/renderthread/RenderThread.cpp32
-rw-r--r--libs/hwui/renderthread/RenderThread.h16
-rw-r--r--libs/hwui/renderthread/TimeLord.cpp18
-rw-r--r--libs/hwui/renderthread/TimeLord.h8
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp120
-rw-r--r--libs/hwui/renderthread/VulkanManager.h36
-rw-r--r--libs/hwui/renderthread/VulkanSurface.cpp15
-rw-r--r--libs/hwui/renderthread/VulkanSurface.h17
-rw-r--r--libs/hwui/service/GraphicsStatsService.h22
-rw-r--r--libs/hwui/shader/BlurShader.cpp41
-rw-r--r--libs/hwui/shader/BlurShader.h46
-rw-r--r--libs/hwui/tests/common/TestUtils.h24
-rw-r--r--libs/hwui/tests/common/scenes/MagnifierAnimation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/RecentsAnimation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/RectGridAnimation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/TvApp.cpp4
-rw-r--r--libs/hwui/tests/macrobench/TestSceneRunner.cpp6
-rw-r--r--libs/hwui/tests/microbench/DisplayListCanvasBench.cpp4
-rwxr-xr-xlibs/hwui/tests/scripts/prep_generic.sh209
-rw-r--r--libs/hwui/tests/unit/CacheManagerTests.cpp6
-rw-r--r--libs/hwui/tests/unit/CanvasContextTests.cpp11
-rw-r--r--libs/hwui/tests/unit/FatalTestCanvas.h15
-rw-r--r--libs/hwui/tests/unit/RenderNodeDrawableTests.cpp24
-rw-r--r--libs/hwui/tests/unit/RenderNodeTests.cpp50
-rw-r--r--libs/hwui/tests/unit/SkiaCanvasTests.cpp4
-rw-r--r--libs/hwui/tests/unit/SkiaDisplayListTests.cpp17
-rw-r--r--libs/hwui/tests/unit/TypefaceTests.cpp54
-rw-r--r--libs/hwui/utils/Blur.h4
-rw-r--r--libs/hwui/utils/Color.cpp61
-rw-r--r--libs/hwui/utils/Color.h16
-rw-r--r--libs/hwui/utils/MathUtils.h4
-rw-r--r--libs/hwui/utils/NdkUtils.cpp (renamed from libs/hwui/GlFunctorLifecycleListener.h)20
-rw-r--r--libs/hwui/utils/NdkUtils.h38
-rw-r--r--libs/hwui/utils/VectorDrawableUtils.h6
-rw-r--r--libs/usb/tests/AccessoryChat/AndroidManifest.xml20
338 files changed, 33767 insertions, 1943 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java
index 92e575804bbe..5397302f6882 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java
@@ -37,6 +37,8 @@ import android.util.Log;
import androidx.annotation.NonNull;
+import com.android.internal.R;
+
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
@@ -151,13 +153,18 @@ class SettingsSidecarImpl extends StubSidecar {
return features;
}
- ContentResolver resolver = mContext.getContentResolver();
- final String displayFeaturesString = Settings.Global.getString(resolver, DISPLAY_FEATURES);
if (isInMultiWindow(windowToken)) {
// It is recommended not to report any display features in multi-window mode, since it
// won't be possible to synchronize the display feature positions with window movement.
return features;
}
+
+ ContentResolver resolver = mContext.getContentResolver();
+ String displayFeaturesString = Settings.Global.getString(resolver, DISPLAY_FEATURES);
+ if (TextUtils.isEmpty(displayFeaturesString)) {
+ displayFeaturesString = mContext.getResources().getString(
+ R.string.config_display_features);
+ }
if (TextUtils.isEmpty(displayFeaturesString)) {
return features;
}
@@ -192,7 +199,7 @@ class SettingsSidecarImpl extends StubSidecar {
Rect featureRect = new Rect(left, top, right, bottom);
rotateRectToDisplayRotation(featureRect, displayId);
transformToWindowSpaceRect(featureRect, windowToken);
- if (!featureRect.isEmpty()) {
+ if (isNotZero(featureRect)) {
SidecarDisplayFeature feature = new SidecarDisplayFeature();
feature.setRect(featureRect);
feature.setType(type);
@@ -207,6 +214,10 @@ class SettingsSidecarImpl extends StubSidecar {
return features;
}
+ private static boolean isNotZero(Rect rect) {
+ return rect.height() > 0 || rect.width() > 0;
+ }
+
@Override
protected void onListenersChanged() {
if (mSettingsObserver == null) {
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index b8934dc8c583..0defbd6451fe 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -12,18 +12,119 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+// Begin ProtoLog
+java_library {
+ name: "wm_shell_protolog-groups",
+ srcs: [
+ "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java",
+ ":protolog-common-src",
+ ],
+}
+
+filegroup {
+ name: "wm_shell-sources",
+ srcs: [
+ "src/**/*.java",
+ ],
+ path: "src",
+}
+
+// TODO(b/168581922) protologtool do not support kotlin(*.kt)
+filegroup {
+ name: "wm_shell-sources-kt",
+ srcs: [
+ "src/**/*.kt",
+ ],
+ path: "src",
+}
+
+genrule {
+ name: "wm_shell_protolog_src",
+ srcs: [
+ ":wm_shell_protolog-groups",
+ ":wm_shell-sources",
+ ],
+ tools: ["protologtool"],
+ cmd: "$(location protologtool) transform-protolog-calls " +
+ "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+ "--protolog-impl-class com.android.wm.shell.protolog.ShellProtoLogImpl " +
+ "--protolog-cache-class com.android.wm.shell.protolog.ShellProtoLogCache " +
+ "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
+ "--loggroups-jar $(location :wm_shell_protolog-groups) " +
+ "--output-srcjar $(out) " +
+ "$(locations :wm_shell-sources)",
+ out: ["wm_shell_protolog.srcjar"],
+}
+
+genrule {
+ name: "generate-wm_shell_protolog.json",
+ srcs: [
+ ":wm_shell_protolog-groups",
+ ":wm_shell-sources",
+ ],
+ tools: ["protologtool"],
+ cmd: "$(location protologtool) generate-viewer-config " +
+ "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+ "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
+ "--loggroups-jar $(location :wm_shell_protolog-groups) " +
+ "--viewer-conf $(out) " +
+ "$(locations :wm_shell-sources)",
+ out: ["wm_shell_protolog.json"],
+}
+
+filegroup {
+ name: "wm_shell_protolog.json",
+ srcs: ["res/raw/wm_shell_protolog.json"],
+}
+
+genrule {
+ name: "checked-wm_shell_protolog.json",
+ srcs: [
+ ":generate-wm_shell_protolog.json",
+ ":wm_shell_protolog.json",
+ ],
+ cmd: "cp $(location :generate-wm_shell_protolog.json) $(out) && " +
+ "{ ! (diff $(out) $(location :wm_shell_protolog.json) | grep -q '^<') || " +
+ "{ echo -e '\\n\\n################################################################\\n#\\n" +
+ "# ERROR: ProtoLog viewer config is stale. To update it, run:\\n#\\n" +
+ "# cp $(location :generate-wm_shell_protolog.json) " +
+ "$(location :wm_shell_protolog.json)\\n#\\n" +
+ "################################################################\\n\\n' >&2 && false; } }",
+ out: ["wm_shell_protolog.json"],
+}
+// End ProtoLog
+
+java_library {
+ name: "WindowManager-Shell-proto",
+
+ srcs: ["proto/*.proto"],
+
+ proto: {
+ type: "nano",
+ },
+}
+
android_library {
name: "WindowManager-Shell",
srcs: [
- "src/**/*.java",
+ ":wm_shell_protolog_src",
+ // TODO(b/168581922) protologtool do not support kotlin(*.kt)
+ ":wm_shell-sources-kt",
"src/**/I*.aidl",
],
resource_dirs: [
"res",
],
+ static_libs: [
+ "androidx.dynamicanimation_dynamicanimation",
+ "kotlinx-coroutines-android",
+ "kotlinx-coroutines-core",
+ "protolog-lib",
+ "WindowManager-Shell-proto",
+ "androidx.appcompat_appcompat",
+ ],
+ kotlincflags: ["-Xjvm-default=enable"],
manifest: "AndroidManifest.xml",
- platform_apis: true,
- sdk_version: "current",
- min_sdk_version: "system_current",
-}
+ min_sdk_version: "26",
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/proto/wm_shell_trace.proto b/libs/WindowManager/Shell/proto/wm_shell_trace.proto
new file mode 100644
index 000000000000..b9e72525f32b
--- /dev/null
+++ b/libs/WindowManager/Shell/proto/wm_shell_trace.proto
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.wm.shell;
+
+option java_multiple_files = true;
+
+message WmShellTraceProto {
+
+ // Not used, just a test value
+ optional bool test_value = 1;
+}
diff --git a/libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml b/libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml
new file mode 100644
index 000000000000..01b8fdbe4437
--- /dev/null
+++ b/libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"
+ android:interpolator="@android:interpolator/linear_out_slow_in"
+ android:duration="280" />
diff --git a/libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml b/libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml
new file mode 100644
index 000000000000..6f316a75dbed
--- /dev/null
+++ b/libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:duration="160"
+ android:interpolator="@android:interpolator/fast_out_linear_in"
+ android:zAdjustment="top"/> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml
new file mode 100644
index 000000000000..29d9b257cc59
--- /dev/null
+++ b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:propertyName="alpha"
+ android:valueTo="1"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:duration="100" />
diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml
new file mode 100644
index 000000000000..70f553b89657
--- /dev/null
+++ b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:propertyName="alpha"
+ android:valueTo="0"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:duration="100" />
diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml
new file mode 100644
index 000000000000..29d9b257cc59
--- /dev/null
+++ b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:propertyName="alpha"
+ android:valueTo="1"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:duration="100" />
diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml
new file mode 100644
index 000000000000..70f553b89657
--- /dev/null
+++ b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:propertyName="alpha"
+ android:valueTo="0"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:duration="100" />
diff --git a/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png b/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png
new file mode 100644
index 000000000000..6c1f1cfdea7c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png
Binary files differ
diff --git a/libs/WindowManager/Shell/res/drawable/dismiss_circle_background.xml b/libs/WindowManager/Shell/res/drawable/dismiss_circle_background.xml
new file mode 100644
index 000000000000..7809c8398c2d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/dismiss_circle_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <stroke
+ android:width="1dp"
+ android:color="#AAFFFFFF" />
+
+ <solid android:color="#77000000" />
+
+</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient.xml b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient.xml
new file mode 100644
index 000000000000..8b3057d5841e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:angle="270"
+ android:startColor="#00000000"
+ android:endColor="#77000000"
+ android:type="linear" />
+</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient_transition.xml b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient_transition.xml
new file mode 100644
index 000000000000..772d0a5ea89b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient_transition.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<transition xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/transparent" />
+ <item android:drawable="@drawable/floating_dismiss_gradient" />
+</transition> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/pip_expand.xml b/libs/WindowManager/Shell/res/drawable/pip_expand.xml
new file mode 100644
index 000000000000..c99d81934aab
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_expand.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="36dp"
+ android:viewportWidth="36"
+ android:viewportHeight="36">
+
+ <path
+ android:pathData="M0 0h36v36H0z" />
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M10 21H7v8h8v-3h-5v-5zm-3-6h3v-5h5V7H7v8zm19 11h-5v3h8v-8h-3v5zM21
+7v3h5v5h3V7h-8z" />
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_close_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_close_white.xml
new file mode 100644
index 000000000000..bcc850a854de
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_ic_close_white.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+ android:fillColor="#FFFFFFFF"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_fullscreen_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_fullscreen_white.xml
new file mode 100644
index 000000000000..56699dc04e10
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_ic_fullscreen_white.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_pause_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_pause_white.xml
new file mode 100644
index 000000000000..ef9b2d9c1c63
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_ic_pause_white.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_play_arrow_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_play_arrow_white.xml
new file mode 100644
index 000000000000..f12d2cbebc87
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_ic_play_arrow_white.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M8 5v14l11-7z" />
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_settings.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_settings.xml
new file mode 100644
index 000000000000..b61e98ce2f9f
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_ic_settings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M13.85,22.25h-3.7c-0.74,0 -1.36,-0.54 -1.45,-1.27l-0.27,-1.89c-0.27,-0.14 -0.53,-0.29 -0.79,-0.46l-1.8,0.72c-0.7,0.26 -1.47,-0.03 -1.81,-0.65L2.2,15.53c-0.35,-0.66 -0.2,-1.44 0.36,-1.88l1.53,-1.19c-0.01,-0.15 -0.02,-0.3 -0.02,-0.46c0,-0.15 0.01,-0.31 0.02,-0.46l-1.52,-1.19C1.98,9.9 1.83,9.09 2.2,8.47l1.85,-3.19c0.34,-0.62 1.11,-0.9 1.79,-0.63l1.81,0.73c0.26,-0.17 0.52,-0.32 0.78,-0.46l0.27,-1.91c0.09,-0.7 0.71,-1.25 1.44,-1.25h3.7c0.74,0 1.36,0.54 1.45,1.27l0.27,1.89c0.27,0.14 0.53,0.29 0.79,0.46l1.8,-0.72c0.71,-0.26 1.48,0.03 1.82,0.65l1.84,3.18c0.36,0.66 0.2,1.44 -0.36,1.88l-1.52,1.19c0.01,0.15 0.02,0.3 0.02,0.46s-0.01,0.31 -0.02,0.46l1.52,1.19c0.56,0.45 0.72,1.23 0.37,1.86l-1.86,3.22c-0.34,0.62 -1.11,0.9 -1.8,0.63l-1.8,-0.72c-0.26,0.17 -0.52,0.32 -0.78,0.46l-0.27,1.91C15.21,21.71 14.59,22.25 13.85,22.25zM13.32,20.72c0,0.01 0,0.01 0,0.02L13.32,20.72zM10.68,20.7l0,0.02C10.69,20.72 10.69,20.71 10.68,20.7zM10.62,20.25h2.76l0.37,-2.55l0.53,-0.22c0.44,-0.18 0.88,-0.44 1.34,-0.78l0.45,-0.34l2.38,0.96l1.38,-2.4l-2.03,-1.58l0.07,-0.56c0.03,-0.26 0.06,-0.51 0.06,-0.78c0,-0.27 -0.03,-0.53 -0.06,-0.78l-0.07,-0.56l2.03,-1.58l-1.39,-2.4l-2.39,0.96l-0.45,-0.35c-0.42,-0.32 -0.87,-0.58 -1.33,-0.77L13.75,6.3l-0.37,-2.55h-2.76L10.25,6.3L9.72,6.51C9.28,6.7 8.84,6.95 8.38,7.3L7.93,7.63L5.55,6.68L4.16,9.07l2.03,1.58l-0.07,0.56C6.09,11.47 6.06,11.74 6.06,12c0,0.26 0.02,0.53 0.06,0.78l0.07,0.56l-2.03,1.58l1.38,2.4l2.39,-0.96l0.45,0.35c0.43,0.33 0.86,0.58 1.33,0.77l0.53,0.22L10.62,20.25zM18.22,17.72c0,0.01 -0.01,0.02 -0.01,0.03L18.22,17.72zM5.77,17.71l0.01,0.02C5.78,17.72 5.77,17.71 5.77,17.71zM3.93,9.47L3.93,9.47C3.93,9.47 3.93,9.47 3.93,9.47zM18.22,6.27c0,0.01 0.01,0.02 0.01,0.02L18.22,6.27zM5.79,6.25L5.78,6.27C5.78,6.27 5.79,6.26 5.79,6.25zM13.31,3.28c0,0.01 0,0.01 0,0.02L13.31,3.28zM10.69,3.26l0,0.02C10.69,3.27 10.69,3.27 10.69,3.26z"/>
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M12,12m-3.5,0a3.5,3.5 0,1 1,7 0a3.5,3.5 0,1 1,-7 0"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_skip_next_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_next_white.xml
new file mode 100644
index 000000000000..040c7e642241
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_next_white.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
+ <path
+ android:pathData="M0 0h24v24H0z" />
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_skip_previous_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_previous_white.xml
new file mode 100644
index 000000000000..b9b94b73a00f
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_previous_white.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
+ <path
+ android:pathData="M0 0h24v24H0z" />
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/pip_icon.xml b/libs/WindowManager/Shell/res/drawable/pip_icon.xml
new file mode 100644
index 000000000000..b19d907d1ff3
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_icon.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="36dp"
+ android:viewportWidth="25"
+ android:viewportHeight="25">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M19,7h-8v6h8L19,7zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,1.98 2,1.98h18c1.1,0 2,-0.88 2,-1.98L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.98h18v14.03z"/>
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/pip_resize_handle.xml b/libs/WindowManager/Shell/res/drawable/pip_resize_handle.xml
new file mode 100644
index 000000000000..4d1e080cf466
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/pip_resize_handle.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="12.0dp"
+ android:height="12.0dp"
+ android:viewportWidth="12"
+ android:viewportHeight="12">
+ <group
+ android:translateX="12"
+ android:rotation="90">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M3.41421 0L2 1.41422L10.4853 9.8995L11.8995 8.48528L3.41421 0ZM2.41421 4.24268L1 5.65689L6.65685 11.3137L8.07107 9.89953L2.41421 4.24268Z" />
+ </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml
new file mode 100644
index 000000000000..cce13035dba7
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#9AFFFFFF" android:radius="17dp" />
diff --git a/libs/WindowManager/Shell/res/layout/divider.xml b/libs/WindowManager/Shell/res/layout/divider.xml
new file mode 100644
index 000000000000..f1f0df054240
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/divider.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="72dp"
+ android:layout_height="1dp"
+ android:layout_marginTop="8dp"
+ android:background="?android:attr/colorForeground"
+ android:alpha="?android:attr/disabledAlpha" />
diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
new file mode 100644
index 000000000000..ad870252d819
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.wm.shell.splitscreen.DividerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <View
+ style="@style/DockedDividerBackground"
+ android:id="@+id/docked_divider_background"
+ android:background="@color/docked_divider_background"/>
+
+ <com.android.wm.shell.splitscreen.MinimizedDockShadow
+ style="@style/DockedDividerMinimizedShadow"
+ android:id="@+id/minimized_dock_shadow"
+ android:alpha="0"/>">
+
+ <com.android.wm.shell.splitscreen.DividerHandleView
+ style="@style/DockedDividerHandle"
+ android:id="@+id/docked_divider_handle"
+ android:contentDescription="@string/accessibility_divider"
+ android:background="@null"/>
+
+</com.android.wm.shell.splitscreen.DividerView>
diff --git a/libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml b/libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml
new file mode 100644
index 000000000000..3c778c431a2e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include
+ layout="@*android:layout/transient_notification"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"/>
+</FrameLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml b/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml
new file mode 100644
index 000000000000..dc54caf0f14a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ 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
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/one_handed_tutorial_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center_horizontal | center_vertical"
+ android:background="@android:color/transparent">
+
+ <ImageView
+ android:id="@+id/one_handed_tutorial_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="0dp"
+ android:gravity="center_horizontal"
+ android:src="@drawable/one_handed_tutorial"
+ android:scaleType="centerInside" />
+
+ <TextView
+ android:id="@+id/one_handed_tutorial_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="0dp"
+ android:gravity="center_horizontal"
+ android:textAlignment="center"
+ android:fontFamily="google-sans-medium"
+ android:text="@string/one_handed_tutorial_title"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:textColor="@android:color/white"/>
+
+ <TextView
+ android:id="@+id/one_handed_tutorial_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="0dp"
+ android:layout_marginStart="86dp"
+ android:layout_marginEnd="86dp"
+ android:gravity="center_horizontal"
+ android:fontFamily="roboto-regular"
+ android:text="@string/one_handed_tutorial_description"
+ android:textAlignment="center"
+ android:textSize="14sp"
+ android:textStyle="normal"
+ android:alpha="0.7"
+ android:textColor="@android:color/white"/>
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/pip_menu.xml b/libs/WindowManager/Shell/res/layout/pip_menu.xml
new file mode 100644
index 000000000000..2e0a5e09e34f
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/pip_menu.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- Menu layout -->
+ <FrameLayout
+ android:id="@+id/menu_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:forceHasOverlappingRendering="false"
+ android:accessibilityTraversalAfter="@id/dismiss">
+
+ <!-- The margins for this container is calculated in the code depending on whether the
+ actions_container is visible. -->
+ <FrameLayout
+ android:id="@+id/expand_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ImageButton
+ android:id="@+id/expand_button"
+ android:layout_width="60dp"
+ android:layout_height="60dp"
+ android:layout_gravity="center"
+ android:contentDescription="@string/pip_phone_expand"
+ android:padding="10dp"
+ android:src="@drawable/pip_expand"
+ android:background="?android:selectableItemBackgroundBorderless" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/actions_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/pip_action_size"
+ android:layout_gravity="bottom"
+ android:visibility="invisible">
+ <LinearLayout
+ android:id="@+id/actions_group"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:divider="@android:color/transparent"
+ android:showDividers="middle" />
+ </FrameLayout>
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/top_end_container"
+ android:layout_gravity="top|end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <ImageButton
+ android:id="@+id/settings"
+ android:layout_width="@dimen/pip_action_size"
+ android:layout_height="@dimen/pip_action_size"
+ android:padding="@dimen/pip_action_padding"
+ android:contentDescription="@string/pip_phone_settings"
+ android:src="@drawable/pip_ic_settings"
+ android:background="?android:selectableItemBackgroundBorderless" />
+
+ <ImageButton
+ android:id="@+id/dismiss"
+ android:layout_width="@dimen/pip_action_size"
+ android:layout_height="@dimen/pip_action_size"
+ android:padding="@dimen/pip_action_padding"
+ android:contentDescription="@string/pip_phone_close"
+ android:src="@drawable/pip_ic_close_white"
+ android:background="?android:selectableItemBackgroundBorderless" />
+ </LinearLayout>
+
+ <!--TODO (b/156917828): Add content description for a11y purposes?-->
+ <ImageButton
+ android:id="@+id/resize_handle"
+ android:layout_width="@dimen/pip_resize_handle_size"
+ android:layout_height="@dimen/pip_resize_handle_size"
+ android:layout_gravity="top|start"
+ android:layout_margin="@dimen/pip_resize_handle_margin"
+ android:padding="@dimen/pip_resize_handle_padding"
+ android:src="@drawable/pip_resize_handle"
+ android:background="?android:selectableItemBackgroundBorderless" />
+</FrameLayout>
diff --git a/libs/WindowManager/Shell/res/layout/pip_menu_action.xml b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml
new file mode 100644
index 000000000000..7a026ca63f50
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<ImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/pip_action_size"
+ android:layout_height="@dimen/pip_action_size"
+ android:padding="@dimen/pip_action_padding"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:forceHasOverlappingRendering="false" />
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml
new file mode 100644
index 000000000000..727ac3412a25
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<!-- Layout for {@link com.android.wm.shell.pip.tv.PipControlButtonView}. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <ImageView android:id="@+id/button"
+ android:layout_width="34dp"
+ android:layout_height="34dp"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:focusable="true"
+ android:src="@drawable/tv_pip_button_focused"
+ android:importantForAccessibility="yes" />
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="34dp"
+ android:layout_height="34dp"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:padding="5dp"
+ android:importantForAccessibility="no" />
+
+ <TextView android:id="@+id/desc"
+ android:layout_width="100dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/icon"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="3dp"
+ android:gravity="center"
+ android:text="@string/pip_fullscreen"
+ android:alpha="0"
+ android:fontFamily="sans-serif"
+ android:textSize="12sp"
+ android:textColor="#EEEEEE"
+ android:importantForAccessibility="no" />
+</merge>
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml b/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml
new file mode 100644
index 000000000000..d2f235e273d5
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<!-- Layout for {@link com.android.wm.shell.pip.tv.PipControlsView}. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <com.android.wm.shell.pip.tv.PipControlButtonView
+ android:id="@+id/full_button"
+ android:layout_width="@dimen/picture_in_picture_button_width"
+ android:layout_height="wrap_content"
+ android:src="@drawable/pip_ic_fullscreen_white"
+ android:text="@string/pip_fullscreen" />
+
+ <com.android.wm.shell.pip.tv.PipControlButtonView
+ android:id="@+id/close_button"
+ android:layout_width="@dimen/picture_in_picture_button_width"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/picture_in_picture_button_start_margin"
+ android:src="@drawable/pip_ic_close_white"
+ android:text="@string/pip_close" />
+
+ <com.android.wm.shell.pip.tv.PipControlButtonView
+ android:id="@+id/play_pause_button"
+ android:layout_width="@dimen/picture_in_picture_button_width"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/picture_in_picture_button_start_margin"
+ android:src="@drawable/pip_ic_pause_white"
+ android:text="@string/pip_pause"
+ android:visibility="gone" />
+</merge>
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_custom_control.xml b/libs/WindowManager/Shell/res/layout/tv_pip_custom_control.xml
new file mode 100644
index 000000000000..452f2cd5ccb6
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_custom_control.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<com.android.wm.shell.pip.tv.PipControlButtonView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/picture_in_picture_button_width"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/picture_in_picture_button_start_margin" />
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
new file mode 100644
index 000000000000..d8474b865a36
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:paddingTop="350dp"
+ android:background="#CC000000"
+ android:gravity="top|center_horizontal"
+ android:clipChildren="false">
+
+ <com.android.wm.shell.pip.tv.PipControlsView
+ android:id="@+id/pip_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:alpha="0" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json
new file mode 100644
index 000000000000..3f6ca0fc5246
--- /dev/null
+++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json
@@ -0,0 +1,97 @@
+{
+ "version": "1.0.0",
+ "messages": {
+ "-1683614271": {
+ "message": "Existing task: id=%d component=%s",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "-1534364071": {
+ "message": "onTransitionReady %s: %s",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TRANSITIONS",
+ "at": "com\/android\/wm\/shell\/Transitions.java"
+ },
+ "-1501874464": {
+ "message": "Fullscreen Task Appeared: #%d",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/FullscreenTaskListener.java"
+ },
+ "-1480787369": {
+ "message": "Transition requested: type=%d %s",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TRANSITIONS",
+ "at": "com\/android\/wm\/shell\/Transitions.java"
+ },
+ "-1340279385": {
+ "message": "Remove listener=%s",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "-880817403": {
+ "message": "Task vanished taskId=%d",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "-460572385": {
+ "message": "Task appeared taskId=%d",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "-191422040": {
+ "message": "Transition animations finished, notifying core %s",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TRANSITIONS",
+ "at": "com\/android\/wm\/shell\/Transitions.java"
+ },
+ "157713005": {
+ "message": "Task info changed taskId=%d",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "481673835": {
+ "message": "addListenerForTaskId taskId=%s",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "564235578": {
+ "message": "Fullscreen Task Vanished: #%d",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/FullscreenTaskListener.java"
+ },
+ "580605218": {
+ "message": "Registering organizer",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "980952660": {
+ "message": "Task root back pressed taskId=%d",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ },
+ "1990759023": {
+ "message": "addListenerForType types=%s listener=%s",
+ "level": "VERBOSE",
+ "group": "WM_SHELL_TASK_ORG",
+ "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
+ }
+ },
+ "groups": {
+ "WM_SHELL_TASK_ORG": {
+ "tag": "WindowManagerShell"
+ },
+ "WM_SHELL_TRANSITIONS": {
+ "tag": "WindowManagerShell"
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml
new file mode 100644
index 000000000000..77a601ddf440
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values-land/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+*/
+-->
+<resources>
+ <dimen name="docked_divider_handle_width">2dp</dimen>
+ <dimen name="docked_divider_handle_height">16dp</dimen>
+</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml
new file mode 100644
index 000000000000..863bb69d4034
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values-land/styles.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="DockedDividerBackground">
+ <item name="android:layout_width">10dp</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_gravity">center_horizontal</item>
+ </style>
+
+ <style name="DockedDividerHandle">
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:layout_width">48dp</item>
+ <item name="android:layout_height">96dp</item>
+ </style>
+
+ <style name="DockedDividerMinimizedShadow">
+ <item name="android:layout_width">8dp</item>
+ <item name="android:layout_height">match_parent</item>
+ </style>
+</resources>
+
diff --git a/libs/WindowManager/Shell/res/values-sw600dp/config.xml b/libs/WindowManager/Shell/res/values-sw600dp/config.xml
new file mode 100644
index 000000000000..f194532f1e0d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values-sw600dp/config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds. -->
+<resources>
+ <!-- Animation duration when using long press on recents to dock -->
+ <integer name="long_press_dock_anim_duration">290</integer>
+</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
new file mode 100644
index 000000000000..7920fd237a08
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <!-- The dimensions to user for picture-in-picture action buttons. -->
+ <dimen name="picture_in_picture_button_width">100dp</dimen>
+ <dimen name="picture_in_picture_button_start_margin">-50dp</dimen>
+</resources>
+
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
new file mode 100644
index 000000000000..6a19083e3788
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 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.
+ */
+-->
+<resources>
+ <color name="docked_divider_background">#ff000000</color>
+ <color name="docked_divider_handle">#ffffff</color>
+ <drawable name="forced_resizable_background">#59000000</drawable>
+ <color name="minimize_dock_shadow_start">#60000000</color>
+ <color name="minimize_dock_shadow_end">#00000000</color>
+</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index c894eb0133b5..e99350b264b9 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -1,21 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
-/*
-** Copyright 2019, 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.
-*/
--->
+ 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.
+-->
<resources>
-</resources> \ No newline at end of file
+ <!-- Animation duration for resizing of PIP when entering/exiting. -->
+ <integer name="config_pipResizeAnimationDuration">425</integer>
+
+ <!-- Allow dragging the PIP to a location to close it -->
+ <bool name="config_pipEnableDismissDragToEdge">true</bool>
+
+ <!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu -->
+ <bool name="config_pipEnableResizeForMenu">true</bool>
+
+ <!-- Allow PIP to enable round corner, see also R.dimen.pip_corner_radius -->
+ <bool name="config_pipEnableRoundCorner">false</bool>
+
+ <!-- Animation duration when using long press on recents to dock -->
+ <integer name="long_press_dock_anim_duration">250</integer>
+
+ <!-- Allow one handed to enable round corner -->
+ <bool name="config_one_handed_enable_round_corner">true</bool>
+
+ <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows,
+ when the PIP menu is shown in center. -->
+ <string translatable="false" name="pip_menu_bounds">"596 280 1324 690"</string>
+</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
new file mode 100644
index 000000000000..a9917a6b07da
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <dimen name="dismiss_circle_size">52dp</dimen>
+
+ <!-- The height of the gradient indicating the dismiss edge when moving a PIP. -->
+ <dimen name="floating_dismiss_gradient_height">250dp</dimen>
+
+ <!-- The padding around a PiP actions. -->
+ <dimen name="pip_action_padding">12dp</dimen>
+
+ <!-- The height of the PiP actions container in which the actions are vertically centered. -->
+ <dimen name="pip_action_size">48dp</dimen>
+
+ <!-- The padding between actions in the PiP in landscape Note that the PiP does not reflect
+ the configuration of the device, so we can't use -land resources. -->
+ <dimen name="pip_between_action_padding_land">8dp</dimen>
+
+ <!-- The buffer to use when calculating whether the pip is in an adjust zone. -->
+ <dimen name="pip_bottom_offset_buffer">1dp</dimen>
+
+ <!-- The corner radius for PiP window. -->
+ <dimen name="pip_corner_radius">8dp</dimen>
+
+ <!-- The bottom margin of the PIP drag to dismiss info text shown when moving a PIP. -->
+ <dimen name="pip_dismiss_text_bottom_margin">24dp</dimen>
+
+ <!-- The bottom margin of the expand container when there are actions.
+ Equal to pip_action_size - pip_action_padding. -->
+ <dimen name="pip_expand_container_edge_margin">30dp</dimen>
+
+ <!-- The shortest-edge size of the expanded PiP. -->
+ <dimen name="pip_expanded_shortest_edge_size">160dp</dimen>
+
+ <!-- The additional offset to apply to the IME animation to account for the input field. -->
+ <dimen name="pip_ime_offset">48dp</dimen>
+
+ <!-- The touchable/draggable edge size for PIP resize. -->
+ <dimen name="pip_resize_edge_size">48dp</dimen>
+
+ <!-- PIP Resize handle size, margin and padding. -->
+ <dimen name="pip_resize_handle_size">12dp</dimen>
+ <dimen name="pip_resize_handle_margin">4dp</dimen>
+ <dimen name="pip_resize_handle_padding">0dp</dimen>
+
+ <dimen name="dismiss_target_x_size">24dp</dimen>
+ <dimen name="floating_dismiss_bottom_margin">50dp</dimen>
+
+ <!-- How high we lift the divider when touching -->
+ <dimen name="docked_stack_divider_lift_elevation">4dp</dimen>
+
+ <dimen name="docked_divider_handle_width">16dp</dimen>
+ <dimen name="docked_divider_handle_height">2dp</dimen>
+
+ <!-- One-Handed Mode -->
+ <!-- Threshold for dragging distance to enable one-handed mode -->
+ <dimen name="gestures_onehanded_drag_threshold">20dp</dimen>
+</resources>
diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml
new file mode 100644
index 000000000000..fb892388cf74
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/ids.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <item type="id" name="action_pip_resize" />
+
+ <!-- Accessibility actions for the docked stack divider -->
+ <item type="id" name="action_move_tl_full" />
+ <item type="id" name="action_move_tl_70" />
+ <item type="id" name="action_move_tl_50" />
+ <item type="id" name="action_move_tl_30" />
+ <item type="id" name="action_move_rb_full" />
+</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
new file mode 100644
index 000000000000..da5965dab71a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Label for PIP close button [CHAR LIMIT=NONE]-->
+ <string name="pip_phone_close">Close</string>
+
+ <!-- Making the PIP fullscreen [CHAR LIMIT=25] -->
+ <string name="pip_phone_expand">Expand</string>
+
+ <!-- Label for PIP settings button [CHAR LIMIT=NONE]-->
+ <string name="pip_phone_settings">Settings</string>
+
+ <!-- Title of menu shown over picture-in-picture. Used for accessibility. -->
+ <string name="pip_menu_title">Menu</string>
+
+ <!-- PiP BTW notification title. [CHAR LIMIT=50] -->
+ <string name="pip_notification_title"><xliff:g id="name" example="Google Maps">%s</xliff:g> is in picture-in-picture</string>
+
+ <!-- PiP BTW notification description. [CHAR LIMIT=NONE] -->
+ <string name="pip_notification_message">If you don\'t want <xliff:g id="name" example="Google Maps">%s</xliff:g> to use this feature, tap to open settings and turn it off.</string>
+
+ <!-- Button to play the current media on picture-in-picture (PIP) [CHAR LIMIT=30] -->
+ <string name="pip_play">Play</string>
+
+ <!-- Button to pause the current media on picture-in-picture (PIP) [CHAR LIMIT=30] -->
+ <string name="pip_pause">Pause</string>
+
+ <!-- Button to skip to the next media on picture-in-picture (PIP) [CHAR LIMIT=30] -->
+ <string name="pip_skip_to_next">Skip to next</string>
+
+ <!-- Button to skip to the prev media on picture-in-picture (PIP) [CHAR LIMIT=30] -->
+ <string name="pip_skip_to_prev">Skip to previous</string>
+
+ <!-- Accessibility action for resizing PIP [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_pip_resize">Resize</string>
+
+ <!-- TODO Deprecated. Label for PIP action to Minimize the PIP. DO NOT TRANSLATE [CHAR LIMIT=25] -->
+ <string name="pip_phone_minimize">Minimize</string>
+
+ <!-- TODO Deprecated. Label for PIP the drag to dismiss hint. DO NOT TRANSLATE [CHAR LIMIT=NONE]-->
+ <string name="pip_phone_dismiss_hint">Drag down to dismiss</string>
+
+ <!-- Multi-Window strings -->
+ <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed in split-screen and that things might crash/not work properly [CHAR LIMIT=NONE] -->
+ <string name="dock_forced_resizable">App may not work with split-screen.</string>
+ <!-- Warning message when we try to dock a non-resizeable task and launch it in fullscreen instead. -->
+ <string name="dock_non_resizeble_failed_to_dock_text">App does not support split-screen.</string>
+ <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed on a secondary display and that things might crash/not work properly [CHAR LIMIT=NONE] -->
+ <string name="forced_resizable_secondary_display">App may not work on a secondary display.</string>
+ <!-- Warning message when we try to launch a non-resizeable activity on a secondary display and launch it on the primary instead. -->
+ <string name="activity_launch_on_secondary_display_failed_text">App does not support launch on secondary displays.</string>
+
+ <!-- Accessibility label for the divider that separates the windows in split-screen mode [CHAR LIMIT=NONE] -->
+ <string name="accessibility_divider">Split-screen divider</string>
+
+ <!-- Accessibility action for moving docked stack divider to make the left screen full screen [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_left_full">Left full screen</string>
+ <!-- Accessibility action for moving docked stack divider to make the left screen 70% [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_left_70">Left 70%</string>
+ <!-- Accessibility action for moving docked stack divider to make the left screen 50% [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_left_50">Left 50%</string>
+ <!-- Accessibility action for moving docked stack divider to make the left screen 30% [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_left_30">Left 30%</string>
+ <!-- Accessibility action for moving docked stack divider to make the right screen full screen [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_right_full">Right full screen</string>
+
+ <!-- Accessibility action for moving docked stack divider to make the top screen full screen [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_top_full">Top full screen</string>
+ <!-- Accessibility action for moving docked stack divider to make the top screen 70% [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_top_70">Top 70%</string>
+ <!-- Accessibility action for moving docked stack divider to make the top screen 50% [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_top_50">Top 50%</string>
+ <!-- Accessibility action for moving docked stack divider to make the top screen 30% [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_top_30">Top 30%</string>
+ <!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_divider_bottom_full">Bottom full screen</string>
+
+ <!-- One-Handed Tutorial title [CHAR LIMIT=60] -->
+ <string name="one_handed_tutorial_title">Using one-handed mode</string>
+ <!-- One-Handed Tutorial description [CHAR LIMIT=NONE] -->
+ <string name="one_handed_tutorial_description">To exit, swipe up from the bottom of the screen or tap anywhere above the app</string>
+ <!-- Accessibility description for start one-handed mode [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_start_one_handed">Start one-handed mode</string>
+ <!-- Accessibility description for stop one-handed mode [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_stop_one_handed">Exit one-handed mode</string>
+</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml
new file mode 100644
index 000000000000..2dfdcabaa931
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/strings_tv.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Picture-in-Picture (PIP) notification -->
+ <!-- Title for the notification channel for TV PIP controls. [CHAR LIMIT=NONE] -->
+ <string name="notification_channel_tv_pip">Picture-in-Picture</string>
+
+ <!-- Title of the picture-in-picture (PIP) notification title
+ when the media doesn't have title [CHAR LIMIT=NONE] -->
+ <string name="pip_notification_unknown_title">(No title program)</string>
+
+ <!-- Picture-in-Picture (PIP) menu -->
+ <eat-comment />
+ <!-- Button to close picture-in-picture (PIP) in PIP menu [CHAR LIMIT=30] -->
+ <string name="pip_close">Close PIP</string>
+
+ <!-- Button to move picture-in-picture (PIP) screen to the fullscreen in PIP menu [CHAR LIMIT=30] -->
+ <string name="pip_fullscreen">Full screen</string>
+</resources>
+
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
new file mode 100644
index 000000000000..fffcd33f7992
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Theme used for the activity that shows when the system forced an app to be resizable -->
+ <style name="ForcedResizableTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+ <item name="android:windowBackground">@drawable/forced_resizable_background</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:windowAnimationStyle">@style/Animation.ForcedResizable</item>
+ </style>
+
+ <style name="Animation.ForcedResizable" parent="@android:style/Animation">
+ <item name="android:activityOpenEnterAnimation">@anim/forced_resizable_enter</item>
+
+ <!-- If the target stack doesn't have focus, we do a task to front animation. -->
+ <item name="android:taskToFrontEnterAnimation">@anim/forced_resizable_enter</item>
+ <item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
+ </style>
+
+ <style name="DockedDividerBackground">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">10dp</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ </style>
+
+ <style name="DockedDividerMinimizedShadow">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">8dp</item>
+ </style>
+
+ <style name="DockedDividerHandle">
+ <item name="android:layout_gravity">center_horizontal</item>
+ <item name="android:layout_width">96dp</item>
+ <item name="android:layout_height">48dp</item>
+ </style>
+</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java
new file mode 100644
index 000000000000..5bd693a9311e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.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;
+
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
+import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
+
+import android.app.ActivityManager;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+
+class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
+ private static final String TAG = "FullscreenTaskListener";
+
+ private final SyncTransactionQueue mSyncQueue;
+
+ private final ArrayMap<Integer, SurfaceControl> mTasks = new ArrayMap<>();
+
+ FullscreenTaskListener(SyncTransactionQueue syncQueue) {
+ mSyncQueue = syncQueue;
+ }
+
+ @Override
+ public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+ synchronized (mTasks) {
+ if (mTasks.containsKey(taskInfo.taskId)) {
+ throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId);
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d",
+ taskInfo.taskId);
+ mTasks.put(taskInfo.taskId, leash);
+ mSyncQueue.runInSync(t -> {
+ // Reset several properties back to fullscreen (PiP, for example, leaves all these
+ // properties in a bad state).
+ updateSurfacePosition(t, taskInfo, leash);
+ t.setWindowCrop(leash, null);
+ // TODO(shell-transitions): Eventually set everything in transition so there's no
+ // SF Transaction here.
+ if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+ t.setAlpha(leash, 1f);
+ t.setMatrix(leash, 1, 0, 0, 1);
+ t.show(leash);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+ synchronized (mTasks) {
+ if (mTasks.remove(taskInfo.taskId) == null) {
+ Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
+ taskInfo.taskId);
+ }
+ }
+
+ @Override
+ public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ synchronized (mTasks) {
+ if (!mTasks.containsKey(taskInfo.taskId)) {
+ Slog.e(TAG, "Changed Task wasn't appeared or already vanished: #"
+ + taskInfo.taskId);
+ return;
+ }
+ final SurfaceControl leash = mTasks.get(taskInfo.taskId);
+ mSyncQueue.runInSync(t -> {
+ // Reposition the task in case the bounds has been changed (such as Task level
+ // letterboxing).
+ updateSurfacePosition(t, taskInfo, leash);
+ });
+ }
+ }
+
+ @Override
+ public void dump(@NonNull PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ final String childPrefix = innerPrefix + " ";
+ pw.println(prefix + this);
+ pw.println(innerPrefix + mTasks.size() + " Tasks");
+ }
+
+ @Override
+ public String toString() {
+ return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN);
+ }
+
+ /** Places the Task surface to the latest position. */
+ private static void updateSurfacePosition(SurfaceControl.Transaction t,
+ ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+ // TODO(170725334) drop this after ag/12876439
+ final Configuration config = taskInfo.getConfiguration();
+ final Rect bounds = config.windowConfiguration.getBounds();
+ t.setPosition(leash, bounds.left, bounds.top);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
new file mode 100644
index 000000000000..cbc1c8d6d310
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -0,0 +1,409 @@
+/*
+ * 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;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
+
+import android.annotation.IntDef;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.WindowConfiguration.WindowingMode;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+import android.window.ITaskOrganizerController;
+import android.window.TaskAppearedInfo;
+import android.window.TaskOrganizer;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unified task organizer for all components in the shell.
+ * TODO(b/167582004): may consider consolidating this class and TaskOrganizer
+ */
+public class ShellTaskOrganizer extends TaskOrganizer {
+
+ // Intentionally using negative numbers here so the positive numbers can be used
+ // for task id specific listeners that will be added later.
+ public static final int TASK_LISTENER_TYPE_UNDEFINED = -1;
+ public static final int TASK_LISTENER_TYPE_FULLSCREEN = -2;
+ public static final int TASK_LISTENER_TYPE_MULTI_WINDOW = -3;
+ public static final int TASK_LISTENER_TYPE_PIP = -4;
+ public static final int TASK_LISTENER_TYPE_SPLIT_SCREEN = -5;
+
+ @IntDef(prefix = {"TASK_LISTENER_TYPE_"}, value = {
+ TASK_LISTENER_TYPE_UNDEFINED,
+ TASK_LISTENER_TYPE_FULLSCREEN,
+ TASK_LISTENER_TYPE_MULTI_WINDOW,
+ TASK_LISTENER_TYPE_PIP,
+ TASK_LISTENER_TYPE_SPLIT_SCREEN,
+ })
+ public @interface TaskListenerType {}
+
+ private static final String TAG = "ShellTaskOrganizer";
+
+ /**
+ * Callbacks for when the tasks change in the system.
+ */
+ public interface TaskListener {
+ default void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {}
+ default void onTaskInfoChanged(RunningTaskInfo taskInfo) {}
+ default void onTaskVanished(RunningTaskInfo taskInfo) {}
+ default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {}
+ default void dump(@NonNull PrintWriter pw, String prefix) {};
+ }
+
+ /**
+ * Keys map from either a task id or {@link TaskListenerType}.
+ * @see #addListenerForTaskId
+ * @see #addListenerForType
+ */
+ private final SparseArray<TaskListener> mTaskListeners = new SparseArray<>();
+
+ // Keeps track of all the tasks reported to this organizer (changes in windowing mode will
+ // require us to report to both old and new listeners)
+ private final SparseArray<TaskAppearedInfo> mTasks = new SparseArray<>();
+
+ /** @see #setPendingLaunchCookieListener */
+ private final ArrayMap<IBinder, TaskListener> mLaunchCookieToListener = new ArrayMap<>();
+
+ // TODO(shell-transitions): move to a more "global" Shell location as this isn't only for Tasks
+ private final Transitions mTransitions;
+
+ private final Object mLock = new Object();
+
+ public ShellTaskOrganizer(SyncTransactionQueue syncQueue, TransactionPool transactionPool,
+ ShellExecutor mainExecutor, ShellExecutor animExecutor) {
+ this(null, syncQueue, transactionPool, mainExecutor, animExecutor);
+ }
+
+ @VisibleForTesting
+ ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController,
+ SyncTransactionQueue syncQueue, TransactionPool transactionPool,
+ ShellExecutor mainExecutor, ShellExecutor animExecutor) {
+ super(taskOrganizerController, mainExecutor);
+ addListenerForType(new FullscreenTaskListener(syncQueue), TASK_LISTENER_TYPE_FULLSCREEN);
+ mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions);
+ }
+
+ @Override
+ public List<TaskAppearedInfo> registerOrganizer() {
+ synchronized (mLock) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "Registering organizer");
+ final List<TaskAppearedInfo> taskInfos = super.registerOrganizer();
+ for (int i = 0; i < taskInfos.size(); i++) {
+ final TaskAppearedInfo info = taskInfos.get(i);
+ ProtoLog.v(WM_SHELL_TASK_ORG, "Existing task: id=%d component=%s",
+ info.getTaskInfo().taskId, info.getTaskInfo().baseIntent);
+ onTaskAppeared(info);
+ }
+ return taskInfos;
+ }
+ }
+
+ /**
+ * Adds a listener for a specific task id.
+ */
+ public void addListenerForTaskId(TaskListener listener, int taskId) {
+ synchronized (mLock) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "addListenerForTaskId taskId=%s", taskId);
+ if (mTaskListeners.get(taskId) != null) {
+ throw new IllegalArgumentException(
+ "Listener for taskId=" + taskId + " already exists");
+ }
+
+ final TaskAppearedInfo info = mTasks.get(taskId);
+ if (info == null) {
+ throw new IllegalArgumentException("addListenerForTaskId unknown taskId=" + taskId);
+ }
+
+ final TaskListener oldListener = getTaskListener(info.getTaskInfo());
+ mTaskListeners.put(taskId, listener);
+ updateTaskListenerIfNeeded(info.getTaskInfo(), info.getLeash(), oldListener, listener);
+ }
+ }
+
+ /**
+ * Adds a listener for tasks with given types.
+ */
+ public void addListenerForType(TaskListener listener, @TaskListenerType int... listenerTypes) {
+ synchronized (mLock) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "addListenerForType types=%s listener=%s",
+ Arrays.toString(listenerTypes), listener);
+ for (int listenerType : listenerTypes) {
+ if (mTaskListeners.get(listenerType) != null) {
+ throw new IllegalArgumentException("Listener for listenerType=" + listenerType
+ + " already exists");
+ }
+ mTaskListeners.put(listenerType, listener);
+
+ // Notify the listener of all existing tasks with the given type.
+ for (int i = mTasks.size() - 1; i >= 0; --i) {
+ final TaskAppearedInfo data = mTasks.valueAt(i);
+ final TaskListener taskListener = getTaskListener(data.getTaskInfo());
+ if (taskListener != listener) continue;
+ listener.onTaskAppeared(data.getTaskInfo(), data.getLeash());
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a registered listener.
+ */
+ public void removeListener(TaskListener listener) {
+ synchronized (mLock) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "Remove listener=%s", listener);
+ final int index = mTaskListeners.indexOfValue(listener);
+ if (index == -1) {
+ Log.w(TAG, "No registered listener found");
+ return;
+ }
+
+ // Collect tasks associated with the listener we are about to remove.
+ final ArrayList<TaskAppearedInfo> tasks = new ArrayList<>();
+ for (int i = mTasks.size() - 1; i >= 0; --i) {
+ final TaskAppearedInfo data = mTasks.valueAt(i);
+ final TaskListener taskListener = getTaskListener(data.getTaskInfo());
+ if (taskListener != listener) continue;
+ tasks.add(data);
+ }
+
+ // Remove listener
+ mTaskListeners.removeAt(index);
+
+ // Associate tasks with new listeners if needed.
+ for (int i = tasks.size() - 1; i >= 0; --i) {
+ final TaskAppearedInfo data = tasks.get(i);
+ updateTaskListenerIfNeeded(data.getTaskInfo(), data.getLeash(),
+ null /* oldListener already removed*/, getTaskListener(data.getTaskInfo()));
+ }
+ }
+ }
+
+ /**
+ * Associated a listener to a pending launch cookie so we can route the task later once it
+ * appears.
+ */
+ public void setPendingLaunchCookieListener(IBinder cookie, TaskListener listener) {
+ synchronized (mLock) {
+ mLaunchCookieToListener.put(cookie, listener);
+ }
+ }
+
+ @Override
+ public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+ synchronized (mLock) {
+ onTaskAppeared(new TaskAppearedInfo(taskInfo, leash));
+ }
+ }
+
+ private void onTaskAppeared(TaskAppearedInfo info) {
+ final int taskId = info.getTaskInfo().taskId;
+ ProtoLog.v(WM_SHELL_TASK_ORG, "Task appeared taskId=%d", taskId);
+ mTasks.put(taskId, info);
+ final TaskListener listener =
+ getTaskListener(info.getTaskInfo(), true /*removeLaunchCookieIfNeeded*/);
+ if (listener != null) {
+ listener.onTaskAppeared(info.getTaskInfo(), info.getLeash());
+ }
+ }
+
+ @Override
+ public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+ synchronized (mLock) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "Task info changed taskId=%d", taskInfo.taskId);
+ final TaskAppearedInfo data = mTasks.get(taskInfo.taskId);
+ final TaskListener oldListener = getTaskListener(data.getTaskInfo());
+ final TaskListener newListener = getTaskListener(taskInfo);
+ mTasks.put(taskInfo.taskId, new TaskAppearedInfo(taskInfo, data.getLeash()));
+ final boolean updated = updateTaskListenerIfNeeded(
+ taskInfo, data.getLeash(), oldListener, newListener);
+ if (!updated && newListener != null) {
+ newListener.onTaskInfoChanged(taskInfo);
+ }
+ }
+ }
+
+ @Override
+ public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {
+ synchronized (mLock) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "Task root back pressed taskId=%d", taskInfo.taskId);
+ final TaskListener listener = getTaskListener(taskInfo);
+ if (listener != null) {
+ listener.onBackPressedOnTaskRoot(taskInfo);
+ }
+ }
+ }
+
+ @Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ synchronized (mLock) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "Task vanished taskId=%d", taskInfo.taskId);
+ final int taskId = taskInfo.taskId;
+ final TaskListener listener = getTaskListener(mTasks.get(taskId).getTaskInfo());
+ mTasks.remove(taskId);
+ if (listener != null) {
+ listener.onTaskVanished(taskInfo);
+ }
+ }
+ }
+
+ private boolean updateTaskListenerIfNeeded(RunningTaskInfo taskInfo, SurfaceControl leash,
+ TaskListener oldListener, TaskListener newListener) {
+ if (oldListener == newListener) return false;
+ // TODO: We currently send vanished/appeared as the task moves between types, but
+ // we should consider adding a different mode-changed callback
+ if (oldListener != null) {
+ oldListener.onTaskVanished(taskInfo);
+ }
+ if (newListener != null) {
+ newListener.onTaskAppeared(taskInfo, leash);
+ }
+ return true;
+ }
+
+ private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo) {
+ return getTaskListener(runningTaskInfo, false /*removeLaunchCookieIfNeeded*/);
+ }
+
+ private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo,
+ boolean removeLaunchCookieIfNeeded) {
+
+ final int taskId = runningTaskInfo.taskId;
+ TaskListener listener;
+
+ // First priority goes to listener that might be pending for this task.
+ final ArrayList<IBinder> launchCookies = runningTaskInfo.launchCookies;
+ for (int i = launchCookies.size() - 1; i >= 0; --i) {
+ final IBinder cookie = launchCookies.get(i);
+ listener = mLaunchCookieToListener.get(cookie);
+ if (listener == null) continue;
+
+ if (removeLaunchCookieIfNeeded) {
+ // Remove the cookie and add the listener.
+ mLaunchCookieToListener.remove(cookie);
+ mTaskListeners.put(taskId, listener);
+ }
+ return listener;
+ }
+
+ // Next priority goes to taskId specific listeners.
+ listener = mTaskListeners.get(taskId);
+ if (listener != null) return listener;
+
+ // Next we try type specific listeners.
+ final int windowingMode = getWindowingMode(runningTaskInfo);
+ final int taskListenerType = windowingModeToTaskListenerType(windowingMode);
+ return mTaskListeners.get(taskListenerType);
+ }
+
+ @WindowingMode
+ private static int getWindowingMode(RunningTaskInfo taskInfo) {
+ return taskInfo.configuration.windowConfiguration.getWindowingMode();
+ }
+
+ private static @TaskListenerType int windowingModeToTaskListenerType(
+ @WindowingMode int windowingMode) {
+ switch (windowingMode) {
+ case WINDOWING_MODE_FULLSCREEN:
+ return TASK_LISTENER_TYPE_FULLSCREEN;
+ case WINDOWING_MODE_MULTI_WINDOW:
+ return TASK_LISTENER_TYPE_MULTI_WINDOW;
+ case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY:
+ case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY:
+ return TASK_LISTENER_TYPE_SPLIT_SCREEN;
+ case WINDOWING_MODE_PINNED:
+ return TASK_LISTENER_TYPE_PIP;
+ case WINDOWING_MODE_FREEFORM:
+ case WINDOWING_MODE_UNDEFINED:
+ default:
+ return TASK_LISTENER_TYPE_UNDEFINED;
+ }
+ }
+
+ public static String taskListenerTypeToString(@TaskListenerType int type) {
+ switch (type) {
+ case TASK_LISTENER_TYPE_FULLSCREEN:
+ return "TASK_LISTENER_TYPE_FULLSCREEN";
+ case TASK_LISTENER_TYPE_MULTI_WINDOW:
+ return "TASK_LISTENER_TYPE_MULTI_WINDOW";
+ case TASK_LISTENER_TYPE_SPLIT_SCREEN:
+ return "TASK_LISTENER_TYPE_SPLIT_SCREEN";
+ case TASK_LISTENER_TYPE_PIP:
+ return "TASK_LISTENER_TYPE_PIP";
+ case TASK_LISTENER_TYPE_UNDEFINED:
+ return "TASK_LISTENER_TYPE_UNDEFINED";
+ default:
+ return "taskId#" + type;
+ }
+ }
+
+ public void dump(@NonNull PrintWriter pw, String prefix) {
+ synchronized (mLock) {
+ final String innerPrefix = prefix + " ";
+ final String childPrefix = innerPrefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + mTaskListeners.size() + " Listeners");
+ for (int i = mTaskListeners.size() - 1; i >= 0; --i) {
+ final int key = mTaskListeners.keyAt(i);
+ final TaskListener listener = mTaskListeners.valueAt(i);
+ pw.println(innerPrefix + "#" + i + " " + taskListenerTypeToString(key));
+ listener.dump(pw, childPrefix);
+ }
+
+ pw.println();
+ pw.println(innerPrefix + mTasks.size() + " Tasks");
+ for (int i = mTasks.size() - 1; i >= 0; --i) {
+ final int key = mTasks.keyAt(i);
+ final TaskAppearedInfo info = mTasks.valueAt(i);
+ final TaskListener listener = getTaskListener(info.getTaskInfo());
+ pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener);
+ }
+
+ pw.println();
+ pw.println(innerPrefix + mLaunchCookieToListener.size() + " Launch Cookies");
+ for (int i = mLaunchCookieToListener.size() - 1; i >= 0; --i) {
+ final IBinder key = mLaunchCookieToListener.keyAt(i);
+ final TaskListener listener = mLaunchCookieToListener.valueAt(i);
+ pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener);
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java
new file mode 100644
index 000000000000..04be3b70fc65
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java
@@ -0,0 +1,181 @@
+/*
+ * 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;
+
+import static android.window.TransitionInfo.TRANSIT_CLOSE;
+import static android.window.TransitionInfo.TRANSIT_HIDE;
+import static android.window.TransitionInfo.TRANSIT_OPEN;
+import static android.window.TransitionInfo.TRANSIT_SHOW;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.os.SystemProperties;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.ITransitionPlayer;
+import android.window.TransitionInfo;
+import android.window.WindowOrganizer;
+
+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.protolog.ShellProtoLogGroup;
+
+import java.util.ArrayList;
+
+/** Plays transition animations */
+public class Transitions extends ITransitionPlayer.Stub {
+ private static final String TAG = "ShellTransitions";
+
+ /** Set to {@code true} to enable shell transitions. */
+ public static final boolean ENABLE_SHELL_TRANSITIONS =
+ SystemProperties.getBoolean("persist.debug.shell_transit", false);
+
+ private final WindowOrganizer mOrganizer;
+ private final TransactionPool mTransactionPool;
+ private final ShellExecutor mMainExecutor;
+ private final ShellExecutor mAnimExecutor;
+
+ /** Keeps track of currently tracked transitions and all the animations associated with each */
+ private final ArrayMap<IBinder, ArrayList<Animator>> mActiveTransitions = new ArrayMap<>();
+
+ Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool,
+ @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
+ mOrganizer = organizer;
+ mTransactionPool = pool;
+ mMainExecutor = mainExecutor;
+ mAnimExecutor = animExecutor;
+ }
+
+ // TODO(shell-transitions): real animations
+ private void startExampleAnimation(@NonNull IBinder transition, @NonNull SurfaceControl leash,
+ boolean show) {
+ final float end = show ? 1.f : 0.f;
+ final float start = 1.f - end;
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ final ValueAnimator va = ValueAnimator.ofFloat(start, end);
+ va.setDuration(500);
+ va.addUpdateListener(animation -> {
+ float fraction = animation.getAnimatedFraction();
+ transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
+ transaction.apply();
+ });
+ final Runnable finisher = () -> {
+ transaction.setAlpha(leash, end);
+ transaction.apply();
+ mTransactionPool.release(transaction);
+ mMainExecutor.execute(() -> {
+ mActiveTransitions.get(transition).remove(va);
+ onFinish(transition);
+ });
+ };
+ va.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) { }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finisher.run();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ finisher.run();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) { }
+ });
+ mActiveTransitions.get(transition).add(va);
+ mAnimExecutor.execute(va::start);
+ }
+
+ private static boolean isOpeningType(@WindowManager.TransitionOldType int legacyType) {
+ // TODO(shell-transitions): consider providing and using z-order vs the global type for
+ // this determination.
+ return legacyType == WindowManager.TRANSIT_OLD_TASK_OPEN
+ || legacyType == WindowManager.TRANSIT_OLD_TASK_TO_FRONT
+ || legacyType == WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND
+ || legacyType == WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
+ }
+
+ @Override
+ public void onTransitionReady(@NonNull IBinder transitionToken, TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s",
+ transitionToken, info);
+ // start task
+ mMainExecutor.execute(() -> {
+ if (!mActiveTransitions.containsKey(transitionToken)) {
+ Slog.e(TAG, "Got transitionReady for non-active transition " + transitionToken
+ + " expecting one of " + mActiveTransitions.keySet());
+ }
+ if (mActiveTransitions.get(transitionToken) != null) {
+ throw new IllegalStateException("Got a duplicate onTransitionReady call for "
+ + transitionToken);
+ }
+ mActiveTransitions.put(transitionToken, new ArrayList<>());
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final SurfaceControl leash = info.getChanges().get(i).getLeash();
+ final int mode = info.getChanges().get(i).getMode();
+ if (mode == TRANSIT_OPEN || mode == TRANSIT_SHOW) {
+ t.show(leash);
+ t.setMatrix(leash, 1, 0, 0, 1);
+ if (isOpeningType(info.getType())) {
+ t.setAlpha(leash, 0.f);
+ startExampleAnimation(transitionToken, leash, true /* show */);
+ } else {
+ t.setAlpha(leash, 1.f);
+ }
+ } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_HIDE) {
+ if (!isOpeningType(info.getType())) {
+ startExampleAnimation(transitionToken, leash, false /* show */);
+ }
+ }
+ }
+ t.apply();
+ onFinish(transitionToken);
+ });
+ }
+
+ @MainThread
+ private void onFinish(IBinder transition) {
+ if (!mActiveTransitions.get(transition).isEmpty()) return;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Transition animations finished, notifying core %s", transition);
+ mActiveTransitions.remove(transition);
+ mOrganizer.finishTransition(transition, null, null);
+ }
+
+ @Override
+ public 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);
+ });
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java
new file mode 100644
index 000000000000..acb9a5dae78c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.app.WindowConfiguration;
+import android.os.RemoteException;
+import android.view.WindowManagerGlobal;
+
+import com.android.wm.shell.pip.PinnedStackListenerForwarder;
+
+/**
+ * The singleton wrapper to communicate between WindowManagerService and WMShell features
+ * (e.g: PIP, SplitScreen, Bubble, OneHandedMode...etc)
+ */
+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();
+
+ /**
+ * 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 {
+ mPinnedStackListenerForwarder.addListener(listener);
+ WindowManagerGlobal.getWindowManagerService().registerPinnedStackListener(
+ DEFAULT_DISPLAY, mPinnedStackListenerForwarder);
+ }
+
+ /**
+ * Removes a pinned stack listener.
+ */
+ public void removePinnedStackListener(
+ PinnedStackListenerForwarder.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
new file mode 100644
index 000000000000..357f777e1270
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2014 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.animation;
+
+import android.animation.Animator;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Utility class to calculate general fling animation when the finger is released.
+ */
+public class FlingAnimationUtils {
+
+ private static final String TAG = "FlingAnimationUtils";
+
+ private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
+ private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f;
+ private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
+ private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
+ private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
+
+ private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f;
+ private final float mSpeedUpFactor;
+ private final float mY2;
+
+ private float mMinVelocityPxPerSecond;
+ private float mMaxLengthSeconds;
+ private float mHighVelocityPxPerSecond;
+ private float mLinearOutSlowInX2;
+
+ private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
+ private PathInterpolator mInterpolator;
+ private float mCachedStartGradient = -1;
+ private float mCachedVelocityFactor = -1;
+
+ public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds) {
+ this(displayMetrics, maxLengthSeconds, 0.0f);
+ }
+
+ /**
+ * @param maxLengthSeconds the longest duration an animation can become in seconds
+ * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+ * the end of the animation. 0 means it's at the beginning and no
+ * acceleration will take place.
+ */
+ public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds,
+ float speedUpFactor) {
+ this(displayMetrics, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f);
+ }
+
+ /**
+ * @param maxLengthSeconds the longest duration an animation can become in seconds
+ * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+ * the end of the animation. 0 means it's at the beginning and no
+ * acceleration will take place.
+ * @param x2 the x value to take for the second point of the bezier spline. If a
+ * value below 0 is provided, the value is automatically calculated.
+ * @param y2 the y value to take for the second point of the bezier spline
+ */
+ public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds,
+ float speedUpFactor, float x2, float y2) {
+ mMaxLengthSeconds = maxLengthSeconds;
+ mSpeedUpFactor = speedUpFactor;
+ if (x2 < 0) {
+ mLinearOutSlowInX2 = interpolate(LINEAR_OUT_SLOW_IN_X2,
+ LINEAR_OUT_SLOW_IN_X2_MAX,
+ mSpeedUpFactor);
+ } else {
+ mLinearOutSlowInX2 = x2;
+ }
+ mY2 = y2;
+
+ mMinVelocityPxPerSecond = MIN_VELOCITY_DP_PER_SECOND * displayMetrics.density;
+ mHighVelocityPxPerSecond = HIGH_VELOCITY_DP_PER_SECOND * displayMetrics.density;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(Animator animator, float currValue, float endValue, float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+ float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(Animator animator, float currValue, float endValue, float velocity,
+ float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.mDuration);
+ animator.setInterpolator(properties.mInterpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+ float velocity, float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.mDuration);
+ animator.setInterpolator(properties.mInterpolator);
+ }
+
+ private AnimatorProperties getProperties(float currValue,
+ float endValue, float velocity, float maxDistance) {
+ float maxLengthSeconds = (float) (mMaxLengthSeconds
+ * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float velocityFactor = mSpeedUpFactor == 0.0f
+ ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f);
+ float startGradient = interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT,
+ mY2 / mLinearOutSlowInX2, velocityFactor);
+ float durationSeconds = startGradient * diff / velAbs;
+ Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor);
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.mInterpolator = slowInInterpolator;
+ } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+ // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator = new VelocityInterpolator(
+ durationSeconds, velAbs, diff);
+ InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+ velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN);
+ mAnimatorProperties.mInterpolator = superInterpolator;
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.mInterpolator = Interpolators.FAST_OUT_SLOW_IN;
+ }
+ mAnimatorProperties.mDuration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ private Interpolator getInterpolator(float startGradient, float velocityFactor) {
+ if (Float.isNaN(velocityFactor)) {
+ Log.e(TAG, "Invalid velocity factor", new Throwable());
+ return Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ if (startGradient != mCachedStartGradient
+ || velocityFactor != mCachedVelocityFactor) {
+ float speedup = mSpeedUpFactor * (1.0f - velocityFactor);
+ float x1 = speedup;
+ float y1 = speedup * startGradient;
+ float x2 = mLinearOutSlowInX2;
+ float y2 = mY2;
+ try {
+ mInterpolator = new PathInterpolator(x1, y1, x2, y2);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Illegal path with "
+ + "x1=" + x1 + " y1=" + y1 + " x2=" + x2 + " y2=" + y2, e);
+ }
+ mCachedStartGradient = startGradient;
+ mCachedVelocityFactor = velocityFactor;
+ }
+ return mInterpolator;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(Animator animator, float currValue, float endValue,
+ float velocity, float maxDistance) {
+ AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.mDuration);
+ animator.setInterpolator(properties.mInterpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length
+ * gets multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue,
+ float velocity, float maxDistance) {
+ AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+ maxDistance);
+ animator.setDuration(properties.mDuration);
+ animator.setInterpolator(properties.mInterpolator);
+ }
+
+ private AnimatorProperties getDismissingProperties(float currValue, float endValue,
+ float velocity, float maxDistance) {
+ float maxLengthSeconds = (float) (mMaxLengthSeconds
+ * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float y2 = calculateLinearOutFasterInY2(velAbs);
+
+ float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
+ Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
+ float durationSeconds = startGradient * diff / velAbs;
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.mInterpolator = mLinearOutFasterIn;
+ } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+ // Cross fade between linear-out-faster-in and linear interpolator with current
+ // velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator = new VelocityInterpolator(
+ durationSeconds, velAbs, diff);
+ InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+ velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN);
+ mAnimatorProperties.mInterpolator = superInterpolator;
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.mInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
+ }
+ mAnimatorProperties.mDuration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ /**
+ * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
+ * velocity. The faster the velocity, the more "linear" the interpolator gets.
+ *
+ * @param velocity the velocity of the gesture.
+ * @return the y2 control point for a cubic bezier path interpolator
+ */
+ private float calculateLinearOutFasterInY2(float velocity) {
+ float t = (velocity - mMinVelocityPxPerSecond)
+ / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
+ t = Math.max(0, Math.min(1, t));
+ return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
+ }
+
+ /**
+ * @return the minimum velocity a gesture needs to have to be considered a fling
+ */
+ public float getMinVelocityPxPerSecond() {
+ return mMinVelocityPxPerSecond;
+ }
+
+ /**
+ * An interpolator which interpolates two interpolators with an interpolator.
+ */
+ private static final class InterpolatorInterpolator implements Interpolator {
+
+ private Interpolator mInterpolator1;
+ private Interpolator mInterpolator2;
+ private Interpolator mCrossfader;
+
+ InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
+ Interpolator crossfader) {
+ mInterpolator1 = interpolator1;
+ mInterpolator2 = interpolator2;
+ mCrossfader = crossfader;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float t = mCrossfader.getInterpolation(input);
+ return (1 - t) * mInterpolator1.getInterpolation(input)
+ + t * mInterpolator2.getInterpolation(input);
+ }
+ }
+
+ /**
+ * An interpolator which interpolates with a fixed velocity.
+ */
+ private static final class VelocityInterpolator implements Interpolator {
+
+ private float mDurationSeconds;
+ private float mVelocity;
+ private float mDiff;
+
+ private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
+ mDurationSeconds = durationSeconds;
+ mVelocity = velocity;
+ mDiff = diff;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float time = input * mDurationSeconds;
+ return time * mVelocity / mDiff;
+ }
+ }
+
+ private static class AnimatorProperties {
+ Interpolator mInterpolator;
+ long mDuration;
+ }
+
+ /** Builder for {@link #FlingAnimationUtils}. */
+ public static class Builder {
+ private final DisplayMetrics mDisplayMetrics;
+ float mMaxLengthSeconds;
+ float mSpeedUpFactor;
+ float mX2;
+ float mY2;
+
+ public Builder(DisplayMetrics displayMetrics) {
+ mDisplayMetrics = displayMetrics;
+ reset();
+ }
+
+ /** Sets the longest duration an animation can become in seconds. */
+ public Builder setMaxLengthSeconds(float maxLengthSeconds) {
+ mMaxLengthSeconds = maxLengthSeconds;
+ return this;
+ }
+
+ /**
+ * Sets the factor for how much the slow down should be shifted towards the end of the
+ * animation.
+ */
+ public Builder setSpeedUpFactor(float speedUpFactor) {
+ mSpeedUpFactor = speedUpFactor;
+ return this;
+ }
+
+ /** Sets the x value to take for the second point of the bezier spline. */
+ public Builder setX2(float x2) {
+ mX2 = x2;
+ return this;
+ }
+
+ /** Sets the y value to take for the second point of the bezier spline. */
+ public Builder setY2(float y2) {
+ mY2 = y2;
+ return this;
+ }
+
+ /** Resets all parameters of the builder. */
+ public Builder reset() {
+ mMaxLengthSeconds = 0;
+ mSpeedUpFactor = 0.0f;
+ mX2 = -1.0f;
+ mY2 = 1.0f;
+
+ return this;
+ }
+
+ /** Builds {@link #FlingAnimationUtils}. */
+ public FlingAnimationUtils build() {
+ return new FlingAnimationUtils(mDisplayMetrics, mMaxLengthSeconds, mSpeedUpFactor,
+ mX2, mY2);
+ }
+ }
+
+ private static float interpolate(float start, float end, float amount) {
+ return start * (1.0f - amount) + end * amount;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FloatProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FloatProperties.kt
new file mode 100644
index 000000000000..d4f82829aa52
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FloatProperties.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.animation
+
+import android.graphics.Rect
+import android.graphics.RectF
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+
+/**
+ * Helpful extra properties to use with the [PhysicsAnimator]. These allow you to animate objects
+ * such as [Rect] and [RectF].
+ *
+ * There are additional, more basic properties available in [DynamicAnimation].
+ */
+class FloatProperties {
+ companion object {
+ /**
+ * Represents the x-coordinate of a [Rect]. Typically used to animate moving a Rect
+ * horizontally.
+ *
+ * This property's getter returns [Rect.left], and its setter uses [Rect.offsetTo], which
+ * sets [Rect.left] to the new value and offsets [Rect.right] so that the width of the Rect
+ * does not change.
+ */
+ @JvmField
+ val RECT_X = object : FloatPropertyCompat<Rect>("RectX") {
+ override fun setValue(rect: Rect?, value: Float) {
+ rect?.offsetTo(value.toInt(), rect.top)
+ }
+
+ override fun getValue(rect: Rect?): Float {
+ return rect?.left?.toFloat() ?: -Float.MAX_VALUE
+ }
+ }
+
+ /**
+ * Represents the y-coordinate of a [Rect]. Typically used to animate moving a Rect
+ * vertically.
+ *
+ * This property's getter returns [Rect.top], and its setter uses [Rect.offsetTo], which
+ * sets [Rect.top] to the new value and offsets [Rect.bottom] so that the height of the Rect
+ * does not change.
+ */
+ @JvmField
+ val RECT_Y = object : FloatPropertyCompat<Rect>("RectY") {
+ override fun setValue(rect: Rect?, value: Float) {
+ rect?.offsetTo(rect.left, value.toInt())
+ }
+
+ override fun getValue(rect: Rect?): Float {
+ return rect?.top?.toFloat() ?: -Float.MAX_VALUE
+ }
+ }
+
+ /**
+ * Represents the width of a [Rect]. Typically used to animate resizing a Rect horizontally.
+ *
+ * This property's getter returns [Rect.width], and its setter changes the value of
+ * [Rect.right] by adding the animated width value to [Rect.left].
+ */
+ @JvmField
+ val RECT_WIDTH = object : FloatPropertyCompat<Rect>("RectWidth") {
+ override fun getValue(rect: Rect): Float {
+ return rect.width().toFloat()
+ }
+
+ override fun setValue(rect: Rect, value: Float) {
+ rect.right = rect.left + value.toInt()
+ }
+ }
+
+ /**
+ * Represents the height of a [Rect]. Typically used to animate resizing a Rect vertically.
+ *
+ * This property's getter returns [Rect.height], and its setter changes the value of
+ * [Rect.bottom] by adding the animated height value to [Rect.top].
+ */
+ @JvmField
+ val RECT_HEIGHT = object : FloatPropertyCompat<Rect>("RectHeight") {
+ override fun getValue(rect: Rect): Float {
+ return rect.height().toFloat()
+ }
+
+ override fun setValue(rect: Rect, value: Float) {
+ rect.bottom = rect.top + value.toInt()
+ }
+ }
+
+ /**
+ * Represents the x-coordinate of a [RectF]. Typically used to animate moving a RectF
+ * horizontally.
+ *
+ * This property's getter returns [RectF.left], and its setter uses [RectF.offsetTo], which
+ * sets [RectF.left] to the new value and offsets [RectF.right] so that the width of the
+ * RectF does not change.
+ */
+ @JvmField
+ val RECTF_X = object : FloatPropertyCompat<RectF>("RectFX") {
+ override fun setValue(rect: RectF?, value: Float) {
+ rect?.offsetTo(value, rect.top)
+ }
+
+ override fun getValue(rect: RectF?): Float {
+ return rect?.left ?: -Float.MAX_VALUE
+ }
+ }
+
+ /**
+ * Represents the y-coordinate of a [RectF]. Typically used to animate moving a RectF
+ * vertically.
+ *
+ * This property's getter returns [RectF.top], and its setter uses [RectF.offsetTo], which
+ * sets [RectF.top] to the new value and offsets [RectF.bottom] so that the height of the
+ * RectF does not change.
+ */
+ @JvmField
+ val RECTF_Y = object : FloatPropertyCompat<RectF>("RectFY") {
+ override fun setValue(rect: RectF?, value: Float) {
+ rect?.offsetTo(rect.left, value)
+ }
+
+ override fun getValue(rect: RectF?): Float {
+ return rect?.top ?: -Float.MAX_VALUE
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
new file mode 100644
index 000000000000..416ada739aa3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
@@ -0,0 +1,55 @@
+/*
+ * 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.animation;
+
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Common interpolators used in wm shell library.
+ */
+public class Interpolators {
+ /**
+ * Interpolator for alpha in animation.
+ */
+ public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+
+ /**
+ * Interpolator for alpha out animation.
+ */
+ public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
+
+ /**
+ * Interpolator for fast out linear in animation.
+ */
+ public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+
+ /**
+ * Interpolator for fast out slow in animation.
+ */
+ public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+
+ /**
+ * Interpolator for linear out slow in animation.
+ */
+ public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+
+ /**
+ * Interpolator to be used when animating a move based on a click. Pair with enough duration.
+ */
+ public static final Interpolator TOUCH_RESPONSE = new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
new file mode 100644
index 000000000000..5cd660a2caa5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
@@ -0,0 +1,1071 @@
+/*
+ * 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.animation
+
+import android.os.Looper
+import android.util.ArrayMap
+import android.util.Log
+import android.view.View
+import androidx.dynamicanimation.animation.AnimationHandler
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FlingAnimation
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.wm.shell.animation.PhysicsAnimator.Companion.getInstance
+import java.lang.ref.WeakReference
+import java.util.WeakHashMap
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Extension function for all objects which will return a PhysicsAnimator instance for that object.
+ */
+val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(this) }
+
+private const val TAG = "PhysicsAnimator"
+
+private val UNSET = -Float.MAX_VALUE
+
+/**
+ * [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is
+ * where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the
+ * minimum velocity for a fling to reach a certain value, given the fling's friction.
+ */
+private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f
+
+typealias EndAction = () -> Unit
+
+/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */
+typealias UpdateMap<T> =
+ ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
+
+/**
+ * Map of the animators associated with a given object. This ensures that only one animator
+ * per object exists.
+ */
+internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>()
+
+/**
+ * Default spring configuration to use for animations where stiffness and/or damping ratio
+ * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
+ */
+private val globalDefaultSpring = PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_MEDIUM,
+ SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+
+/**
+ * Default fling configuration to use for animations where friction was not provided, and a default
+ * fling config was not set via [PhysicsAnimator.setDefaultFlingConfig].
+ */
+private val globalDefaultFling = PhysicsAnimator.FlingConfig(
+ friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE)
+
+/** Whether to log helpful debug information about animations. */
+private var verboseLogging = false
+
+/**
+ * Animator that uses physics-based animations to animate properties on views and objects. Physics
+ * animations use real-world physical concepts, such as momentum and mass, to realistically simulate
+ * motion. PhysicsAnimator is heavily inspired by [android.view.ViewPropertyAnimator], and
+ * also uses the builder pattern to configure and start animations.
+ *
+ * The physics animations are backed by [DynamicAnimation].
+ *
+ * @param T The type of the object being animated.
+ */
+class PhysicsAnimator<T> private constructor (target: T) {
+ /** Weak reference to the animation target. */
+ val weakTarget = WeakReference(target)
+
+ /** Data class for representing animation frame updates. */
+ data class AnimationUpdate(val value: Float, val velocity: Float)
+
+ /** [DynamicAnimation] instances for the given properties. */
+ private val springAnimations = ArrayMap<FloatPropertyCompat<in T>, SpringAnimation>()
+ private val flingAnimations = ArrayMap<FloatPropertyCompat<in T>, FlingAnimation>()
+
+ /**
+ * Spring and fling configurations for the properties to be animated on the target. We'll
+ * configure and start the DynamicAnimations for these properties according to the provided
+ * configurations.
+ */
+ private val springConfigs = ArrayMap<FloatPropertyCompat<in T>, SpringConfig>()
+ private val flingConfigs = ArrayMap<FloatPropertyCompat<in T>, FlingConfig>()
+
+ /**
+ * Animation listeners for the animation. These will be notified when each property animation
+ * updates or ends.
+ */
+ private val updateListeners = ArrayList<UpdateListener<T>>()
+ private val endListeners = ArrayList<EndListener<T>>()
+
+ /** End actions to run when all animations have completed. */
+ private val endActions = ArrayList<EndAction>()
+
+ /** SpringConfig to use by default for properties whose springs were not provided. */
+ private var defaultSpring: SpringConfig = globalDefaultSpring
+
+ /** FlingConfig to use by default for properties whose fling configs were not provided. */
+ private var defaultFling: FlingConfig = globalDefaultFling
+
+ /**
+ * AnimationHandler to use if it need custom AnimationHandler, if this is null, it will use
+ * the default AnimationHandler in the DynamicAnimation.
+ */
+ private var customAnimationHandler: AnimationHandler? = null
+
+ /**
+ * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to
+ * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add
+ * just one permanent update and end listener to the DynamicAnimations.
+ */
+ internal var internalListeners = ArrayList<InternalListener>()
+
+ /**
+ * Action to run when [start] is called. This can be changed by
+ * [PhysicsAnimatorTestUtils.prepareForTest] to enable animators to run under test and provide
+ * helpful test utilities.
+ */
+ internal var startAction: () -> Unit = ::startInternal
+
+ /**
+ * Action to run when [cancel] is called. This can be changed by
+ * [PhysicsAnimatorTestUtils.prepareForTest] to cancel animations from the main thread, which
+ * is required.
+ */
+ internal var cancelAction: (Set<FloatPropertyCompat<in T>>) -> Unit = ::cancelInternal
+
+ /**
+ * Springs a property to the given value, using the provided configuration settings.
+ *
+ * Springs are used when you know the exact value to which you want to animate. They can be
+ * configured with a start velocity (typically used when the spring is initiated by a touch
+ * event), but this velocity will be realistically attenuated as forces are applied to move the
+ * property towards the end value.
+ *
+ * If you find yourself repeating the same stiffness and damping ratios many times, consider
+ * storing a single [SpringConfig] instance and passing that in instead of individual values.
+ *
+ * @param property The property to spring to the given value. The property must be an instance
+ * of FloatPropertyCompat&lt;? super T&gt;. For example, if this is a
+ * PhysicsAnimator&lt;FrameLayout&gt;, you can use a FloatPropertyCompat&lt;FrameLayout&gt;, as
+ * well as a FloatPropertyCompat&lt;ViewGroup&gt;, and so on.
+ * @param toPosition The value to spring the given property to.
+ * @param startVelocity The initial velocity to use for the animation.
+ * @param stiffness The stiffness to use for the spring. Higher stiffness values result in
+ * faster animations, while lower stiffness means a slower animation. Reasonable values for
+ * low, medium, and high stiffness can be found as constants in [SpringForce].
+ * @param dampingRatio The damping ratio (bounciness) to use for the spring. Higher values
+ * result in a less 'springy' animation, while lower values allow the animation to bounce
+ * back and forth for a longer time after reaching the final position. Reasonable values for
+ * low, medium, and high damping can be found in [SpringForce].
+ */
+ fun spring(
+ property: FloatPropertyCompat<in T>,
+ toPosition: Float,
+ startVelocity: Float = 0f,
+ stiffness: Float = defaultSpring.stiffness,
+ dampingRatio: Float = defaultSpring.dampingRatio
+ ): PhysicsAnimator<T> {
+ if (verboseLogging) {
+ Log.d(TAG, "Springing ${getReadablePropertyName(property)} to $toPosition.")
+ }
+
+ springConfigs[property] =
+ SpringConfig(stiffness, dampingRatio, startVelocity, toPosition)
+ return this
+ }
+
+ /**
+ * Springs a property to a given value using the provided start velocity and configuration
+ * options.
+ *
+ * @see spring
+ */
+ fun spring(
+ property: FloatPropertyCompat<in T>,
+ toPosition: Float,
+ startVelocity: Float,
+ config: SpringConfig = defaultSpring
+ ): PhysicsAnimator<T> {
+ return spring(
+ property, toPosition, startVelocity, config.stiffness, config.dampingRatio)
+ }
+
+ /**
+ * Springs a property to a given value using the provided configuration options, and a start
+ * velocity of 0f.
+ *
+ * @see spring
+ */
+ fun spring(
+ property: FloatPropertyCompat<in T>,
+ toPosition: Float,
+ config: SpringConfig = defaultSpring
+ ): PhysicsAnimator<T> {
+ return spring(property, toPosition, 0f, config)
+ }
+
+ /**
+ * Springs a property to a given value using the provided configuration options, and a start
+ * velocity of 0f.
+ *
+ * @see spring
+ */
+ fun spring(
+ property: FloatPropertyCompat<in T>,
+ toPosition: Float
+ ): PhysicsAnimator<T> {
+ return spring(property, toPosition, 0f)
+ }
+
+ /**
+ * Flings a property using the given start velocity, using a [FlingAnimation] configured using
+ * the provided configuration settings.
+ *
+ * Flings are used when you have a start velocity, and want the property value to realistically
+ * decrease as friction is applied until the velocity reaches zero. Flings do not have a
+ * deterministic end value. If you are attempting to animate to a specific end value, use
+ * [spring].
+ *
+ * If you find yourself repeating the same friction/min/max values, consider storing a single
+ * [FlingConfig] and passing that in instead.
+ *
+ * @param property The property to fling using the given start velocity.
+ * @param startVelocity The start velocity (in pixels per second) with which to start the fling.
+ * @param friction Friction value applied to slow down the animation over time. Higher values
+ * will more quickly slow the animation. Typical friction values range from 1f to 10f.
+ * @param min The minimum value allowed for the animation. If this value is reached, the
+ * animation will end abruptly.
+ * @param max The maximum value allowed for the animation. If this value is reached, the
+ * animation will end abruptly.
+ */
+ fun fling(
+ property: FloatPropertyCompat<in T>,
+ startVelocity: Float,
+ friction: Float = defaultFling.friction,
+ min: Float = defaultFling.min,
+ max: Float = defaultFling.max
+ ): PhysicsAnimator<T> {
+ if (verboseLogging) {
+ Log.d(TAG, "Flinging ${getReadablePropertyName(property)} " +
+ "with velocity $startVelocity.")
+ }
+
+ flingConfigs[property] = FlingConfig(friction, min, max, startVelocity)
+ return this
+ }
+
+ /**
+ * Flings a property using the given start velocity, using a [FlingAnimation] configured using
+ * the provided configuration settings.
+ *
+ * @see fling
+ */
+ fun fling(
+ property: FloatPropertyCompat<in T>,
+ startVelocity: Float,
+ config: FlingConfig = defaultFling
+ ): PhysicsAnimator<T> {
+ return fling(property, startVelocity, config.friction, config.min, config.max)
+ }
+
+ /**
+ * Flings a property using the given start velocity. If the fling animation reaches the min/max
+ * bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back.
+ *
+ * If the object is already out of the fling bounds, it will immediately spring back within
+ * bounds.
+ *
+ * This is useful for animating objects that are bounded by constraints such as screen edges,
+ * since otherwise the fling animation would end abruptly upon reaching the min/max bounds.
+ *
+ * @param property The property to animate.
+ * @param startVelocity The velocity, in pixels/second, with which to start the fling. If the
+ * object is already outside the fling bounds, this velocity will be used as the start velocity
+ * of the spring that will spring it back within bounds.
+ * @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its
+ * minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The
+ * animator will use startVelocity if it's sufficient, or add more velocity if necessary. This
+ * is useful when fling's deceleration-based physics are preferable to the acceleration-based
+ * forces used by springs - typically, when you're allowing the user to move an object somewhere
+ * on the screen, but it needs to be along an edge.
+ * @param flingConfig The configuration to use for the fling portion of the animation.
+ * @param springConfig The configuration to use for the spring portion of the animation.
+ */
+ @JvmOverloads
+ fun flingThenSpring(
+ property: FloatPropertyCompat<in T>,
+ startVelocity: Float,
+ flingConfig: FlingConfig,
+ springConfig: SpringConfig,
+ flingMustReachMinOrMax: Boolean = false
+ ): PhysicsAnimator<T> {
+ val target = weakTarget.get()
+ if (target == null) {
+ Log.w(TAG, "Trying to animate a GC-ed target.")
+ return this
+ }
+ val flingConfigCopy = flingConfig.copy()
+ val springConfigCopy = springConfig.copy()
+ val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max
+
+ if (flingMustReachMinOrMax && isValidValue(toAtLeast)) {
+ val currentValue = property.getValue(target)
+ val flingTravelDistance =
+ startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
+ val projectedFlingEndValue = currentValue + flingTravelDistance
+ val midpoint = (flingConfig.min + flingConfig.max) / 2
+
+ // If fling velocity is too low to push the target past the midpoint between min and
+ // max, then spring back towards the nearest edge, starting with the current velocity.
+ if ((startVelocity < 0 && projectedFlingEndValue > midpoint) ||
+ (startVelocity > 0 && projectedFlingEndValue < midpoint)) {
+ val toPosition =
+ if (projectedFlingEndValue < midpoint) flingConfig.min else flingConfig.max
+ if (isValidValue(toPosition)) {
+ return spring(property, toPosition, startVelocity, springConfig)
+ }
+ }
+
+ // Projected fling end value is past the midpoint, so fling forward.
+ val distanceToDestination = toAtLeast - property.getValue(target)
+
+ // The minimum velocity required for the fling to end up at the given destination,
+ // taking the provided fling friction value.
+ val velocityToReachDestination = distanceToDestination *
+ (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
+
+ // If there's distance to cover, and the provided velocity is moving in the correct
+ // direction, ensure that the velocity is high enough to reach the destination.
+ // Otherwise, just use startVelocity - this means that the fling is at or out of bounds.
+ // The fling will immediately end and a spring will bring the object back into bounds
+ // with this startVelocity.
+ flingConfigCopy.startVelocity = when {
+ distanceToDestination > 0f && startVelocity >= 0f ->
+ max(velocityToReachDestination, startVelocity)
+ distanceToDestination < 0f && startVelocity <= 0f ->
+ min(velocityToReachDestination, startVelocity)
+ else -> startVelocity
+ }
+
+ springConfigCopy.finalPosition = toAtLeast
+ } else {
+ flingConfigCopy.startVelocity = startVelocity
+ }
+
+ flingConfigs[property] = flingConfigCopy
+ springConfigs[property] = springConfigCopy
+ return this
+ }
+
+ private fun isValidValue(value: Float) = value < Float.MAX_VALUE && value > -Float.MAX_VALUE
+
+ /**
+ * Adds a listener that will be called whenever any property on the animated object is updated.
+ * This will be called on every animation frame, with the current value of the animated object
+ * and the new property values.
+ */
+ fun addUpdateListener(listener: UpdateListener<T>): PhysicsAnimator<T> {
+ updateListeners.add(listener)
+ return this
+ }
+
+ /**
+ * Adds a listener that will be called when a property stops animating. This is useful if
+ * you care about a specific property ending, or want to use the end value/end velocity from a
+ * particular property's animation. If you just want to run an action when all property
+ * animations have ended, use [withEndActions].
+ */
+ fun addEndListener(listener: EndListener<T>): PhysicsAnimator<T> {
+ endListeners.add(listener)
+ return this
+ }
+
+ /**
+ * Adds end actions that will be run sequentially when animations for every property involved in
+ * this specific animation have ended (unless they were explicitly canceled). For example, if
+ * you call:
+ *
+ * animator
+ * .spring(TRANSLATION_X, ...)
+ * .spring(TRANSLATION_Y, ...)
+ * .withEndAction(action)
+ * .start()
+ *
+ * 'action' will be run when both TRANSLATION_X and TRANSLATION_Y end.
+ *
+ * Other properties may still be animating, if those animations were not started in the same
+ * call. For example:
+ *
+ * animator
+ * .spring(ALPHA, ...)
+ * .start()
+ *
+ * animator
+ * .spring(TRANSLATION_X, ...)
+ * .spring(TRANSLATION_Y, ...)
+ * .withEndAction(action)
+ * .start()
+ *
+ * 'action' will still be run as soon as TRANSLATION_X and TRANSLATION_Y end, even if ALPHA is
+ * still animating.
+ *
+ * If you want to run actions as soon as a subset of property animations have ended, you want
+ * access to the animation's end value/velocity, or you want to run these actions even if the
+ * animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param,
+ * which indicates that all relevant animations have ended.
+ */
+ fun withEndActions(vararg endActions: EndAction?): PhysicsAnimator<T> {
+ this.endActions.addAll(endActions.filterNotNull())
+ return this
+ }
+
+ /**
+ * Helper overload so that callers from Java can use Runnables or method references as end
+ * actions without having to explicitly return Unit.
+ */
+ fun withEndActions(vararg endActions: Runnable?): PhysicsAnimator<T> {
+ this.endActions.addAll(endActions.filterNotNull().map { it::run })
+ return this
+ }
+
+ fun setDefaultSpringConfig(defaultSpring: SpringConfig) {
+ this.defaultSpring = defaultSpring
+ }
+
+ fun setDefaultFlingConfig(defaultFling: FlingConfig) {
+ this.defaultFling = defaultFling
+ }
+
+ /**
+ * Set the custom AnimationHandler for all aniatmion in this animator. Set this with null for
+ * restoring to default AnimationHandler.
+ */
+ fun setCustomAnimationHandler(handler: AnimationHandler) {
+ this.customAnimationHandler = handler
+ }
+
+ /** Starts the animations! */
+ fun start() {
+ startAction()
+ }
+
+ /**
+ * Starts the animations for real! This is typically called immediately by [start] unless this
+ * animator is under test.
+ */
+ internal fun startInternal() {
+ if (!Looper.getMainLooper().isCurrentThread) {
+ Log.e(TAG, "Animations can only be started on the main thread. If you are seeing " +
+ "this message in a test, call PhysicsAnimatorTestUtils#prepareForTest in " +
+ "your test setup.")
+ }
+ val target = weakTarget.get()
+ if (target == null) {
+ Log.w(TAG, "Trying to animate a GC-ed object.")
+ return
+ }
+
+ // Functions that will actually start the animations. These are run after we build and add
+ // the InternalListener, since some animations might update/end immediately and we don't
+ // want to miss those updates.
+ val animationStartActions = ArrayList<() -> Unit>()
+
+ for (animatedProperty in getAnimatedProperties()) {
+ val flingConfig = flingConfigs[animatedProperty]
+ val springConfig = springConfigs[animatedProperty]
+
+ // The property's current value on the object.
+ val currentValue = animatedProperty.getValue(target)
+
+ // Start by checking for a fling configuration. If one is present, we're either flinging
+ // or flinging-then-springing. Either way, we'll want to start the fling first.
+ if (flingConfig != null) {
+ animationStartActions.add {
+ // When the animation is starting, adjust the min/max bounds to include the
+ // current value of the property, if necessary. This is required to allow a
+ // fling to bring an out-of-bounds object back into bounds. For example, if an
+ // object was dragged halfway off the left side of the screen, but then flung to
+ // the right, we don't want the animation to end instantly just because the
+ // object started out of bounds. If the fling is in the direction that would
+ // take it farther out of bounds, it will end instantly as expected.
+ flingConfig.apply {
+ min = min(currentValue, this.min)
+ max = max(currentValue, this.max)
+ }
+
+ // Flings can't be updated to a new position while maintaining velocity, because
+ // we're using the explicitly provided start velocity. Cancel any flings (or
+ // springs) on this property before flinging.
+ cancel(animatedProperty)
+
+ // Apply the custom animation handler if it not null
+ val flingAnim = getFlingAnimation(animatedProperty, target)
+ flingAnim.animationHandler =
+ customAnimationHandler ?: flingAnim.animationHandler
+
+ // Apply the configuration and start the animation.
+ flingAnim.also { flingConfig.applyToAnimation(it) }.start()
+ }
+ }
+
+ // Check for a spring configuration. If one is present, we're either springing, or
+ // flinging-then-springing.
+ if (springConfig != null) {
+
+ // If there is no corresponding fling config, we're only springing.
+ if (flingConfig == null) {
+ // Apply the configuration and start the animation.
+ val springAnim = getSpringAnimation(animatedProperty, target)
+
+ // If customAnimationHander is exist and has not been set to the animation,
+ // it should set here.
+ if (customAnimationHandler != null &&
+ springAnim.animationHandler != customAnimationHandler) {
+ // Cancel the animation before set animation handler
+ if (springAnim.isRunning) {
+ cancel(animatedProperty)
+ }
+ // Apply the custom animation handler if it not null
+ springAnim.animationHandler =
+ customAnimationHandler ?: springAnim.animationHandler
+ }
+
+ // Apply the configuration and start the animation.
+ springConfig.applyToAnimation(springAnim)
+ animationStartActions.add(springAnim::start)
+ } else {
+ // If there's a corresponding fling config, we're flinging-then-springing. Save
+ // the fling's original bounds so we can spring to them when the fling ends.
+ val flingMin = flingConfig.min
+ val flingMax = flingConfig.max
+
+ // Add an end listener that will start the spring when the fling ends.
+ endListeners.add(0, object : EndListener<T> {
+ override fun onAnimationEnd(
+ target: T,
+ property: FloatPropertyCompat<in T>,
+ wasFling: Boolean,
+ canceled: Boolean,
+ finalValue: Float,
+ finalVelocity: Float,
+ allRelevantPropertyAnimsEnded: Boolean
+ ) {
+ // If this isn't the relevant property, it wasn't a fling, or the fling
+ // was explicitly cancelled, don't spring.
+ if (property != animatedProperty || !wasFling || canceled) {
+ return
+ }
+
+ val endedWithVelocity = abs(finalVelocity) > 0
+
+ // If the object was out of bounds when the fling animation started, it
+ // will immediately end. In that case, we'll spring it back in bounds.
+ val endedOutOfBounds = finalValue !in flingMin..flingMax
+
+ // If the fling ended either out of bounds or with remaining velocity,
+ // it's time to spring.
+ if (endedWithVelocity || endedOutOfBounds) {
+ springConfig.startVelocity = finalVelocity
+
+ // If the spring's final position isn't set, this is a
+ // flingThenSpring where flingMustReachMinOrMax was false. We'll
+ // need to set the spring's final position here.
+ if (springConfig.finalPosition == UNSET) {
+ if (endedWithVelocity) {
+ // If the fling ended with negative velocity, that means it
+ // hit the min bound, so spring to that bound (and vice
+ // versa).
+ springConfig.finalPosition =
+ if (finalVelocity < 0) flingMin else flingMax
+ } else if (endedOutOfBounds) {
+ // If the fling ended out of bounds, spring it to the
+ // nearest bound.
+ springConfig.finalPosition =
+ if (finalValue < flingMin) flingMin else flingMax
+ }
+ }
+
+ // Apply the custom animation handler if it not null
+ val springAnim = getSpringAnimation(animatedProperty, target)
+ springAnim.animationHandler =
+ customAnimationHandler ?: springAnim.animationHandler
+
+ // Apply the configuration and start the spring animation.
+ springAnim.also { springConfig.applyToAnimation(it) }.start()
+ }
+ }
+ })
+ }
+ }
+ }
+
+ // Add an internal listener that will dispatch animation events to the provided listeners.
+ internalListeners.add(InternalListener(
+ target,
+ getAnimatedProperties(),
+ ArrayList(updateListeners),
+ ArrayList(endListeners),
+ ArrayList(endActions)))
+
+ // Actually start the DynamicAnimations. This is delayed until after the InternalListener is
+ // constructed and added so that we don't miss the end listener firing for any animations
+ // that immediately end.
+ animationStartActions.forEach { it.invoke() }
+
+ clearAnimator()
+ }
+
+ /** Clear the animator's builder variables. */
+ private fun clearAnimator() {
+ springConfigs.clear()
+ flingConfigs.clear()
+
+ updateListeners.clear()
+ endListeners.clear()
+ endActions.clear()
+ }
+
+ /** Retrieves a spring animation for the given property, building one if needed. */
+ private fun getSpringAnimation(
+ property: FloatPropertyCompat<in T>,
+ target: T
+ ): SpringAnimation {
+ return springAnimations.getOrPut(
+ property,
+ { configureDynamicAnimation(SpringAnimation(target, property), property)
+ as SpringAnimation })
+ }
+
+ /** Retrieves a fling animation for the given property, building one if needed. */
+ private fun getFlingAnimation(property: FloatPropertyCompat<in T>, target: T): FlingAnimation {
+ return flingAnimations.getOrPut(
+ property,
+ { configureDynamicAnimation(FlingAnimation(target, property), property)
+ as FlingAnimation })
+ }
+
+ /**
+ * Adds update and end listeners to the DynamicAnimation which will dispatch to the internal
+ * listeners.
+ */
+ private fun configureDynamicAnimation(
+ anim: DynamicAnimation<*>,
+ property: FloatPropertyCompat<in T>
+ ): DynamicAnimation<*> {
+ anim.addUpdateListener { _, value, velocity ->
+ for (i in 0 until internalListeners.size) {
+ internalListeners[i].onInternalAnimationUpdate(property, value, velocity)
+ }
+ }
+ anim.addEndListener { _, canceled, value, velocity ->
+ internalListeners.removeAll {
+ it.onInternalAnimationEnd(
+ property, canceled, value, velocity, anim is FlingAnimation)
+ }
+ if (springAnimations[property] == anim) {
+ springAnimations.remove(property)
+ }
+ if (flingAnimations[property] == anim) {
+ flingAnimations.remove(property)
+ }
+ }
+ return anim
+ }
+
+ /**
+ * Internal listener class that receives updates from DynamicAnimation listeners, and dispatches
+ * them to the appropriate update/end listeners. This class is also aware of which properties
+ * were being animated when the end listeners were passed in, so that we can provide the
+ * appropriate value for allEnded to [EndListener.onAnimationEnd].
+ */
+ internal inner class InternalListener constructor(
+ private val target: T,
+ private var properties: Set<FloatPropertyCompat<in T>>,
+ private var updateListeners: List<UpdateListener<T>>,
+ private var endListeners: List<EndListener<T>>,
+ private var endActions: List<EndAction>
+ ) {
+
+ /** The number of properties whose animations haven't ended. */
+ private var numPropertiesAnimating = properties.size
+
+ /**
+ * Update values that haven't yet been dispatched because not all property animations have
+ * updated yet.
+ */
+ private val undispatchedUpdates =
+ ArrayMap<FloatPropertyCompat<in T>, AnimationUpdate>()
+
+ /** Called when a DynamicAnimation updates. */
+ internal fun onInternalAnimationUpdate(
+ property: FloatPropertyCompat<in T>,
+ value: Float,
+ velocity: Float
+ ) {
+
+ // If this property animation isn't relevant to this listener, ignore it.
+ if (!properties.contains(property)) {
+ return
+ }
+
+ undispatchedUpdates[property] = AnimationUpdate(value, velocity)
+ maybeDispatchUpdates()
+ }
+
+ /**
+ * Called when a DynamicAnimation ends.
+ *
+ * @return True if this listener should be removed from the list of internal listeners, so
+ * it no longer receives updates from DynamicAnimations.
+ */
+ internal fun onInternalAnimationEnd(
+ property: FloatPropertyCompat<in T>,
+ canceled: Boolean,
+ finalValue: Float,
+ finalVelocity: Float,
+ isFling: Boolean
+ ): Boolean {
+
+ // If this property animation isn't relevant to this listener, ignore it.
+ if (!properties.contains(property)) {
+ return false
+ }
+
+ // Dispatch updates if we have one for each property.
+ numPropertiesAnimating--
+ maybeDispatchUpdates()
+
+ // If we didn't have an update for each property, dispatch the update for the ending
+ // property. This guarantees that an update isn't sent for this property *after* we call
+ // onAnimationEnd for that property.
+ if (undispatchedUpdates.contains(property)) {
+ updateListeners.forEach { updateListener ->
+ updateListener.onAnimationUpdateForProperty(
+ target,
+ UpdateMap<T>().also { it[property] = undispatchedUpdates[property] })
+ }
+
+ undispatchedUpdates.remove(property)
+ }
+
+ val allEnded = !arePropertiesAnimating(properties)
+ endListeners.forEach {
+ it.onAnimationEnd(
+ target, property, isFling, canceled, finalValue, finalVelocity,
+ allEnded)
+
+ // Check that the end listener didn't restart this property's animation.
+ if (isPropertyAnimating(property)) {
+ return false
+ }
+ }
+
+ // If all of the animations that this listener cares about have ended, run the end
+ // actions unless the animation was canceled.
+ if (allEnded && !canceled) {
+ endActions.forEach { it() }
+ }
+
+ return allEnded
+ }
+
+ /**
+ * Dispatch undispatched values if we've received an update from each of the animating
+ * properties.
+ */
+ private fun maybeDispatchUpdates() {
+ if (undispatchedUpdates.size >= numPropertiesAnimating &&
+ undispatchedUpdates.size > 0) {
+ updateListeners.forEach {
+ it.onAnimationUpdateForProperty(target, ArrayMap(undispatchedUpdates))
+ }
+
+ undispatchedUpdates.clear()
+ }
+ }
+ }
+
+ /** Return true if any animations are running on the object. */
+ fun isRunning(): Boolean {
+ return arePropertiesAnimating(springAnimations.keys.union(flingAnimations.keys))
+ }
+
+ /** Returns whether the given property is animating. */
+ fun isPropertyAnimating(property: FloatPropertyCompat<in T>): Boolean {
+ return springAnimations[property]?.isRunning ?: false ||
+ flingAnimations[property]?.isRunning ?: false
+ }
+
+ /** Returns whether any of the given properties are animating. */
+ fun arePropertiesAnimating(properties: Set<FloatPropertyCompat<in T>>): Boolean {
+ return properties.any { isPropertyAnimating(it) }
+ }
+
+ /** Return the set of properties that will begin animating upon calling [start]. */
+ internal fun getAnimatedProperties(): Set<FloatPropertyCompat<in T>> {
+ return springConfigs.keys.union(flingConfigs.keys)
+ }
+
+ /**
+ * Cancels the given properties. This is typically called immediately by [cancel], unless this
+ * animator is under test.
+ */
+ internal fun cancelInternal(properties: Set<FloatPropertyCompat<in T>>) {
+ for (property in properties) {
+ flingAnimations[property]?.cancel()
+ springAnimations[property]?.cancel()
+ }
+ }
+
+ /** Cancels all in progress animations on all properties. */
+ fun cancel() {
+ cancelAction(flingAnimations.keys)
+ cancelAction(springAnimations.keys)
+ }
+
+ /** Cancels in progress animations on the provided properties only. */
+ fun cancel(vararg properties: FloatPropertyCompat<in T>) {
+ cancelAction(properties.toSet())
+ }
+
+ /**
+ * Container object for spring animation configuration settings. This allows you to store
+ * default stiffness and damping ratio values in a single configuration object, which you can
+ * pass to [spring].
+ */
+ data class SpringConfig internal constructor(
+ internal var stiffness: Float,
+ internal var dampingRatio: Float,
+ internal var startVelocity: Float = 0f,
+ internal var finalPosition: Float = UNSET
+ ) {
+
+ constructor() :
+ this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio)
+
+ constructor(stiffness: Float, dampingRatio: Float) :
+ this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f)
+
+ /** Apply these configuration settings to the given SpringAnimation. */
+ internal fun applyToAnimation(anim: SpringAnimation) {
+ val springForce = anim.spring ?: SpringForce()
+ anim.spring = springForce.apply {
+ stiffness = this@SpringConfig.stiffness
+ dampingRatio = this@SpringConfig.dampingRatio
+ finalPosition = this@SpringConfig.finalPosition
+ }
+
+ if (startVelocity != 0f) anim.setStartVelocity(startVelocity)
+ }
+ }
+
+ /**
+ * Container object for fling animation configuration settings. This allows you to store default
+ * friction values (as well as optional min/max values) in a single configuration object, which
+ * you can pass to [fling] and related methods.
+ */
+ data class FlingConfig internal constructor(
+ internal var friction: Float,
+ internal var min: Float,
+ internal var max: Float,
+ internal var startVelocity: Float
+ ) {
+
+ constructor() : this(globalDefaultFling.friction)
+
+ constructor(friction: Float) :
+ this(friction, globalDefaultFling.min, globalDefaultFling.max)
+
+ constructor(friction: Float, min: Float, max: Float) :
+ this(friction, min, max, startVelocity = 0f)
+
+ /** Apply these configuration settings to the given FlingAnimation. */
+ internal fun applyToAnimation(anim: FlingAnimation) {
+ anim.apply {
+ friction = this@FlingConfig.friction
+ setMinValue(min)
+ setMaxValue(max)
+ setStartVelocity(startVelocity)
+ }
+ }
+ }
+
+ /**
+ * Listener for receiving values from in progress animations. Used with
+ * [PhysicsAnimator.addUpdateListener].
+ *
+ * @param <T> The type of the object being animated.
+ </T> */
+ interface UpdateListener<T> {
+
+ /**
+ * Called on each animation frame with the target object, and a map of FloatPropertyCompat
+ * -> AnimationUpdate, containing the latest value and velocity for that property. When
+ * multiple properties are animating together, the map will typically contain one entry for
+ * each property. However, you should never assume that this is the case - when a property
+ * animation ends earlier than the others, you'll receive an UpdateMap containing only that
+ * property's final update. Subsequently, you'll only receive updates for the properties
+ * that are still animating.
+ *
+ * Always check that the map contains an update for the property you're interested in before
+ * accessing it.
+ *
+ * @param target The animated object itself.
+ * @param values Map of property to AnimationUpdate, which contains that property
+ * animation's latest value and velocity. You should never assume that a particular property
+ * is present in this map.
+ */
+ fun onAnimationUpdateForProperty(
+ target: T,
+ values: UpdateMap<T>
+ )
+ }
+
+ /**
+ * Listener for receiving callbacks when animations end.
+ *
+ * @param <T> The type of the object being animated.
+ </T> */
+ interface EndListener<T> {
+
+ /**
+ * Called with the final animation values as each property animation ends. This can be used
+ * to respond to specific property animations concluding (such as hiding a view when ALPHA
+ * ends, even if the corresponding TRANSLATION animations have not ended).
+ *
+ * If you just want to run an action when all of the property animations have ended, you can
+ * use [PhysicsAnimator.withEndActions].
+ *
+ * @param target The animated object itself.
+ * @param property The property whose animation has just ended.
+ * @param wasFling Whether this property ended after a fling animation (as opposed to a
+ * spring animation). If this property was animated via [flingThenSpring], this will be true
+ * if the fling animation did not reach the min/max bounds, decelerating to a stop
+ * naturally. It will be false if it hit the bounds and was sprung back.
+ * @param canceled Whether the animation was explicitly canceled before it naturally ended.
+ * @param finalValue The final value of the animated property.
+ * @param finalVelocity The final velocity (in pixels per second) of the ended animation.
+ * This is typically zero, unless this was a fling animation which ended abruptly due to
+ * reaching its configured min/max values.
+ * @param allRelevantPropertyAnimsEnded Whether all properties relevant to this end listener
+ * have ended. Relevant properties are those which were animated alongside the
+ * [addEndListener] call where this animator was passed in. For example:
+ *
+ * animator
+ * .spring(TRANSLATION_X, 100f)
+ * .spring(TRANSLATION_Y, 200f)
+ * .withEndListener(firstEndListener)
+ * .start()
+ *
+ * firstEndListener will be called first for TRANSLATION_X, with allEnded = false,
+ * because TRANSLATION_Y is still running. When TRANSLATION_Y ends, it'll be called with
+ * allEnded = true.
+ *
+ * If a subsequent call to start() is made with other properties, those properties are not
+ * considered relevant and allEnded will still equal true when only TRANSLATION_X and
+ * TRANSLATION_Y end. For example, if immediately after the prior example, while
+ * TRANSLATION_X and TRANSLATION_Y are still animating, we called:
+ *
+ * animator.
+ * .spring(SCALE_X, 2f, stiffness = 10f) // That will take awhile...
+ * .withEndListener(secondEndListener)
+ * .start()
+ *
+ * firstEndListener will still be called with allEnded = true when TRANSLATION_X/Y end, even
+ * though SCALE_X is still animating. Similarly, secondEndListener will be called with
+ * allEnded = true as soon as SCALE_X ends, even if the translation animations are still
+ * running.
+ */
+ fun onAnimationEnd(
+ target: T,
+ property: FloatPropertyCompat<in T>,
+ wasFling: Boolean,
+ canceled: Boolean,
+ finalValue: Float,
+ finalVelocity: Float,
+ allRelevantPropertyAnimsEnded: Boolean
+ )
+ }
+
+ companion object {
+
+ /**
+ * Constructor to use to for new physics animator instances in [getInstance]. This is
+ * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that
+ * all code using the physics animator is given testable instances instead.
+ */
+ internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator
+
+ @JvmStatic
+ @Suppress("UNCHECKED_CAST")
+ fun <T : Any> getInstance(target: T): PhysicsAnimator<T> {
+ if (!animators.containsKey(target)) {
+ animators[target] = instanceConstructor(target)
+ }
+
+ return animators[target] as PhysicsAnimator<T>
+ }
+
+ /**
+ * Set whether all physics animators should log a lot of information about animations.
+ * Useful for debugging!
+ */
+ @JvmStatic
+ fun setVerboseLogging(debug: Boolean) {
+ verboseLogging = debug
+ }
+
+ /**
+ * Estimates the end value of a fling that starts at the given value using the provided
+ * start velocity and fling configuration.
+ *
+ * This is only an estimate. Fling animations use a timing-based physics simulation that is
+ * non-deterministic, so this exact value may not be reached.
+ */
+ @JvmStatic
+ fun estimateFlingEndValue(
+ startValue: Float,
+ startVelocity: Float,
+ flingConfig: FlingConfig
+ ): Float {
+ val distance = startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
+ return Math.min(flingConfig.max, Math.max(flingConfig.min, startValue + distance))
+ }
+
+ @JvmStatic
+ fun getReadablePropertyName(property: FloatPropertyCompat<*>): String {
+ return when (property) {
+ DynamicAnimation.TRANSLATION_X -> "translationX"
+ DynamicAnimation.TRANSLATION_Y -> "translationY"
+ DynamicAnimation.TRANSLATION_Z -> "translationZ"
+ DynamicAnimation.SCALE_X -> "scaleX"
+ DynamicAnimation.SCALE_Y -> "scaleY"
+ DynamicAnimation.ROTATION -> "rotation"
+ DynamicAnimation.ROTATION_X -> "rotationX"
+ DynamicAnimation.ROTATION_Y -> "rotationY"
+ DynamicAnimation.SCROLL_X -> "scrollX"
+ DynamicAnimation.SCROLL_Y -> "scrollY"
+ DynamicAnimation.ALPHA -> "alpha"
+ else -> "Custom FloatPropertyCompat instance"
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt
new file mode 100644
index 000000000000..86eb8da952f1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt
@@ -0,0 +1,485 @@
+/*
+ * 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.animation
+
+import android.os.Handler
+import android.os.Looper
+import android.util.ArrayMap
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.prepareForTest
+import java.util.*
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.collections.ArrayList
+import kotlin.collections.HashMap
+import kotlin.collections.HashSet
+import kotlin.collections.Set
+import kotlin.collections.component1
+import kotlin.collections.component2
+import kotlin.collections.drop
+import kotlin.collections.forEach
+import kotlin.collections.getOrPut
+import kotlin.collections.set
+import kotlin.collections.toList
+import kotlin.collections.toTypedArray
+
+typealias UpdateMatcher = (PhysicsAnimator.AnimationUpdate) -> Boolean
+typealias UpdateFramesPerProperty<T> =
+ ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>
+
+/**
+ * Utilities for testing code that uses [PhysicsAnimator].
+ *
+ * Start by calling [prepareForTest] at the beginning of each test - this will modify the behavior
+ * of all PhysicsAnimator instances so that they post animations to the main thread (so they don't
+ * crash). It'll also enable the use of the other static helper methods in this class, which you can
+ * use to do things like block the test until animations complete (so you can test end states), or
+ * verify keyframes.
+ */
+object PhysicsAnimatorTestUtils {
+ var timeoutMs: Long = 2000
+ private var startBlocksUntilAnimationsEnd = false
+ private val animationThreadHandler = Handler(Looper.getMainLooper())
+ private val allAnimatedObjects = HashSet<Any>()
+ private val animatorTestHelpers = HashMap<PhysicsAnimator<*>, AnimatorTestHelper<*>>()
+
+ /**
+ * Modifies the behavior of all [PhysicsAnimator] instances so that they post animations to the
+ * main thread, and report all of their
+ */
+ @JvmStatic
+ fun prepareForTest() {
+ val defaultConstructor = PhysicsAnimator.instanceConstructor
+ PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> {
+ val animator = defaultConstructor(target)
+ allAnimatedObjects.add(target)
+ animatorTestHelpers[animator] = AnimatorTestHelper(animator)
+ return animator
+ }
+
+ timeoutMs = 2000
+ startBlocksUntilAnimationsEnd = false
+ allAnimatedObjects.clear()
+ }
+
+ @JvmStatic
+ fun tearDown() {
+ val latch = CountDownLatch(1)
+ animationThreadHandler.post {
+ animatorTestHelpers.keys.forEach { it.cancel() }
+ latch.countDown()
+ }
+
+ latch.await()
+
+ animatorTestHelpers.clear()
+ animators.clear()
+ allAnimatedObjects.clear()
+ }
+
+ /**
+ * Sets the maximum time (in milliseconds) to block the test thread while waiting for animations
+ * before throwing an exception.
+ */
+ @JvmStatic
+ fun setBlockTimeout(timeoutMs: Long) {
+ PhysicsAnimatorTestUtils.timeoutMs = timeoutMs
+ }
+
+ /**
+ * Sets whether all animations should block the test thread until they end. This is typically
+ * the desired behavior, since you can invoke code that runs an animation and then assert things
+ * about its end state.
+ */
+ @JvmStatic
+ fun setAllAnimationsBlock(block: Boolean) {
+ startBlocksUntilAnimationsEnd = block
+ }
+
+ /**
+ * Blocks the calling thread until animations of the given property on the target object end.
+ */
+ @JvmStatic
+ @Throws(InterruptedException::class)
+ fun <T : Any> blockUntilAnimationsEnd(
+ animator: PhysicsAnimator<T>,
+ vararg properties: FloatPropertyCompat<in T>
+ ) {
+ val animatingProperties = HashSet<FloatPropertyCompat<in T>>()
+ for (property in properties) {
+ if (animator.isPropertyAnimating(property)) {
+ animatingProperties.add(property)
+ }
+ }
+
+ if (animatingProperties.size > 0) {
+ val latch = CountDownLatch(animatingProperties.size)
+ getAnimationTestHelper(animator).addTestEndListener(
+ object : PhysicsAnimator.EndListener<T> {
+ override fun onAnimationEnd(
+ target: T,
+ property: FloatPropertyCompat<in T>,
+ wasFling: Boolean,
+ canceled: Boolean,
+ finalValue: Float,
+ finalVelocity: Float,
+ allRelevantPropertyAnimsEnded: Boolean
+ ) {
+ if (animatingProperties.contains(property)) {
+ latch.countDown()
+ }
+ }
+ })
+
+ latch.await(timeoutMs, TimeUnit.MILLISECONDS)
+ }
+ }
+
+ /**
+ * Blocks the calling thread until all animations of the given property (on all target objects)
+ * have ended. Useful when you don't have access to the objects being animated, but still need
+ * to wait for them to end so that other testable side effects occur (such as update/end
+ * listeners).
+ */
+ @JvmStatic
+ @Throws(InterruptedException::class)
+ @Suppress("UNCHECKED_CAST")
+ fun <T : Any> blockUntilAnimationsEnd(
+ properties: FloatPropertyCompat<in T>
+ ) {
+ for (target in allAnimatedObjects) {
+ try {
+ blockUntilAnimationsEnd(
+ PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties)
+ } catch (e: ClassCastException) {
+ // Keep checking the other objects for ones whose types match the provided
+ // properties.
+ }
+ }
+ }
+
+ /**
+ * Blocks the calling thread until the first animation frame in which predicate returns true. If
+ * the given object isn't animating, returns without blocking.
+ */
+ @JvmStatic
+ @Throws(InterruptedException::class)
+ fun <T : Any> blockUntilFirstAnimationFrameWhereTrue(
+ animator: PhysicsAnimator<T>,
+ predicate: (T) -> Boolean
+ ) {
+ if (animator.isRunning()) {
+ val latch = CountDownLatch(1)
+ getAnimationTestHelper(animator).addTestUpdateListener(object : PhysicsAnimator
+ .UpdateListener<T> {
+ override fun onAnimationUpdateForProperty(
+ target: T,
+ values: UpdateMap<T>
+ ) {
+ if (predicate(target)) {
+ latch.countDown()
+ }
+ }
+ })
+
+ latch.await(timeoutMs, TimeUnit.MILLISECONDS)
+ }
+ }
+
+ /**
+ * Verifies that the animator reported animation frame values to update listeners that satisfy
+ * the given matchers, in order. Not all frames need to satisfy a matcher - we'll run through
+ * all animation frames, and check them against the current predicate. If it returns false, we
+ * continue through the frames until it returns true, and then move on to the next matcher.
+ * Verification fails if we run out of frames while unsatisfied matchers remain.
+ *
+ * If verification is successful, all frames to this point are considered 'verified' and will be
+ * cleared. Subsequent calls to this method will start verification at the next animation frame.
+ *
+ * Example: Verify that an animation surpassed x = 50f before going negative.
+ * verifyAnimationUpdateFrames(
+ * animator, TRANSLATION_X,
+ * { u -> u.value > 50f },
+ * { u -> u.value < 0f })
+ *
+ * Example: verify that an animation went backwards at some point while still being on-screen.
+ * verifyAnimationUpdateFrames(
+ * animator, TRANSLATION_X,
+ * { u -> u.velocity < 0f && u.value >= 0f })
+ *
+ * This method is intended to help you test longer, more complicated animations where it's
+ * critical that certain values were reached. Using this method to test short animations can
+ * fail due to the animation having fewer frames than provided matchers. For example, an
+ * animation from x = 1f to x = 5f might only have two frames, at x = 3f and x = 5f. The
+ * following would then fail despite it seeming logically sound:
+ *
+ * verifyAnimationUpdateFrames(
+ * animator, TRANSLATION_X,
+ * { u -> u.value > 1f },
+ * { u -> u.value > 2f },
+ * { u -> u.value > 3f })
+ *
+ * Tests might also fail if your matchers are too granular, such as this example test after an
+ * animation from x = 0f to x = 100f. It's unlikely there was a frame specifically between 2f
+ * and 3f.
+ *
+ * verifyAnimationUpdateFrames(
+ * animator, TRANSLATION_X,
+ * { u -> u.value > 2f && u.value < 3f },
+ * { u -> u.value >= 50f })
+ *
+ * Failures will print a helpful log of all animation frames so you can see what caused the test
+ * to fail.
+ */
+ fun <T : Any> verifyAnimationUpdateFrames(
+ animator: PhysicsAnimator<T>,
+ property: FloatPropertyCompat<in T>,
+ firstUpdateMatcher: UpdateMatcher,
+ vararg additionalUpdateMatchers: UpdateMatcher
+ ) {
+ val updateFrames: UpdateFramesPerProperty<T> = getAnimationUpdateFrames(animator)
+
+ if (!updateFrames.containsKey(property)) {
+ error("No frames for given target object and property.")
+ }
+
+ // Copy the frames to avoid a ConcurrentModificationException if the animation update
+ // listeners attempt to add a new frame while we're verifying these.
+ val framesForProperty = ArrayList(updateFrames[property]!!)
+ val matchers = ArrayDeque<UpdateMatcher>(
+ additionalUpdateMatchers.toList())
+ val frameTraceMessage = StringBuilder()
+
+ var curMatcher = firstUpdateMatcher
+
+ // Loop through the updates from the testable animator.
+ for (update in framesForProperty) {
+
+ // Check whether this frame satisfies the current matcher.
+ if (curMatcher(update)) {
+
+ // If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining
+ // frames and return without failing.
+ if (matchers.size == 0) {
+ getAnimationUpdateFrames(animator).remove(property)
+ return
+ }
+
+ frameTraceMessage.append("$update\t(satisfied matcher)\n")
+ curMatcher = matchers.pop() // Get the next matcher and keep going.
+ } else {
+ frameTraceMessage.append("${update}\n")
+ }
+ }
+
+ val readablePropertyName = PhysicsAnimator.getReadablePropertyName(property)
+ getAnimationUpdateFrames(animator).remove(property)
+
+ throw RuntimeException(
+ "Failed to verify animation frames for property $readablePropertyName: " +
+ "Provided ${additionalUpdateMatchers.size + 1} matchers, " +
+ "however ${matchers.size + 1} remained unsatisfied.\n\n" +
+ "All frames:\n$frameTraceMessage")
+ }
+
+ /**
+ * Overload of [verifyAnimationUpdateFrames] that builds matchers for you, from given float
+ * values. For example, to verify that an animations passed from 0f to 50f to 100f back to 50f:
+ *
+ * verifyAnimationUpdateFrames(animator, TRANSLATION_X, 0f, 50f, 100f, 50f)
+ *
+ * This verifies that update frames were received with values of >= 0f, >= 50f, >= 100f, and
+ * <= 50f.
+ *
+ * The same caveats apply: short animations might not have enough frames to satisfy all of the
+ * matchers, and overly specific calls (such as 0f, 1f, 2f, 3f, etc. for an animation from
+ * x = 0f to x = 100f) might fail as the animation only had frames at 0f, 25f, 50f, 75f, and
+ * 100f. As with [verifyAnimationUpdateFrames], failures will print a helpful log of all frames
+ * so you can see what caused the test to fail.
+ */
+ fun <T : Any> verifyAnimationUpdateFrames(
+ animator: PhysicsAnimator<T>,
+ property: FloatPropertyCompat<in T>,
+ startValue: Float,
+ firstTargetValue: Float,
+ vararg additionalTargetValues: Float
+ ) {
+ val matchers = ArrayList<UpdateMatcher>()
+
+ val values = ArrayList<Float>().also {
+ it.add(firstTargetValue)
+ it.addAll(additionalTargetValues.toList())
+ }
+
+ var prevVal = startValue
+ for (value in values) {
+ if (value > prevVal) {
+ matchers.add { update -> update.value >= value }
+ } else {
+ matchers.add { update -> update.value <= value }
+ }
+
+ prevVal = value
+ }
+
+ verifyAnimationUpdateFrames(
+ animator, property, matchers[0], *matchers.drop(0).toTypedArray())
+ }
+
+ /**
+ * Returns all of the values that have ever been reported to update listeners, per property.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun <T : Any> getAnimationUpdateFrames(animator: PhysicsAnimator<T>):
+ UpdateFramesPerProperty<T> {
+ return animatorTestHelpers[animator]?.getUpdates() as UpdateFramesPerProperty<T>
+ }
+
+ /**
+ * Clears animation frame updates from the given animator so they aren't used the next time its
+ * passed to [verifyAnimationUpdateFrames].
+ */
+ fun <T : Any> clearAnimationUpdateFrames(animator: PhysicsAnimator<T>) {
+ animatorTestHelpers[animator]?.clearUpdates()
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun <T> getAnimationTestHelper(animator: PhysicsAnimator<T>): AnimatorTestHelper<T> {
+ return animatorTestHelpers[animator] as AnimatorTestHelper<T>
+ }
+
+ /**
+ * Helper class for testing an animator. This replaces the animator's start action with
+ * [startForTest] and adds test listeners to enable other test utility behaviors. We build one
+ * these for each Animator and keep them around so we can access the updates.
+ */
+ class AnimatorTestHelper<T> (private val animator: PhysicsAnimator<T>) {
+
+ /** All updates received for each property animation. */
+ private val allUpdates =
+ ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>()
+
+ private val testEndListeners = ArrayList<PhysicsAnimator.EndListener<T>>()
+ private val testUpdateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>()
+
+ /** Whether we're currently in the middle of executing startInternal(). */
+ private var currentlyRunningStartInternal = false
+
+ init {
+ animator.startAction = ::startForTest
+ animator.cancelAction = ::cancelForTest
+ }
+
+ internal fun addTestEndListener(listener: PhysicsAnimator.EndListener<T>) {
+ testEndListeners.add(listener)
+ }
+
+ internal fun addTestUpdateListener(listener: PhysicsAnimator.UpdateListener<T>) {
+ testUpdateListeners.add(listener)
+ }
+
+ internal fun getUpdates(): UpdateFramesPerProperty<T> {
+ return allUpdates
+ }
+
+ internal fun clearUpdates() {
+ allUpdates.clear()
+ }
+
+ private fun startForTest() {
+ // The testable animator needs to block the main thread until super.start() has been
+ // called, since callers expect .start() to be synchronous but we're posting it to a
+ // handler here. We may also continue blocking until all animations end, if
+ // startBlocksUntilAnimationsEnd = true.
+ val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1)
+
+ animationThreadHandler.post {
+ // Add an update listener that dispatches to any test update listeners added by
+ // tests.
+ animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> {
+ override fun onAnimationUpdateForProperty(
+ target: T,
+ values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
+ ) {
+ values.forEach { (property, value) ->
+ allUpdates.getOrPut(property, { ArrayList() }).add(value)
+ }
+
+ for (listener in testUpdateListeners) {
+ listener.onAnimationUpdateForProperty(target, values)
+ }
+ }
+ })
+
+ // Add an end listener that dispatches to any test end listeners added by tests, and
+ // unblocks the main thread if required.
+ animator.addEndListener(object : PhysicsAnimator.EndListener<T> {
+ override fun onAnimationEnd(
+ target: T,
+ property: FloatPropertyCompat<in T>,
+ wasFling: Boolean,
+ canceled: Boolean,
+ finalValue: Float,
+ finalVelocity: Float,
+ allRelevantPropertyAnimsEnded: Boolean
+ ) {
+ for (listener in testEndListeners) {
+ listener.onAnimationEnd(
+ target, property, wasFling, canceled, finalValue, finalVelocity,
+ allRelevantPropertyAnimsEnded)
+ }
+
+ if (allRelevantPropertyAnimsEnded) {
+ testEndListeners.clear()
+ testUpdateListeners.clear()
+
+ if (startBlocksUntilAnimationsEnd) {
+ unblockLatch.countDown()
+ }
+ }
+ }
+ })
+
+ currentlyRunningStartInternal = true
+ animator.startInternal()
+ currentlyRunningStartInternal = false
+ unblockLatch.countDown()
+ }
+
+ unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS)
+ }
+
+ private fun cancelForTest(properties: Set<FloatPropertyCompat<in T>>) {
+ // If this was called from startInternal, we are already on the animation thread, and
+ // should just call cancelInternal rather than posting it. If we post it, the
+ // cancellation will occur after the rest of startInternal() and we'll immediately
+ // cancel the animation we worked so hard to start!
+ if (currentlyRunningStartInternal) {
+ animator.cancelInternal(properties)
+ return
+ }
+
+ val unblockLatch = CountDownLatch(1)
+
+ animationThreadHandler.post {
+ animator.cancelInternal(properties)
+ unblockLatch.countDown()
+ }
+
+ unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS)
+ }
+ }
+}
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
new file mode 100644
index 000000000000..96b9f86673fc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java
@@ -0,0 +1,61 @@
+/*
+ * 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/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
new file mode 100644
index 000000000000..976fba52b9e2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
@@ -0,0 +1,62 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Circular view with a semitransparent, circular background with an 'X' inside it.
+ *
+ * This is used by both Bubbles and PIP as the dismiss target.
+ */
+public class DismissCircleView extends FrameLayout {
+
+ private final ImageView mIconView = new ImageView(getContext());
+
+ public DismissCircleView(Context context) {
+ super(context);
+ final Resources res = getResources();
+
+ setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
+
+ mIconView.setImageDrawable(res.getDrawable(R.drawable.pip_ic_close_white));
+ addView(mIconView);
+
+ setViewSizes();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ setViewSizes();
+ }
+
+ /** Retrieves the current dimensions for the icon and circle and applies them. */
+ private void setViewSizes() {
+ final Resources res = getResources();
+ final int iconSize = res.getDimensionPixelSize(R.dimen.dismiss_target_x_size);
+ mIconView.setLayoutParams(
+ new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER));
+ }
+}
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
new file mode 100644
index 000000000000..3263f79888d6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2019 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 android.os.Handler;
+import android.os.RemoteException;
+import android.view.IDisplayWindowRotationCallback;
+import android.view.IDisplayWindowRotationController;
+import android.view.IWindowManager;
+import android.window.WindowContainerTransaction;
+
+import java.util.ArrayList;
+
+/**
+ * This module deals with display rotations coming from WM. When WM starts a rotation: after it has
+ * frozen the screen, it will call into this class. This will then call all registered local
+ * controllers and give them a chance to queue up task changes to be applied synchronously with that
+ * rotation.
+ */
+public class DisplayChangeController {
+
+ private final Handler mHandler;
+ private final IWindowManager mWmService;
+
+ 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;
+ try {
+ mWmService.setDisplayWindowRotationController(mDisplayRotationController);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to register rotation controller");
+ }
+ }
+
+ /**
+ * Adds a display rotation controller.
+ */
+ public void addRotationListener(OnDisplayChangingListener listener) {
+ synchronized (mRotationListener) {
+ mRotationListener.add(listener);
+ }
+ }
+
+ /**
+ * Removes a display rotation controller.
+ */
+ public void removeRotationListener(OnDisplayChangingListener listener) {
+ synchronized (mRotationListener) {
+ mRotationListener.remove(listener);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public interface OnDisplayChangingListener {
+ /**
+ * Called before the display is rotated. Contents of this method must run synchronously.
+ * @param displayId Id of display that is rotating.
+ * @param fromRotation starting rotation of the display.
+ * @param toRotation target rotation of the display (after rotating).
+ * @param t A task transaction to populate.
+ */
+ void onRotateDisplay(int displayId, int fromRotation, int toRotation,
+ WindowContainerTransaction t);
+ }
+}
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
new file mode 100644
index 000000000000..418973204add
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2019 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 android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.IDisplayWindowListener;
+import android.view.IWindowManager;
+
+import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener;
+
+import java.util.ArrayList;
+
+/**
+ * This module deals with display rotations coming from WM. When WM starts a rotation: after it has
+ * frozen the screen, it will call into this class. This will then call all registered local
+ * controllers and give them a chance to queue up task changes to be applied synchronously with that
+ * rotation.
+ */
+public class DisplayController {
+ private static final String TAG = "DisplayController";
+
+ private final Handler mHandler;
+ private final Context mContext;
+ private final IWindowManager mWmService;
+ private final DisplayChangeController mChangeController;
+
+ private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>();
+ private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
+
+ /**
+ * Gets a display by id from DisplayManager.
+ */
+ public Display getDisplay(int displayId) {
+ final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+ 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);
+ try {
+ mWmService.registerDisplayWindowListener(mDisplayContainerListener);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to register hierarchy listener");
+ }
+ }
+
+ /**
+ * Gets the DisplayLayout associated with a display.
+ */
+ public @Nullable DisplayLayout getDisplayLayout(int displayId) {
+ final DisplayRecord r = mDisplays.get(displayId);
+ return r != null ? r.mDisplayLayout : null;
+ }
+
+ /**
+ * Gets a display-specific context for a display.
+ */
+ public @Nullable Context getDisplayContext(int displayId) {
+ final DisplayRecord r = mDisplays.get(displayId);
+ return r != null ? r.mContext : null;
+ }
+
+ /**
+ * Add a display window-container listener. It will get notified whenever a display's
+ * configuration changes or when displays are added/removed from the WM hierarchy.
+ */
+ public void addDisplayWindowListener(OnDisplaysChangedListener listener) {
+ synchronized (mDisplays) {
+ if (mDisplayChangedListeners.contains(listener)) {
+ return;
+ }
+ mDisplayChangedListeners.add(listener);
+ for (int i = 0; i < mDisplays.size(); ++i) {
+ listener.onDisplayAdded(mDisplays.keyAt(i));
+ }
+ }
+ }
+
+ /**
+ * Remove a display window-container listener.
+ */
+ public void removeDisplayWindowListener(OnDisplaysChangedListener listener) {
+ synchronized (mDisplays) {
+ mDisplayChangedListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds a display rotation controller.
+ */
+ public void addDisplayChangingController(OnDisplayChangingListener controller) {
+ mChangeController.addRotationListener(controller);
+ }
+
+ /**
+ * Removes a display rotation controller.
+ */
+ public void removeDisplayChangingController(OnDisplayChangingListener controller) {
+ mChangeController.removeRotationListener(controller);
+ }
+
+ private static class DisplayRecord {
+ int mDisplayId;
+ Context mContext;
+ DisplayLayout mDisplayLayout;
+ }
+
+ /**
+ * Gets notified when a display is added/removed to the WM hierarchy and when a display's
+ * window-configuration changes.
+ *
+ * @see IDisplayWindowListener
+ */
+ public interface OnDisplaysChangedListener {
+ /**
+ * Called when a display has been added to the WM hierarchy.
+ */
+ default void onDisplayAdded(int displayId) {}
+
+ /**
+ * Called when a display's window-container configuration changes.
+ */
+ default void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {}
+
+ /**
+ * Called when a display is removed.
+ */
+ default void onDisplayRemoved(int displayId) {}
+
+ /**
+ * Called when fixed rotation on a display is started.
+ */
+ default void onFixedRotationStarted(int displayId, int newRotation) {}
+
+ /**
+ * Called when fixed rotation on a display is finished.
+ */
+ default void onFixedRotationFinished(int displayId) {}
+ }
+}
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
new file mode 100644
index 000000000000..ea18a19c2ee5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (C) 2019 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 android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+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;
+import android.util.SparseArray;
+import android.view.IDisplayWindowInsetsController;
+import android.view.IWindowManager;
+import android.view.InsetsSource;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.WindowInsets;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.internal.view.IInputMethodManager;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * Manages IME control at the display-level. This occurs when IME comes up in multi-window mode.
+ */
+public class DisplayImeController implements DisplayController.OnDisplaysChangedListener {
+ private static final String TAG = "DisplayImeController";
+
+ private static final boolean DEBUG = false;
+
+ // NOTE: All these constants came from InsetsController.
+ public static final int ANIMATION_DURATION_SHOW_MS = 275;
+ public static final int ANIMATION_DURATION_HIDE_MS = 340;
+ public static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+ private static final int DIRECTION_NONE = 0;
+ private static final int DIRECTION_SHOW = 1;
+ private static final int DIRECTION_HIDE = 2;
+ private static final int FLOATING_IME_BOTTOM_INSET = -80;
+
+ protected final IWindowManager mWmService;
+ protected final Executor mExecutor;
+ private final TransactionPool mTransactionPool;
+ private final DisplayController mDisplayController;
+ private final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>();
+ private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>();
+
+
+ public DisplayImeController(IWindowManager wmService, DisplayController displayController,
+ Executor mainExecutor, TransactionPool transactionPool) {
+ mExecutor = mainExecutor;
+ mWmService = wmService;
+ mTransactionPool = transactionPool;
+ mDisplayController = displayController;
+ }
+
+ /** Starts monitor displays changes and set insets controller for each displays. */
+ public void startMonitorDisplays() {
+ mDisplayController.addDisplayWindowListener(this);
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ // Add's a system-ui window-manager specifically for ime. This type is special because
+ // WM will defer IME inset handling to it in multi-window scenarious.
+ PerDisplay pd = new PerDisplay(displayId,
+ mDisplayController.getDisplayLayout(displayId).rotation());
+ try {
+ mWmService.setDisplayWindowInsetsController(displayId, pd);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Unable to set insets controller on display " + displayId);
+ }
+ mImePerDisplay.put(displayId, pd);
+ }
+
+ @Override
+ public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+ PerDisplay pd = mImePerDisplay.get(displayId);
+ if (pd == null) {
+ return;
+ }
+ if (mDisplayController.getDisplayLayout(displayId).rotation()
+ != pd.mRotation && isImeShowing(displayId)) {
+ pd.startAnimation(true, false /* forceRestart */);
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ try {
+ mWmService.setDisplayWindowInsetsController(displayId, null);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Unable to remove insets controller on display " + displayId);
+ }
+ mImePerDisplay.remove(displayId);
+ }
+
+ private boolean isImeShowing(int displayId) {
+ PerDisplay pd = mImePerDisplay.get(displayId);
+ if (pd == null) {
+ return false;
+ }
+ final InsetsSource imeSource = pd.mInsetsState.getSource(InsetsState.ITYPE_IME);
+ return imeSource != null && pd.mImeSourceControl != null && imeSource.isVisible();
+ }
+
+ private void dispatchPositionChanged(int displayId, int imeTop,
+ SurfaceControl.Transaction t) {
+ synchronized (mPositionProcessors) {
+ for (ImePositionProcessor pp : mPositionProcessors) {
+ pp.onImePositionChanged(displayId, imeTop, t);
+ }
+ }
+ }
+
+ @ImePositionProcessor.ImeAnimationFlags
+ private int dispatchStartPositioning(int displayId, int hiddenTop, int shownTop,
+ boolean show, boolean isFloating, SurfaceControl.Transaction t) {
+ synchronized (mPositionProcessors) {
+ int flags = 0;
+ for (ImePositionProcessor pp : mPositionProcessors) {
+ flags |= pp.onImeStartPositioning(
+ displayId, hiddenTop, shownTop, show, isFloating, t);
+ }
+ return flags;
+ }
+ }
+
+ private void dispatchEndPositioning(int displayId, boolean cancel,
+ SurfaceControl.Transaction t) {
+ synchronized (mPositionProcessors) {
+ for (ImePositionProcessor pp : mPositionProcessors) {
+ pp.onImeEndPositioning(displayId, cancel, t);
+ }
+ }
+ }
+
+ /**
+ * Adds an {@link ImePositionProcessor} to be called during ime position updates.
+ */
+ public void addPositionProcessor(ImePositionProcessor processor) {
+ synchronized (mPositionProcessors) {
+ if (mPositionProcessors.contains(processor)) {
+ return;
+ }
+ mPositionProcessors.add(processor);
+ }
+ }
+
+ /**
+ * Removes an {@link ImePositionProcessor} to be called during ime position updates.
+ */
+ public void removePositionProcessor(ImePositionProcessor processor) {
+ synchronized (mPositionProcessors) {
+ mPositionProcessors.remove(processor);
+ }
+ }
+
+ /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */
+ public class PerDisplay extends IDisplayWindowInsetsController.Stub {
+ final int mDisplayId;
+ final InsetsState mInsetsState = new InsetsState();
+ InsetsSourceControl mImeSourceControl = null;
+ int mAnimationDirection = DIRECTION_NONE;
+ ValueAnimator mAnimation = null;
+ int mRotation = Surface.ROTATION_0;
+ boolean mImeShowing = false;
+ final Rect mImeFrame = new Rect();
+ boolean mAnimateAlpha = true;
+
+ public PerDisplay(int displayId, int initialRotation) {
+ mDisplayId = displayId;
+ mRotation = initialRotation;
+ }
+
+ @Override
+ public void insetsChanged(InsetsState insetsState) {
+ mExecutor.execute(() -> {
+ if (mInsetsState.equals(insetsState)) {
+ return;
+ }
+
+ final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME);
+ final Rect newFrame = newSource.getFrame();
+ final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame();
+
+ mInsetsState.set(insetsState, true /* copySources */);
+ if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) {
+ if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation");
+ startAnimation(mImeShowing, true /* forceRestart */);
+ }
+ });
+ }
+
+ @Override
+ public void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls) {
+ insetsChanged(insetsState);
+ if (activeControls != null) {
+ for (InsetsSourceControl activeControl : activeControls) {
+ if (activeControl == null) {
+ continue;
+ }
+ if (activeControl.getType() == InsetsState.ITYPE_IME) {
+ mExecutor.execute(() -> {
+ final Point lastSurfacePosition = mImeSourceControl != null
+ ? mImeSourceControl.getSurfacePosition() : null;
+ final boolean positionChanged =
+ !activeControl.getSurfacePosition().equals(lastSurfacePosition);
+ final boolean leashChanged =
+ !haveSameLeash(mImeSourceControl, activeControl);
+ mImeSourceControl = activeControl;
+ if (mAnimation != null) {
+ if (positionChanged) {
+ startAnimation(mImeShowing, true /* forceRestart */);
+ }
+ } else {
+ if (leashChanged) {
+ applyVisibilityToLeash();
+ }
+ if (!mImeShowing) {
+ removeImeSurface();
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+
+ private void applyVisibilityToLeash() {
+ SurfaceControl leash = mImeSourceControl.getLeash();
+ if (leash != null) {
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ if (mImeShowing) {
+ t.show(leash);
+ } else {
+ t.hide(leash);
+ }
+ t.apply();
+ mTransactionPool.release(t);
+ }
+ }
+
+ @Override
+ public void showInsets(int types, boolean fromIme) {
+ if ((types & WindowInsets.Type.ime()) == 0) {
+ return;
+ }
+ if (DEBUG) Slog.d(TAG, "Got showInsets for ime");
+ mExecutor.execute(() -> startAnimation(true /* show */, false /* forceRestart */));
+ }
+
+ @Override
+ public void hideInsets(int types, boolean fromIme) {
+ if ((types & WindowInsets.Type.ime()) == 0) {
+ return;
+ }
+ if (DEBUG) Slog.d(TAG, "Got hideInsets for ime");
+ mExecutor.execute(() -> startAnimation(false /* show */, false /* forceRestart */));
+ }
+
+ @Override
+ public void topFocusedWindowChanged(String packageName) {
+ // no-op
+ }
+
+ /**
+ * Sends the local visibility state back to window manager. Needed for legacy adjustForIme.
+ */
+ private void setVisibleDirectly(boolean visible) {
+ mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible);
+ try {
+ mWmService.modifyDisplayWindowInsets(mDisplayId, mInsetsState);
+ } catch (RemoteException e) {
+ }
+ }
+
+ private int imeTop(float surfaceOffset) {
+ return mImeFrame.top + (int) surfaceOffset;
+ }
+
+ private boolean calcIsFloating(InsetsSource imeSource) {
+ final Rect frame = imeSource.getFrame();
+ if (frame.height() == 0) {
+ return true;
+ }
+ // Some Floating Input Methods will still report a frame, but the frame is actually
+ // a nav-bar inset created by WM and not part of the IME (despite being reported as
+ // an IME inset). For now, we assume that no non-floating IME will be <= this nav bar
+ // frame height so any reported frame that is <= nav-bar frame height is assumed to
+ // be floating.
+ return frame.height() <= mDisplayController.getDisplayLayout(mDisplayId)
+ .navBarFrameHeight();
+ }
+
+ private void startAnimation(final boolean show, final boolean forceRestart) {
+ final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME);
+ if (imeSource == null || mImeSourceControl == null) {
+ return;
+ }
+ final Rect newFrame = imeSource.getFrame();
+ final boolean isFloating = calcIsFloating(imeSource) && show;
+ if (isFloating) {
+ // This is a "floating" or "expanded" IME, so to get animations, just
+ // pretend the ime has some size just below the screen.
+ mImeFrame.set(newFrame);
+ final int floatingInset = (int) (mDisplayController.getDisplayLayout(mDisplayId)
+ .density() * FLOATING_IME_BOTTOM_INSET);
+ mImeFrame.bottom -= floatingInset;
+ } else if (newFrame.height() != 0) {
+ // Don't set a new frame if it's empty and hiding -- this maintains continuity
+ mImeFrame.set(newFrame);
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Run startAnim show:" + show + " was:"
+ + (mAnimationDirection == DIRECTION_SHOW ? "SHOW"
+ : (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE")));
+ }
+ if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show)
+ || (mAnimationDirection == DIRECTION_HIDE && !show)) {
+ return;
+ }
+ boolean seek = false;
+ float seekValue = 0;
+ if (mAnimation != null) {
+ if (mAnimation.isRunning()) {
+ seekValue = (float) mAnimation.getAnimatedValue();
+ seek = true;
+ }
+ mAnimation.cancel();
+ }
+ final float defaultY = mImeSourceControl.getSurfacePosition().y;
+ final float x = mImeSourceControl.getSurfacePosition().x;
+ final float hiddenY = defaultY + mImeFrame.height();
+ final float shownY = defaultY;
+ final float startY = show ? hiddenY : shownY;
+ final float endY = show ? shownY : hiddenY;
+ if (mAnimationDirection == DIRECTION_NONE && mImeShowing && show) {
+ // IME is already showing, so set seek to end
+ seekValue = shownY;
+ seek = true;
+ }
+ mAnimationDirection = show ? DIRECTION_SHOW : DIRECTION_HIDE;
+ mImeShowing = show;
+ mAnimation = ValueAnimator.ofFloat(startY, endY);
+ mAnimation.setDuration(
+ show ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS);
+ if (seek) {
+ mAnimation.setCurrentFraction((seekValue - startY) / (endY - startY));
+ }
+
+ mAnimation.addUpdateListener(animation -> {
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ float value = (float) animation.getAnimatedValue();
+ t.setPosition(mImeSourceControl.getLeash(), x, value);
+ final float alpha = (mAnimateAlpha || isFloating)
+ ? (value - hiddenY) / (shownY - hiddenY) : 1.f;
+ t.setAlpha(mImeSourceControl.getLeash(), alpha);
+ dispatchPositionChanged(mDisplayId, imeTop(value), t);
+ t.apply();
+ mTransactionPool.release(t);
+ });
+ mAnimation.setInterpolator(INTERPOLATOR);
+ mAnimation.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled = false;
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ t.setPosition(mImeSourceControl.getLeash(), x, startY);
+ if (DEBUG) {
+ Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:"
+ + imeTop(hiddenY) + "->" + imeTop(shownY)
+ + " showing:" + (mAnimationDirection == DIRECTION_SHOW));
+ }
+ int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY),
+ imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t);
+ mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0;
+ final float alpha = (mAnimateAlpha || isFloating)
+ ? (startY - hiddenY) / (shownY - hiddenY)
+ : 1.f;
+ t.setAlpha(mImeSourceControl.getLeash(), alpha);
+ if (mAnimationDirection == DIRECTION_SHOW) {
+ t.show(mImeSourceControl.getLeash());
+ }
+ t.apply();
+ mTransactionPool.release(t);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled);
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ if (!mCancelled) {
+ t.setPosition(mImeSourceControl.getLeash(), x, endY);
+ t.setAlpha(mImeSourceControl.getLeash(), 1.f);
+ }
+ dispatchEndPositioning(mDisplayId, mCancelled, t);
+ if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
+ t.hide(mImeSourceControl.getLeash());
+ removeImeSurface();
+ }
+ t.apply();
+ mTransactionPool.release(t);
+
+ mAnimationDirection = DIRECTION_NONE;
+ mAnimation = null;
+ }
+ });
+ if (!show) {
+ // When going away, queue up insets change first, otherwise any bounds changes
+ // can have a "flicker" of ime-provided insets.
+ setVisibleDirectly(false /* visible */);
+ }
+ mAnimation.start();
+ if (show) {
+ // When showing away, queue up insets change last, otherwise any bounds changes
+ // can have a "flicker" of ime-provided insets.
+ setVisibleDirectly(true /* visible */);
+ }
+ }
+ }
+
+ void removeImeSurface() {
+ final IInputMethodManager imms = getImms();
+ if (imms != null) {
+ try {
+ // Remove the IME surface to make the insets invisible for
+ // non-client controlled insets.
+ imms.removeImeSurface();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to remove IME surface.", e);
+ }
+ }
+ }
+
+ /**
+ * Allows other things to synchronize with the ime position
+ */
+ public interface ImePositionProcessor {
+ /**
+ * Indicates that ime shouldn't animate alpha. It will always be opaque. Used when stuff
+ * behind the IME shouldn't be visible (for example during split-screen adjustment where
+ * there is nothing behind the ime).
+ */
+ int IME_ANIMATION_NO_ALPHA = 1;
+
+ /** @hide */
+ @IntDef(prefix = {"IME_ANIMATION_"}, value = {
+ IME_ANIMATION_NO_ALPHA,
+ })
+ @interface ImeAnimationFlags {
+ }
+
+ /**
+ * Called when the IME position is starting to animate.
+ *
+ * @param hiddenTop The y position of the top of the IME surface when it is hidden.
+ * @param shownTop The y position of the top of the IME surface when it is shown.
+ * @param showing {@code true} when we are animating from hidden to shown, {@code false}
+ * when animating from shown to hidden.
+ * @param isFloating {@code true} when the ime is a floating ime (doesn't inset).
+ * @return flags that may alter how ime itself is animated (eg. no-alpha).
+ */
+ @ImeAnimationFlags
+ default int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
+ boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
+ return 0;
+ }
+
+ /**
+ * Called when the ime position changed. This is expected to be a synchronous call on the
+ * animation thread. Operations can be added to the transaction to be applied in sync.
+ *
+ * @param imeTop The current y position of the top of the IME surface.
+ */
+ default void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
+ }
+
+ /**
+ * Called when the IME position is done animating.
+ *
+ * @param cancel {@code true} if this was cancelled. This implies another start is coming.
+ */
+ default void onImeEndPositioning(int displayId, boolean cancel,
+ SurfaceControl.Transaction t) {
+ }
+ }
+
+ public IInputMethodManager getImms() {
+ return IInputMethodManager.Stub.asInterface(
+ ServiceManager.getService(Context.INPUT_METHOD_SERVICE));
+ }
+
+ private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ if (a.getLeash() == b.getLeash()) {
+ return true;
+ }
+ if (a.getLeash() == null || b.getLeash() == null) {
+ return false;
+ }
+ return a.getLeash().isSameSurface(b.getLeash());
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
new file mode 100644
index 000000000000..3181dbf74ace
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (C) 2019 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.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
+import static android.os.Process.SYSTEM_UID;
+import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
+import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.RotationUtils;
+import android.util.Size;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.Surface;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Contains information about the layout-properties of a display. This refers to internal layout
+ * like insets/cutout/rotation. In general, this can be thought of as the shell analog to
+ * DisplayPolicy.
+ */
+public class DisplayLayout {
+ @IntDef(prefix = { "NAV_BAR_" }, value = {
+ NAV_BAR_LEFT,
+ NAV_BAR_RIGHT,
+ NAV_BAR_BOTTOM,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NavBarPosition {}
+
+ // Navigation bar position values
+ public static final int NAV_BAR_LEFT = 1 << 0;
+ public static final int NAV_BAR_RIGHT = 1 << 1;
+ public static final int NAV_BAR_BOTTOM = 1 << 2;
+
+ private int mUiMode;
+ private int mWidth;
+ private int mHeight;
+ private DisplayCutout mCutout;
+ private int mRotation;
+ private int mDensityDpi;
+ private final Rect mNonDecorInsets = new Rect();
+ private final Rect mStableInsets = new Rect();
+ private boolean mHasNavigationBar = false;
+ private boolean mHasStatusBar = false;
+ private int mNavBarFrameHeight = 0;
+
+ /**
+ * Create empty layout.
+ */
+ public DisplayLayout() {
+ }
+
+ /**
+ * Construct a custom display layout using a DisplayInfo.
+ * @param info
+ * @param res
+ */
+ public DisplayLayout(DisplayInfo info, Resources res, boolean hasNavigationBar,
+ boolean hasStatusBar) {
+ init(info, res, hasNavigationBar, hasStatusBar);
+ }
+
+ /**
+ * Construct a display layout based on a live display.
+ * @param context Used for resources.
+ */
+ public DisplayLayout(@NonNull Context context, @NonNull Display rawDisplay) {
+ final int displayId = rawDisplay.getDisplayId();
+ DisplayInfo info = new DisplayInfo();
+ rawDisplay.getDisplayInfo(info);
+ init(info, context.getResources(), hasNavigationBar(info, context, displayId),
+ hasStatusBar(displayId));
+ }
+
+ public DisplayLayout(DisplayLayout dl) {
+ set(dl);
+ }
+
+ /** sets this DisplayLayout to a copy of another on. */
+ public void set(DisplayLayout dl) {
+ mUiMode = dl.mUiMode;
+ mWidth = dl.mWidth;
+ mHeight = dl.mHeight;
+ mCutout = dl.mCutout;
+ mRotation = dl.mRotation;
+ mDensityDpi = dl.mDensityDpi;
+ mHasNavigationBar = dl.mHasNavigationBar;
+ mHasStatusBar = dl.mHasStatusBar;
+ mNonDecorInsets.set(dl.mNonDecorInsets);
+ mStableInsets.set(dl.mStableInsets);
+ }
+
+ private void init(DisplayInfo info, Resources res, boolean hasNavigationBar,
+ boolean hasStatusBar) {
+ mUiMode = res.getConfiguration().uiMode;
+ mWidth = info.logicalWidth;
+ mHeight = info.logicalHeight;
+ mRotation = info.rotation;
+ mCutout = info.displayCutout;
+ mDensityDpi = info.logicalDensityDpi;
+ mHasNavigationBar = hasNavigationBar;
+ mHasStatusBar = hasStatusBar;
+ recalcInsets(res);
+ }
+
+ private void recalcInsets(Resources res) {
+ computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets,
+ mHasNavigationBar);
+ mStableInsets.set(mNonDecorInsets);
+ if (mHasStatusBar) {
+ convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar);
+ }
+ mNavBarFrameHeight = getNavigationBarFrameHeight(res, mWidth > mHeight);
+ }
+
+ /**
+ * Apply a rotation to this layout and its parameters.
+ * @param res
+ * @param targetRotation
+ */
+ public void rotateTo(Resources res, @Surface.Rotation int targetRotation) {
+ final int rotationDelta = (targetRotation - mRotation + 4) % 4;
+ final boolean changeOrient = (rotationDelta % 2) != 0;
+
+ final int origWidth = mWidth;
+ final int origHeight = mHeight;
+
+ mRotation = targetRotation;
+ if (changeOrient) {
+ mWidth = origHeight;
+ mHeight = origWidth;
+ }
+
+ if (mCutout != null && !mCutout.isEmpty()) {
+ mCutout = calculateDisplayCutoutForRotation(mCutout, rotationDelta, origWidth,
+ origHeight);
+ }
+
+ recalcInsets(res);
+ }
+
+ /** Get this layout's non-decor insets. */
+ public Rect nonDecorInsets() {
+ return mNonDecorInsets;
+ }
+
+ /** Get this layout's stable insets. */
+ public Rect stableInsets() {
+ return mStableInsets;
+ }
+
+ /** Get this layout's width. */
+ public int width() {
+ return mWidth;
+ }
+
+ /** Get this layout's height. */
+ public int height() {
+ return mHeight;
+ }
+
+ /** Get this layout's display rotation. */
+ public int rotation() {
+ return mRotation;
+ }
+
+ /** Get this layout's display density. */
+ public int densityDpi() {
+ return mDensityDpi;
+ }
+
+ /** Get the density scale for the display. */
+ public float density() {
+ return mDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ }
+
+ /** Get whether this layout is landscape. */
+ public boolean isLandscape() {
+ return mWidth > mHeight;
+ }
+
+ /** Get the navbar frame height (used by ime). */
+ public int navBarFrameHeight() {
+ return mNavBarFrameHeight;
+ }
+
+ /** Gets the orientation of this layout */
+ public int getOrientation() {
+ return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
+ }
+
+ /** Gets the calculated stable-bounds for this layout */
+ public void getStableBounds(Rect outBounds) {
+ outBounds.set(0, 0, mWidth, mHeight);
+ outBounds.inset(mStableInsets);
+ }
+
+ /**
+ * Gets navigation bar position for this layout
+ * @return Navigation bar position for this layout.
+ */
+ public @NavBarPosition int getNavigationBarPosition(Resources res) {
+ return navigationBarPosition(res, mWidth, mHeight, mRotation);
+ }
+
+ /**
+ * Rotates bounds as if parentBounds and bounds are a group. The group is rotated by `delta`
+ * 90-degree counter-clockwise increments. This assumes that parentBounds is at 0,0 and
+ * remains at 0,0 after rotation.
+ *
+ * Only 'bounds' is mutated.
+ */
+ public static void rotateBounds(Rect inOutBounds, Rect parentBounds, int delta) {
+ int rdelta = ((delta % 4) + 4) % 4;
+ int origLeft = inOutBounds.left;
+ switch (rdelta) {
+ case 0:
+ return;
+ case 1:
+ inOutBounds.left = inOutBounds.top;
+ inOutBounds.top = parentBounds.right - inOutBounds.right;
+ inOutBounds.right = inOutBounds.bottom;
+ inOutBounds.bottom = parentBounds.right - origLeft;
+ return;
+ case 2:
+ inOutBounds.left = parentBounds.right - inOutBounds.right;
+ inOutBounds.right = parentBounds.right - origLeft;
+ return;
+ case 3:
+ inOutBounds.left = parentBounds.bottom - inOutBounds.bottom;
+ inOutBounds.bottom = inOutBounds.right;
+ inOutBounds.right = parentBounds.bottom - inOutBounds.top;
+ inOutBounds.top = origLeft;
+ return;
+ }
+ }
+
+ /**
+ * Calculates the stable insets if we already have the non-decor insets.
+ */
+ private static void convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets,
+ int displayWidth, int displayHeight, boolean hasStatusBar) {
+ if (!hasStatusBar) {
+ return;
+ }
+ int statusBarHeight = getStatusBarHeight(displayWidth > displayHeight, res);
+ inOutInsets.top = Math.max(inOutInsets.top, statusBarHeight);
+ }
+
+ /**
+ * Calculates the insets for the areas that could never be removed in Honeycomb, i.e. system
+ * bar or button bar.
+ *
+ * @param displayRotation the current display rotation
+ * @param displayWidth the current display width
+ * @param displayHeight the current display height
+ * @param displayCutout the current display cutout
+ * @param outInsets the insets to return
+ */
+ static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth,
+ int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
+ boolean hasNavigationBar) {
+ outInsets.setEmpty();
+
+ // Only navigation bar
+ if (hasNavigationBar) {
+ int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
+ int navBarSize =
+ getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode);
+ if (position == NAV_BAR_BOTTOM) {
+ outInsets.bottom = navBarSize;
+ } else if (position == NAV_BAR_RIGHT) {
+ outInsets.right = navBarSize;
+ } else if (position == NAV_BAR_LEFT) {
+ outInsets.left = navBarSize;
+ }
+ }
+
+ if (displayCutout != null) {
+ outInsets.left += displayCutout.getSafeInsetLeft();
+ outInsets.top += displayCutout.getSafeInsetTop();
+ outInsets.right += displayCutout.getSafeInsetRight();
+ outInsets.bottom += displayCutout.getSafeInsetBottom();
+ }
+ }
+
+ /**
+ * Calculates the stable insets without running a layout.
+ *
+ * @param displayRotation the current display rotation
+ * @param displayWidth the current display width
+ * @param displayHeight the current display height
+ * @param displayCutout the current display cutout
+ * @param outInsets the insets to return
+ */
+ static void computeStableInsets(Resources res, int displayRotation, int displayWidth,
+ int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
+ boolean hasNavigationBar, boolean hasStatusBar) {
+ outInsets.setEmpty();
+
+ // Navigation bar and status bar.
+ computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout,
+ uiMode, outInsets, hasNavigationBar);
+ convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight,
+ hasStatusBar);
+ }
+
+ /** Retrieve the statusbar height from resources. */
+ static int getStatusBarHeight(boolean landscape, Resources res) {
+ return landscape ? res.getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height_landscape)
+ : res.getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height_portrait);
+ }
+
+ /** Calculate the DisplayCutout for a particular display size/rotation. */
+ public static DisplayCutout calculateDisplayCutoutForRotation(
+ DisplayCutout cutout, int rotation, int displayWidth, int displayHeight) {
+ if (cutout == null || cutout == DisplayCutout.NO_CUTOUT) {
+ return null;
+ }
+ final Insets waterfallInsets =
+ RotationUtils.rotateInsets(cutout.getWaterfallInsets(), rotation);
+ if (rotation == ROTATION_0) {
+ return computeSafeInsets(cutout, displayWidth, displayHeight);
+ }
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ Rect[] cutoutRects = cutout.getBoundingRectsAll();
+ final Rect[] newBounds = new Rect[cutoutRects.length];
+ final Rect displayBounds = new Rect(0, 0, displayWidth, displayHeight);
+ for (int i = 0; i < cutoutRects.length; ++i) {
+ final Rect rect = new Rect(cutoutRects[i]);
+ if (!rect.isEmpty()) {
+ rotateBounds(rect, displayBounds, rotation);
+ }
+ newBounds[getBoundIndexFromRotation(i, rotation)] = rect;
+ }
+ return computeSafeInsets(
+ DisplayCutout.fromBoundsAndWaterfall(newBounds, waterfallInsets),
+ rotated ? displayHeight : displayWidth,
+ rotated ? displayWidth : displayHeight);
+ }
+
+ private static int getBoundIndexFromRotation(int index, int rotation) {
+ return (index - rotation) < 0
+ ? index - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH
+ : index - rotation;
+ }
+
+ /** Calculate safe insets. */
+ public static DisplayCutout computeSafeInsets(DisplayCutout inner,
+ int displayWidth, int displayHeight) {
+ if (inner == DisplayCutout.NO_CUTOUT) {
+ return null;
+ }
+
+ final Size displaySize = new Size(displayWidth, displayHeight);
+ final Rect safeInsets = computeSafeInsets(displaySize, inner);
+ return inner.replaceSafeInsets(safeInsets);
+ }
+
+ private static Rect computeSafeInsets(
+ Size displaySize, DisplayCutout cutout) {
+ if (displaySize.getWidth() == displaySize.getHeight()) {
+ throw new UnsupportedOperationException("not implemented: display=" + displaySize
+ + " cutout=" + cutout);
+ }
+
+ int leftInset = Math.max(cutout.getWaterfallInsets().left,
+ findCutoutInsetForSide(displaySize, cutout.getBoundingRectLeft(), Gravity.LEFT));
+ int topInset = Math.max(cutout.getWaterfallInsets().top,
+ findCutoutInsetForSide(displaySize, cutout.getBoundingRectTop(), Gravity.TOP));
+ int rightInset = Math.max(cutout.getWaterfallInsets().right,
+ findCutoutInsetForSide(displaySize, cutout.getBoundingRectRight(), Gravity.RIGHT));
+ int bottomInset = Math.max(cutout.getWaterfallInsets().bottom,
+ findCutoutInsetForSide(displaySize, cutout.getBoundingRectBottom(),
+ Gravity.BOTTOM));
+
+ return new Rect(leftInset, topInset, rightInset, bottomInset);
+ }
+
+ private static int findCutoutInsetForSide(Size display, Rect boundingRect, int gravity) {
+ if (boundingRect.isEmpty()) {
+ return 0;
+ }
+
+ int inset = 0;
+ switch (gravity) {
+ case Gravity.TOP:
+ return Math.max(inset, boundingRect.bottom);
+ case Gravity.BOTTOM:
+ return Math.max(inset, display.getHeight() - boundingRect.top);
+ case Gravity.LEFT:
+ return Math.max(inset, boundingRect.right);
+ case Gravity.RIGHT:
+ return Math.max(inset, display.getWidth() - boundingRect.left);
+ default:
+ throw new IllegalArgumentException("unknown gravity: " + gravity);
+ }
+ }
+
+ static boolean hasNavigationBar(DisplayInfo info, Context context, int displayId) {
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ // Allow a system property to override this. Used by the emulator.
+ final String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
+ if ("1".equals(navBarOverride)) {
+ return false;
+ } else if ("0".equals(navBarOverride)) {
+ return true;
+ }
+ return context.getResources().getBoolean(R.bool.config_showNavigationBar);
+ } else {
+ boolean isUntrustedVirtualDisplay = info.type == Display.TYPE_VIRTUAL
+ && info.ownerUid != SYSTEM_UID;
+ final ContentResolver resolver = context.getContentResolver();
+ boolean forceDesktopOnExternal = Settings.Global.getInt(resolver,
+ DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
+
+ return ((info.flags & FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0
+ || (forceDesktopOnExternal && !isUntrustedVirtualDisplay));
+ // TODO(b/142569966): make sure VR2D and DisplayWindowSettings are moved here somehow.
+ }
+ }
+
+ static boolean hasStatusBar(int displayId) {
+ return displayId == Display.DEFAULT_DISPLAY;
+ }
+
+ /** Retrieve navigation bar position from resources based on rotation and size. */
+ public static @NavBarPosition int navigationBarPosition(Resources res, int displayWidth,
+ int displayHeight, int rotation) {
+ boolean navBarCanMove = displayWidth != displayHeight && res.getBoolean(
+ com.android.internal.R.bool.config_navBarCanMove);
+ if (navBarCanMove && displayWidth > displayHeight) {
+ if (rotation == Surface.ROTATION_90) {
+ return NAV_BAR_RIGHT;
+ } else {
+ return NAV_BAR_LEFT;
+ }
+ }
+ return NAV_BAR_BOTTOM;
+ }
+
+ /** Retrieve navigation bar size from resources based on side/orientation/ui-mode */
+ public static int getNavigationBarSize(Resources res, int navBarSide, boolean landscape,
+ int uiMode) {
+ final boolean carMode = (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR;
+ if (carMode) {
+ if (navBarSide == NAV_BAR_BOTTOM) {
+ return res.getDimensionPixelSize(landscape
+ ? R.dimen.navigation_bar_height_landscape_car_mode
+ : R.dimen.navigation_bar_height_car_mode);
+ } else {
+ return res.getDimensionPixelSize(R.dimen.navigation_bar_width_car_mode);
+ }
+
+ } else {
+ if (navBarSide == NAV_BAR_BOTTOM) {
+ return res.getDimensionPixelSize(landscape
+ ? R.dimen.navigation_bar_height_landscape
+ : R.dimen.navigation_bar_height);
+ } else {
+ return res.getDimensionPixelSize(R.dimen.navigation_bar_width);
+ }
+ }
+ }
+
+ /** @see com.android.server.wm.DisplayPolicy#getNavigationBarFrameHeight */
+ public static int getNavigationBarFrameHeight(Resources res, boolean landscape) {
+ return res.getDimensionPixelSize(landscape
+ ? R.dimen.navigation_bar_frame_height_landscape
+ : R.dimen.navigation_bar_frame_height);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt
new file mode 100644
index 000000000000..d5d072a8d449
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt
@@ -0,0 +1,368 @@
+/*
+ * 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 android.graphics.Rect
+import android.util.Log
+import com.android.wm.shell.common.FloatingContentCoordinator.FloatingContent
+import java.util.HashMap
+
+/** Tag for debug logging. */
+private const val TAG = "FloatingCoordinator"
+
+/**
+ * Coordinates the positions and movement of floating content, such as PIP and Bubbles, to ensure
+ * that they don't overlap. If content does overlap due to content appearing or moving, the
+ * coordinator will ask content to move to resolve the conflict.
+ *
+ * After implementing [FloatingContent], content should call [onContentAdded] to begin coordination.
+ * Subsequently, call [onContentMoved] whenever the content moves, and the coordinator will move
+ * other content out of the way. [onContentRemoved] should be called when the content is removed or
+ * no longer visible.
+ */
+
+class FloatingContentCoordinator constructor() {
+ /**
+ * Represents a piece of floating content, such as PIP or the Bubbles stack. Provides methods
+ * that allow the [FloatingContentCoordinator] to determine the current location of the content,
+ * as well as the ability to ask it to move out of the way of other content.
+ *
+ * The default implementation of [calculateNewBoundsOnOverlap] moves the content up or down,
+ * depending on the position of the conflicting content. You can override this method if you
+ * want your own custom conflict resolution logic.
+ */
+ interface FloatingContent {
+
+ /**
+ * Return the bounds claimed by this content. This should include the bounds occupied by the
+ * content itself, as well as any padding, if desired. The coordinator will ensure that no
+ * other content is located within these bounds.
+ *
+ * If the content is animating, this method should return the bounds to which the content is
+ * animating. If that animation is cancelled, or updated, be sure that your implementation
+ * of this method returns the appropriate bounds, and call [onContentMoved] so that the
+ * coordinator moves other content out of the way.
+ */
+ fun getFloatingBoundsOnScreen(): Rect
+
+ /**
+ * Return the area within which this floating content is allowed to move. When resolving
+ * conflicts, the coordinator will never ask your content to move to a position where any
+ * part of the content would be out of these bounds.
+ */
+ fun getAllowedFloatingBoundsRegion(): Rect
+
+ /**
+ * Called when the coordinator needs this content to move to the given bounds. It's up to
+ * you how to do that.
+ *
+ * Note that if you start an animation to these bounds, [getFloatingBoundsOnScreen] should
+ * return the destination bounds, not the in-progress animated bounds. This is so the
+ * coordinator knows where floating content is going to be and can resolve conflicts
+ * accordingly.
+ */
+ fun moveToBounds(bounds: Rect)
+
+ /**
+ * Called by the coordinator when it needs to find a new home for this floating content,
+ * because a new or moving piece of content is now overlapping with it.
+ *
+ * [findAreaForContentVertically] and [findAreaForContentAboveOrBelow] are helpful utility
+ * functions that will find new bounds for your content automatically. Unless you require
+ * specific conflict resolution logic, these should be sufficient. By default, this method
+ * delegates to [findAreaForContentVertically].
+ *
+ * @param overlappingContentBounds The bounds of the other piece of content, which
+ * necessitated this content's relocation. Your new position must not overlap with these
+ * bounds.
+ * @param otherContentBounds The bounds of any other pieces of floating content. Your new
+ * position must not overlap with any of these either. These bounds are guaranteed to be
+ * non-overlapping.
+ * @return The new bounds for this content.
+ */
+ @JvmDefault
+ fun calculateNewBoundsOnOverlap(
+ overlappingContentBounds: Rect,
+ otherContentBounds: List<Rect>
+ ): Rect {
+ return findAreaForContentVertically(
+ getFloatingBoundsOnScreen(),
+ overlappingContentBounds,
+ otherContentBounds,
+ getAllowedFloatingBoundsRegion())
+ }
+ }
+
+ /** The bounds of all pieces of floating content added to the coordinator. */
+ private val allContentBounds: MutableMap<FloatingContent, Rect> = HashMap()
+
+ /**
+ * Whether we are currently resolving conflicts by asking content to move. If we are, we'll
+ * temporarily ignore calls to [onContentMoved] - those calls are from the content that is
+ * moving to new, conflict-free bounds, so we don't need to perform conflict detection
+ * calculations in response.
+ */
+ private var currentlyResolvingConflicts = false
+
+ /**
+ * Makes the coordinator aware of a new piece of floating content, and moves any existing
+ * content out of the way, if necessary.
+ *
+ * If you don't want your new content to move existing content, use [getOccupiedBounds] to find
+ * an unoccupied area, and move the content there before calling this method.
+ */
+ fun onContentAdded(newContent: FloatingContent) {
+ updateContentBounds()
+ allContentBounds[newContent] = newContent.getFloatingBoundsOnScreen()
+ maybeMoveConflictingContent(newContent)
+ }
+
+ /**
+ * Called to notify the coordinator that a piece of floating content has moved (or is animating)
+ * to a new position, and that any conflicting floating content should be moved out of the way.
+ *
+ * The coordinator will call [FloatingContent.getFloatingBoundsOnScreen] to find the new bounds
+ * for the moving content. If you're animating the content, be sure that your implementation of
+ * getFloatingBoundsOnScreen returns the bounds to which it's animating, not the content's
+ * current bounds.
+ *
+ * If the animation moving this content is cancelled or updated, you'll need to call this method
+ * again, to ensure that content is moved out of the way of the latest bounds.
+ *
+ * @param content The content that has moved.
+ */
+ fun onContentMoved(content: FloatingContent) {
+
+ // Ignore calls when we are currently resolving conflicts, since those calls are from
+ // content that is moving to new, conflict-free bounds.
+ if (currentlyResolvingConflicts) {
+ return
+ }
+
+ if (!allContentBounds.containsKey(content)) {
+ Log.wtf(TAG, "Received onContentMoved call before onContentAdded! " +
+ "This should never happen.")
+ return
+ }
+
+ updateContentBounds()
+ maybeMoveConflictingContent(content)
+ }
+
+ /**
+ * Called to notify the coordinator that a piece of floating content has been removed or is no
+ * longer visible.
+ */
+ fun onContentRemoved(removedContent: FloatingContent) {
+ allContentBounds.remove(removedContent)
+ }
+
+ /**
+ * Returns a set of Rects that represent the bounds of all of the floating content on the
+ * screen.
+ *
+ * [onContentAdded] will move existing content out of the way if the added content intersects
+ * existing content. That's fine - but if your specific starting position is not important, you
+ * can use this function to find unoccupied space for your content before calling
+ * [onContentAdded], so that moving existing content isn't necessary.
+ */
+ fun getOccupiedBounds(): Collection<Rect> {
+ return allContentBounds.values
+ }
+
+ /**
+ * Identifies any pieces of content that are now overlapping with the given content, and asks
+ * them to move out of the way.
+ */
+ private fun maybeMoveConflictingContent(fromContent: FloatingContent) {
+ currentlyResolvingConflicts = true
+
+ val conflictingNewBounds = allContentBounds[fromContent]!!
+ allContentBounds
+ // Filter to content that intersects with the new bounds. That's content that needs
+ // to move.
+ .filter { (content, bounds) ->
+ content != fromContent && Rect.intersects(conflictingNewBounds, bounds) }
+ // Tell that content to get out of the way, and save the bounds it says it's moving
+ // (or animating) to.
+ .forEach { (content, bounds) ->
+ val newBounds = content.calculateNewBoundsOnOverlap(
+ conflictingNewBounds,
+ // Pass all of the content bounds except the bounds of the
+ // content we're asking to move, and the conflicting new bounds
+ // (since those are passed separately).
+ otherContentBounds = allContentBounds.values
+ .minus(bounds)
+ .minus(conflictingNewBounds))
+
+ // If the new bounds are empty, it means there's no non-overlapping position
+ // that is in bounds. Just leave the content where it is. This should normally
+ // not happen, but sometimes content like PIP reports incorrect bounds
+ // temporarily.
+ if (!newBounds.isEmpty) {
+ content.moveToBounds(newBounds)
+ allContentBounds[content] = content.getFloatingBoundsOnScreen()
+ }
+ }
+
+ currentlyResolvingConflicts = false
+ }
+
+ /**
+ * Update [allContentBounds] by calling [FloatingContent.getFloatingBoundsOnScreen] for all
+ * content and saving the result.
+ */
+ private fun updateContentBounds() {
+ allContentBounds.keys.forEach { allContentBounds[it] = it.getFloatingBoundsOnScreen() }
+ }
+
+ companion object {
+ /**
+ * Finds new bounds for the given content, either above or below its current position. The
+ * new bounds won't intersect with the newly overlapping rect or the exclusion rects, and
+ * will be within the allowed bounds unless no possible position exists.
+ *
+ * You can use this method to help find a new position for your content when the coordinator
+ * calls [FloatingContent.moveToAreaExcluding].
+ *
+ * @param contentRect The bounds of the content for which we're finding a new home.
+ * @param newlyOverlappingRect The bounds of the content that forced this relocation by
+ * intersecting with the content we now need to move. If the overlapping content is
+ * overlapping the top half of this content, we'll try to move this content downward if
+ * possible (since the other content is 'pushing' it down), and vice versa.
+ * @param exclusionRects Any other areas that we need to avoid when finding a new home for
+ * the content. These areas must be non-overlapping with each other.
+ * @param allowedBounds The area within which we're allowed to find new bounds for the
+ * content.
+ * @return New bounds for the content that don't intersect the exclusion rects or the
+ * newly overlapping rect, and that is within bounds - or an empty Rect if no in-bounds
+ * position exists.
+ */
+ @JvmStatic
+ fun findAreaForContentVertically(
+ contentRect: Rect,
+ newlyOverlappingRect: Rect,
+ exclusionRects: Collection<Rect>,
+ allowedBounds: Rect
+ ): Rect {
+ // If the newly overlapping Rect's center is above the content's center, we'll prefer to
+ // find a space for this content that is below the overlapping content, since it's
+ // 'pushing' it down. This may not be possible due to to screen bounds, in which case
+ // we'll find space in the other direction.
+ val overlappingContentPushingDown =
+ newlyOverlappingRect.centerY() < contentRect.centerY()
+
+ // Filter to exclusion rects that are above or below the content that we're finding a
+ // place for. Then, split into two lists - rects above the content, and rects below it.
+ var (rectsToAvoidAbove, rectsToAvoidBelow) = exclusionRects
+ .filter { rectToAvoid -> rectsIntersectVertically(rectToAvoid, contentRect) }
+ .partition { rectToAvoid -> rectToAvoid.top < contentRect.top }
+
+ // Lazily calculate the closest possible new tops for the content, above and below its
+ // current location.
+ val newContentBoundsAbove by lazy {
+ findAreaForContentAboveOrBelow(
+ contentRect,
+ exclusionRects = rectsToAvoidAbove.plus(newlyOverlappingRect),
+ findAbove = true)
+ }
+ val newContentBoundsBelow by lazy {
+ findAreaForContentAboveOrBelow(
+ contentRect,
+ exclusionRects = rectsToAvoidBelow.plus(newlyOverlappingRect),
+ findAbove = false)
+ }
+
+ val positionAboveInBounds by lazy { allowedBounds.contains(newContentBoundsAbove) }
+ val positionBelowInBounds by lazy { allowedBounds.contains(newContentBoundsBelow) }
+
+ // Use the 'below' position if the content is being overlapped from the top, unless it's
+ // out of bounds. Also use it if the content is being overlapped from the bottom, but
+ // the 'above' position is out of bounds. Otherwise, use the 'above' position.
+ val usePositionBelow =
+ overlappingContentPushingDown && positionBelowInBounds ||
+ !overlappingContentPushingDown && !positionAboveInBounds
+
+ // Return the content rect, but offset to reflect the new position.
+ val newBounds = if (usePositionBelow) newContentBoundsBelow else newContentBoundsAbove
+
+ // If the new bounds are within the allowed bounds, return them. If not, it means that
+ // there are no legal new bounds. This can happen if the new content's bounds are too
+ // large (for example, full-screen PIP). Since there is no reasonable action to take
+ // here, return an empty Rect and we will just not move the content.
+ return if (allowedBounds.contains(newBounds)) newBounds else Rect()
+ }
+
+ /**
+ * Finds a new position for the given content, either above or below its current position
+ * depending on whether [findAbove] is true or false, respectively. This new position will
+ * not intersect with any of the [exclusionRects].
+ *
+ * This method is useful as a helper method for implementing your own conflict resolution
+ * logic. Otherwise, you'd want to use [findAreaForContentVertically], which takes screen
+ * bounds and conflicting bounds' location into account when deciding whether to move to new
+ * bounds above or below the current bounds.
+ *
+ * @param contentRect The content we're finding an area for.
+ * @param exclusionRects The areas we need to avoid when finding a new area for the content.
+ * These areas must be non-overlapping with each other.
+ * @param findAbove Whether we are finding an area above the content's current position,
+ * rather than an area below it.
+ */
+ fun findAreaForContentAboveOrBelow(
+ contentRect: Rect,
+ exclusionRects: Collection<Rect>,
+ findAbove: Boolean
+ ): Rect {
+ // Sort the rects, since we want to move the content as little as possible. We'll
+ // start with the rects closest to the content and move outward. If we're finding an
+ // area above the content, that means we sort in reverse order to search the rects
+ // from highest to lowest y-value.
+ val sortedExclusionRects =
+ exclusionRects.sortedBy { if (findAbove) -it.top else it.top }
+
+ val proposedNewBounds = Rect(contentRect)
+ for (exclusionRect in sortedExclusionRects) {
+ // If the proposed new bounds don't intersect with this exclusion rect, that
+ // means there's room for the content here. We know this because the rects are
+ // sorted and non-overlapping, so any subsequent exclusion rects would be higher
+ // (or lower) than this one and can't possibly intersect if this one doesn't.
+ if (!Rect.intersects(proposedNewBounds, exclusionRect)) {
+ break
+ } else {
+ // Otherwise, we need to keep searching for new bounds. If we're finding an
+ // area above, propose new bounds that place the content just above the
+ // exclusion rect. If we're finding an area below, propose new bounds that
+ // place the content just below the exclusion rect.
+ val verticalOffset =
+ if (findAbove) -contentRect.height() else exclusionRect.height()
+ proposedNewBounds.offsetTo(
+ proposedNewBounds.left,
+ exclusionRect.top + verticalOffset)
+ }
+ }
+
+ return proposedNewBounds
+ }
+
+ /** Returns whether or not the two Rects share any of the same space on the X axis. */
+ private fun rectsIntersectVertically(r1: Rect, r2: Rect): Boolean {
+ return (r1.left >= r2.left && r1.left <= r2.right) ||
+ (r1.right <= r2.right && r1.right >= r2.left)
+ }
+ }
+}
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
new file mode 100644
index 000000000000..cd75840b8c71
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java
@@ -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.
+ */
+
+package com.android.wm.shell.common;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+
+/** Executor implementation which is backed by a Handler. */
+public class HandlerExecutor implements ShellExecutor {
+ private final Handler mHandler;
+
+ public HandlerExecutor(@NonNull Handler handler) {
+ mHandler = handler;
+ }
+
+ @Override
+ public void executeDelayed(@NonNull Runnable r, long delayMillis) {
+ if (!mHandler.postDelayed(r, delayMillis)) {
+ throw new RuntimeException(mHandler + " is probably exiting");
+ }
+ }
+
+ @Override
+ 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
new file mode 100644
index 000000000000..aafe2407a1ea
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import java.util.concurrent.Executor;
+
+/**
+ * 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 {
+ /**
+ * See {@link android.os.Handler#postDelayed(Runnable, long)}.
+ */
+ void executeDelayed(Runnable r, long delayMillis);
+
+ /**
+ * See {@link android.os.Handler#removeCallbacks}.
+ */
+ void removeCallbacks(Runnable r);
+}
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
new file mode 100644
index 000000000000..9cb125087cd9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
@@ -0,0 +1,176 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.os.Handler;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransactionCallback;
+import android.window.WindowOrganizer;
+
+import java.util.ArrayList;
+
+/**
+ * Helper for serializing sync-transactions and corresponding callbacks.
+ */
+public final class SyncTransactionQueue {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "SyncTransactionQueue";
+
+ // Just a little longer than the sync-engine timeout of 5s
+ private static final int REPLY_TIMEOUT = 5300;
+
+ private final TransactionPool mTransactionPool;
+ private final Handler mHandler;
+
+ // Sync Transactions currently don't support nesting or interleaving properly, so
+ // queue up transactions to run them serially.
+ private final ArrayList<SyncCallback> mQueue = new ArrayList<>();
+
+ private SyncCallback mInFlight = null;
+ private final ArrayList<TransactionRunnable> mRunnables = new ArrayList<>();
+
+ private final Runnable mOnReplyTimeout = () -> {
+ synchronized (mQueue) {
+ if (mInFlight != null && mQueue.contains(mInFlight)) {
+ Slog.w(TAG, "Sync Transaction timed-out: " + mInFlight.mWCT);
+ mInFlight.onTransactionReady(mInFlight.mId, new SurfaceControl.Transaction());
+ }
+ }
+ };
+
+ public SyncTransactionQueue(TransactionPool pool, Handler handler) {
+ mTransactionPool = pool;
+ mHandler = handler;
+ }
+
+ /**
+ * Queues a sync transaction to be sent serially to WM.
+ */
+ public void queue(WindowContainerTransaction wct) {
+ SyncCallback cb = new SyncCallback(wct);
+ synchronized (mQueue) {
+ if (DEBUG) Slog.d(TAG, "Queueing up " + wct);
+ mQueue.add(cb);
+ if (mQueue.size() == 1) {
+ cb.send();
+ }
+ }
+ }
+
+ /**
+ * Queues a sync transaction only if there are already sync transaction(s) queued or in flight.
+ * Otherwise just returns without queueing.
+ * @return {@code true} if queued, {@code false} if not.
+ */
+ public boolean queueIfWaiting(WindowContainerTransaction wct) {
+ synchronized (mQueue) {
+ if (mQueue.isEmpty()) {
+ if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct);
+ return false;
+ }
+ if (DEBUG) Slog.d(TAG, "Queue is non-empty, so queueing up " + wct);
+ SyncCallback cb = new SyncCallback(wct);
+ mQueue.add(cb);
+ if (mQueue.size() == 1) {
+ cb.send();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Runs a runnable in sync with sync transactions (ie. when the current in-flight transaction
+ * returns. If there are no transactions in-flight, runnable executes immediately.
+ */
+ public void runInSync(TransactionRunnable runnable) {
+ synchronized (mQueue) {
+ if (DEBUG) Slog.d(TAG, "Run in sync. mInFlight=" + mInFlight);
+ if (mInFlight != null) {
+ mRunnables.add(runnable);
+ return;
+ }
+ }
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ runnable.runWithTransaction(t);
+ t.apply();
+ mTransactionPool.release(t);
+ }
+
+ // Synchronized on mQueue
+ private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) {
+ if (DEBUG) Slog.d(TAG, " Running " + mRunnables.size() + " sync runnables");
+ for (int i = 0, n = mRunnables.size(); i < n; ++i) {
+ mRunnables.get(i).runWithTransaction(t);
+ }
+ mRunnables.clear();
+ t.apply();
+ t.close();
+ }
+
+ /** Task to run with transaction. */
+ public interface TransactionRunnable {
+ /** Runs with transaction. */
+ void runWithTransaction(SurfaceControl.Transaction t);
+ }
+
+ private class SyncCallback extends WindowContainerTransactionCallback {
+ int mId = -1;
+ final WindowContainerTransaction mWCT;
+
+ SyncCallback(WindowContainerTransaction wct) {
+ mWCT = wct;
+ }
+
+ // Must be sychronized on mQueue
+ void send() {
+ if (mInFlight != null) {
+ throw new IllegalStateException("Sync Transactions must be serialized. In Flight: "
+ + mInFlight.mId + " - " + mInFlight.mWCT);
+ }
+ mInFlight = this;
+ if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT);
+ mId = new WindowOrganizer().applySyncTransaction(mWCT, this);
+ if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId);
+ mHandler.postDelayed(mOnReplyTimeout, REPLY_TIMEOUT);
+ }
+
+ @Override
+ public void onTransactionReady(int id,
+ @NonNull SurfaceControl.Transaction t) {
+ mHandler.post(() -> {
+ synchronized (mQueue) {
+ if (mId != id) {
+ Slog.e(TAG, "Got an unexpected onTransactionReady. Expected "
+ + mId + " but got " + id);
+ return;
+ }
+ mInFlight = null;
+ mHandler.removeCallbacks(mOnReplyTimeout);
+ if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId);
+ mQueue.remove(this);
+ onTransactionReceived(t);
+ if (!mQueue.isEmpty()) {
+ mQueue.get(0).send();
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
new file mode 100644
index 000000000000..24381d937e2f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2019 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.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Region;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.MergedConfiguration;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.DragEvent;
+import android.view.IScrollCaptureCallbacks;
+import android.view.IWindow;
+import android.view.IWindowManager;
+import android.view.IWindowSession;
+import android.view.IWindowSessionCallback;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.window.ClientWindowFrames;
+
+import com.android.internal.os.IResultReceiver;
+
+import java.util.HashMap;
+
+/**
+ * Represents the "windowing" layer of the WM Shell. This layer allows shell components to place and
+ * manipulate windows without talking to WindowManager.
+ */
+public class SystemWindows {
+ private static final String TAG = "SystemWindows";
+
+ private final SparseArray<PerDisplay> mPerDisplay = new SparseArray<>();
+ private final HashMap<View, SurfaceControlViewHost> mViewRoots = new HashMap<>();
+ private final DisplayController mDisplayController;
+ private final IWindowManager mWmService;
+ private IWindowSession mSession;
+
+ private final DisplayController.OnDisplaysChangedListener mDisplayListener =
+ new DisplayController.OnDisplaysChangedListener() {
+ @Override
+ public void onDisplayAdded(int displayId) { }
+
+ @Override
+ public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+ PerDisplay pd = mPerDisplay.get(displayId);
+ if (pd == null) {
+ return;
+ }
+ pd.updateConfiguration(newConfig);
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) { }
+ };
+
+ public SystemWindows(DisplayController displayController, IWindowManager wmService) {
+ mWmService = wmService;
+ mDisplayController = displayController;
+ mDisplayController.addDisplayWindowListener(mDisplayListener);
+ try {
+ mSession = wmService.openSession(
+ new IWindowSessionCallback.Stub() {
+ @Override
+ public void onAnimatorScaleChanged(float scale) {}
+ });
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to create layer", e);
+ }
+ }
+
+ /**
+ * Adds a view to system-ui window management.
+ */
+ public void addView(View view, WindowManager.LayoutParams attrs, int displayId,
+ int windowType) {
+ PerDisplay pd = mPerDisplay.get(displayId);
+ if (pd == null) {
+ pd = new PerDisplay(displayId);
+ mPerDisplay.put(displayId, pd);
+ }
+ pd.addView(view, attrs, windowType);
+ }
+
+ /**
+ * Removes a view from system-ui window management.
+ * @param view
+ */
+ public void removeView(View view) {
+ SurfaceControlViewHost root = mViewRoots.remove(view);
+ root.release();
+ }
+
+ /**
+ * Updates the layout params of a view.
+ */
+ public void updateViewLayout(@NonNull View view, ViewGroup.LayoutParams params) {
+ SurfaceControlViewHost root = mViewRoots.get(view);
+ if (root == null || !(params instanceof WindowManager.LayoutParams)) {
+ return;
+ }
+ view.setLayoutParams(params);
+ root.relayout((WindowManager.LayoutParams) params);
+ }
+
+ /**
+ * Sets the touchable region of a view's window. This will be cropped to the window size.
+ * @param view
+ * @param region
+ */
+ public void setTouchableRegion(@NonNull View view, Region region) {
+ SurfaceControlViewHost root = mViewRoots.get(view);
+ if (root == null) {
+ return;
+ }
+ WindowlessWindowManager wwm = root.getWindowlessWM();
+ if (!(wwm instanceof SysUiWindowManager)) {
+ return;
+ }
+ ((SysUiWindowManager) wwm).setTouchableRegionForWindow(view, region);
+ }
+
+ /**
+ * Adds a root for system-ui window management with no views. Only useful for IME.
+ */
+ public void addRoot(int displayId, int windowType) {
+ PerDisplay pd = mPerDisplay.get(displayId);
+ if (pd == null) {
+ pd = new PerDisplay(displayId);
+ mPerDisplay.put(displayId, pd);
+ }
+ pd.addRoot(windowType);
+ }
+
+ /**
+ * Get the IWindow token for a specific root.
+ *
+ * @param windowType A window type from {@link WindowManager}.
+ */
+ IWindow getWindow(int displayId, int windowType) {
+ PerDisplay pd = mPerDisplay.get(displayId);
+ if (pd == null) {
+ return null;
+ }
+ return pd.getWindow(windowType);
+ }
+
+ /**
+ * Gets the SurfaceControl associated with a root view. This is the same surface that backs the
+ * ViewRootImpl.
+ */
+ public SurfaceControl getViewSurface(View rootView) {
+ for (int i = 0; i < mPerDisplay.size(); ++i) {
+ for (int iWm = 0; iWm < mPerDisplay.valueAt(i).mWwms.size(); ++iWm) {
+ SurfaceControl out = mPerDisplay.valueAt(i).mWwms.valueAt(iWm)
+ .getSurfaceControlForWindow(rootView);
+ if (out != null) {
+ return out;
+ }
+ }
+ }
+ return null;
+ }
+
+ private class PerDisplay {
+ final int mDisplayId;
+ private final SparseArray<SysUiWindowManager> mWwms = new SparseArray<>();
+
+ PerDisplay(int displayId) {
+ mDisplayId = displayId;
+ }
+
+ public void addView(View view, WindowManager.LayoutParams attrs, int windowType) {
+ SysUiWindowManager wwm = addRoot(windowType);
+ if (wwm == null) {
+ Slog.e(TAG, "Unable to create systemui root");
+ return;
+ }
+ final Display display = mDisplayController.getDisplay(mDisplayId);
+ SurfaceControlViewHost viewRoot =
+ new SurfaceControlViewHost(
+ view.getContext(), display, wwm, true /* useSfChoreographer */);
+ attrs.flags |= FLAG_HARDWARE_ACCELERATED;
+ viewRoot.setView(view, attrs);
+ mViewRoots.put(view, viewRoot);
+
+ try {
+ mWmService.setShellRootAccessibilityWindow(mDisplayId, windowType,
+ viewRoot.getWindowToken());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error setting accessibility window for " + mDisplayId + ":"
+ + windowType, e);
+ }
+ }
+
+ SysUiWindowManager addRoot(int windowType) {
+ SysUiWindowManager wwm = mWwms.get(windowType);
+ if (wwm != null) {
+ return wwm;
+ }
+ SurfaceControl rootSurface = null;
+ ContainerWindow win = new ContainerWindow();
+ try {
+ rootSurface = mWmService.addShellRoot(mDisplayId, win, windowType);
+ } catch (RemoteException e) {
+ }
+ if (rootSurface == null) {
+ Slog.e(TAG, "Unable to get root surfacecontrol for systemui");
+ return null;
+ }
+ Context displayContext = mDisplayController.getDisplayContext(mDisplayId);
+ wwm = new SysUiWindowManager(mDisplayId, displayContext, rootSurface, win);
+ mWwms.put(windowType, wwm);
+ return wwm;
+ }
+
+ IWindow getWindow(int windowType) {
+ SysUiWindowManager wwm = mWwms.get(windowType);
+ if (wwm == null) {
+ return null;
+ }
+ return wwm.mContainerWindow;
+ }
+
+ void updateConfiguration(Configuration configuration) {
+ for (int i = 0; i < mWwms.size(); ++i) {
+ mWwms.valueAt(i).updateConfiguration(configuration);
+ }
+ }
+ }
+
+ /**
+ * A subclass of WindowlessWindowManager that provides insets to its viewroots.
+ */
+ public class SysUiWindowManager extends WindowlessWindowManager {
+ final int mDisplayId;
+ ContainerWindow mContainerWindow;
+ public SysUiWindowManager(int displayId, Context ctx, SurfaceControl rootSurface,
+ ContainerWindow container) {
+ super(ctx.getResources().getConfiguration(), rootSurface, null /* hostInputToken */);
+ mContainerWindow = container;
+ mDisplayId = displayId;
+ }
+
+ void updateConfiguration(Configuration configuration) {
+ setConfiguration(configuration);
+ }
+
+ SurfaceControl getSurfaceControlForWindow(View rootView) {
+ return getSurfaceControl(rootView);
+ }
+
+ void setTouchableRegionForWindow(View rootView, Region region) {
+ IBinder token = rootView.getWindowToken();
+ if (token == null) {
+ return;
+ }
+ setTouchRegion(token, region);
+ }
+ }
+
+ static class ContainerWindow extends IWindow.Stub {
+ ContainerWindow() {}
+
+ @Override
+ public void resized(ClientWindowFrames frames, boolean reportDraw,
+ MergedConfiguration newMergedConfiguration, boolean forceLayout,
+ boolean alwaysConsumeSystemBars, int displayId) {}
+
+ @Override
+ public void locationInParentDisplayChanged(Point offset) {}
+
+ @Override
+ public void insetsChanged(InsetsState insetsState) {}
+
+ @Override
+ public void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls) {}
+
+ @Override
+ public void showInsets(int types, boolean fromIme) {}
+
+ @Override
+ public void hideInsets(int types, boolean fromIme) {}
+
+ @Override
+ public void moved(int newX, int newY) {}
+
+ @Override
+ public void dispatchAppVisibility(boolean visible) {}
+
+ @Override
+ public void dispatchGetNewSurface() {}
+
+ @Override
+ public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {}
+
+ @Override
+ public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {}
+
+ @Override
+ public void closeSystemDialogs(String reason) {}
+
+ @Override
+ public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep,
+ float zoom, boolean sync) {}
+
+ @Override
+ public void dispatchWallpaperCommand(String action, int x, int y,
+ int z, Bundle extras, boolean sync) {}
+
+ /* Drag/drop */
+ @Override
+ public void dispatchDragEvent(DragEvent event) {}
+
+ @Override
+ public void updatePointerIcon(float x, float y) {}
+
+ @Override
+ public void dispatchWindowShown() {}
+
+ @Override
+ public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {}
+
+ @Override
+ public void dispatchPointerCaptureChanged(boolean hasCapture) {}
+
+ @Override
+ public void requestScrollCapture(IScrollCaptureCallbacks callbacks) {
+ try {
+ callbacks.onUnavailable();
+ } catch (RemoteException ex) {
+ // ignore
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java
new file mode 100644
index 000000000000..4c34566b0d98
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java
@@ -0,0 +1,50 @@
+/*
+ * 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 android.util.Pools;
+import android.view.SurfaceControl;
+
+/**
+ * Provides a synchronized pool of {@link SurfaceControl.Transaction}s to minimize allocations.
+ */
+public class TransactionPool {
+ private final Pools.SynchronizedPool<SurfaceControl.Transaction> mTransactionPool =
+ new Pools.SynchronizedPool<>(4);
+
+ public TransactionPool() {
+ }
+
+ /** Gets a transaction from the pool. */
+ public SurfaceControl.Transaction acquire() {
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ if (t == null) {
+ return new SurfaceControl.Transaction();
+ }
+ return t;
+ }
+
+ /**
+ * Return a transaction to the pool. DO NOT call {@link SurfaceControl.Transaction#close()} if
+ * returning to pool.
+ */
+ public void release(SurfaceControl.Transaction t) {
+ if (!mTransactionPool.release(t)) {
+ t.close();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
new file mode 100644
index 000000000000..b4d738712893
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
@@ -0,0 +1,699 @@
+/*
+ * 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.magnetictarget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.database.ContentObserver
+import android.graphics.PointF
+import android.os.Handler
+import android.os.UserHandle
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.provider.Settings
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.View
+import android.view.ViewConfiguration
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.wm.shell.animation.PhysicsAnimator
+import kotlin.math.abs
+import kotlin.math.hypot
+
+/**
+ * Utility class for creating 'magnetized' objects that are attracted to one or more magnetic
+ * targets. Magnetic targets attract objects that are dragged near them, and hold them there unless
+ * they're moved away or released. Releasing objects inside a magnetic target typically performs an
+ * action on the object.
+ *
+ * MagnetizedObject also supports flinging to targets, which will result in the object being pulled
+ * into the target and released as if it was dragged into it.
+ *
+ * To use this class, either construct an instance with an object of arbitrary type, or use the
+ * [MagnetizedObject.magnetizeView] shortcut method if you're magnetizing a view. Then, set
+ * [magnetListener] to receive event callbacks. In your touch handler, pass all MotionEvents
+ * that move this object to [maybeConsumeMotionEvent]. If that method returns true, consider the
+ * event consumed by the MagnetizedObject and don't move the object unless it begins returning false
+ * again.
+ *
+ * @param context Context, used to retrieve a Vibrator instance for vibration effects.
+ * @param underlyingObject The actual object that we're magnetizing.
+ * @param xProperty Property that sets the x value of the object's position.
+ * @param yProperty Property that sets the y value of the object's position.
+ */
+abstract class MagnetizedObject<T : Any>(
+ val context: Context,
+
+ /** The actual object that is animated. */
+ val underlyingObject: T,
+
+ /** Property that gets/sets the object's X value. */
+ val xProperty: FloatPropertyCompat<in T>,
+
+ /** Property that gets/sets the object's Y value. */
+ val yProperty: FloatPropertyCompat<in T>
+) {
+
+ /** Return the width of the object. */
+ abstract fun getWidth(underlyingObject: T): Float
+
+ /** Return the height of the object. */
+ abstract fun getHeight(underlyingObject: T): Float
+
+ /**
+ * Fill the provided array with the location of the top-left of the object, relative to the
+ * entire screen. Compare to [View.getLocationOnScreen].
+ */
+ abstract fun getLocationOnScreen(underlyingObject: T, loc: IntArray)
+
+ /** Methods for listening to events involving a magnetized object. */
+ interface MagnetListener {
+
+ /**
+ * Called when touch events move within the magnetic field of a target, causing the
+ * object to animate to the target and become 'stuck' there. The animation happens
+ * automatically here - you should not move the object. You can, however, change its state
+ * to indicate to the user that it's inside the target and releasing it will have an effect.
+ *
+ * [maybeConsumeMotionEvent] is now returning true and will continue to do so until a call
+ * to [onUnstuckFromTarget] or [onReleasedInTarget].
+ *
+ * @param target The target that the object is now stuck to.
+ */
+ fun onStuckToTarget(target: MagneticTarget)
+
+ /**
+ * Called when the object is no longer stuck to a target. This means that either touch
+ * events moved outside of the magnetic field radius, or that a forceful fling out of the
+ * target was detected.
+ *
+ * The object won't be automatically animated out of the target, since you're responsible
+ * for moving the object again. You should move it (or animate it) using your own
+ * movement/animation logic.
+ *
+ * Reverse any effects applied in [onStuckToTarget] here.
+ *
+ * If [wasFlungOut] is true, [maybeConsumeMotionEvent] returned true for the ACTION_UP event
+ * that concluded the fling. If [wasFlungOut] is false, that means a drag gesture is ongoing
+ * and [maybeConsumeMotionEvent] is now returning false.
+ *
+ * @param target The target that this object was just unstuck from.
+ * @param velX The X velocity of the touch gesture when it exited the magnetic field.
+ * @param velY The Y velocity of the touch gesture when it exited the magnetic field.
+ * @param wasFlungOut Whether the object was unstuck via a fling gesture. This means that
+ * an ACTION_UP event was received, and that the gesture velocity was sufficient to conclude
+ * that the user wants to un-stick the object despite no touch events occurring outside of
+ * the magnetic field radius.
+ */
+ fun onUnstuckFromTarget(
+ target: MagneticTarget,
+ velX: Float,
+ velY: Float,
+ wasFlungOut: Boolean
+ )
+
+ /**
+ * Called when the object is released inside a target, or flung towards it with enough
+ * velocity to reach it.
+ *
+ * @param target The target that the object was released in.
+ */
+ fun onReleasedInTarget(target: MagneticTarget)
+ }
+
+ private val animator: PhysicsAnimator<T> = PhysicsAnimator.getInstance(underlyingObject)
+ private val objectLocationOnScreen = IntArray(2)
+
+ /**
+ * Targets that have been added to this object. These will all be considered when determining
+ * magnetic fields and fling trajectories.
+ */
+ private val associatedTargets = ArrayList<MagneticTarget>()
+
+ private val velocityTracker: VelocityTracker = VelocityTracker.obtain()
+ private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+
+ private var touchDown = PointF()
+ private var touchSlop = 0
+ private var movedBeyondSlop = false
+
+ /** Whether touch events are presently occurring within the magnetic field area of a target. */
+ val objectStuckToTarget: Boolean
+ get() = targetObjectIsStuckTo != null
+
+ /** The target the object is stuck to, or null if the object is not stuck to any target. */
+ private var targetObjectIsStuckTo: MagneticTarget? = null
+
+ /**
+ * Sets the listener to receive events. This must be set, or [maybeConsumeMotionEvent]
+ * will always return false and no magnetic effects will occur.
+ */
+ lateinit var magnetListener: MagnetizedObject.MagnetListener
+
+ /**
+ * Optional update listener to provide to the PhysicsAnimator that is used to spring the object
+ * into the target.
+ */
+ var physicsAnimatorUpdateListener: PhysicsAnimator.UpdateListener<T>? = null
+
+ /**
+ * Optional end listener to provide to the PhysicsAnimator that is used to spring the object
+ * into the target.
+ */
+ var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null
+
+ /**
+ * Method that is called when the object should be animated stuck to the target. The default
+ * implementation uses the object's x and y properties to animate the object centered inside the
+ * target. You can override this if you need custom animation.
+ *
+ * The method is invoked with the MagneticTarget that the object is sticking to, the X and Y
+ * velocities of the gesture that brought the object into the magnetic radius, whether or not it
+ * was flung, and a callback you must call after your animation completes.
+ */
+ var animateStuckToTarget: (MagneticTarget, Float, Float, Boolean, (() -> Unit)?) -> Unit =
+ ::animateStuckToTargetInternal
+
+ /**
+ * Sets whether forcefully flinging the object vertically towards a target causes it to be
+ * attracted to the target and then released immediately, despite never being dragged within the
+ * magnetic field.
+ */
+ var flingToTargetEnabled = true
+
+ /**
+ * If fling to target is enabled, forcefully flinging the object towards a target will cause
+ * it to be attracted to the target and then released immediately, despite never being dragged
+ * within the magnetic field.
+ *
+ * This sets the width of the area considered 'near' enough a target to be considered a fling,
+ * in terms of percent of the target view's width. For example, setting this to 3f means that
+ * flings towards a 100px-wide target will be considered 'near' enough if they're towards the
+ * 300px-wide area around the target.
+ *
+ * Flings whose trajectory intersects the area will be attracted and released - even if the
+ * target view itself isn't intersected:
+ *
+ * | |
+ * | 0 |
+ * | / |
+ * | / |
+ * | X / |
+ * |.....###.....|
+ *
+ *
+ * Flings towards the target whose trajectories do not intersect the area will be treated as
+ * normal flings and the magnet will leave the object alone:
+ *
+ * | |
+ * | |
+ * | 0 |
+ * | / |
+ * | / X |
+ * |.....###.....|
+ *
+ */
+ var flingToTargetWidthPercent = 3f
+
+ /**
+ * Sets the minimum velocity (in pixels per second) required to fling an object to the target
+ * without dragging it into the magnetic field.
+ */
+ var flingToTargetMinVelocity = 4000f
+
+ /**
+ * Sets the minimum velocity (in pixels per second) required to fling un-stuck an object stuck
+ * to the target. If this velocity is reached, the object will be freed even if it wasn't moved
+ * outside the magnetic field radius.
+ */
+ var flingUnstuckFromTargetMinVelocity = 4000f
+
+ /**
+ * Sets the maximum X velocity above which the object will not stick to the target. Even if the
+ * object is dragged through the magnetic field, it will not stick to the target until the
+ * horizontal velocity is below this value.
+ */
+ var stickToTargetMaxXVelocity = 2000f
+
+ /**
+ * Enable or disable haptic vibration effects when the object interacts with the magnetic field.
+ *
+ * If you're experiencing crashes when the object enters targets, ensure that you have the
+ * android.permission.VIBRATE permission!
+ */
+ var hapticsEnabled = true
+
+ /** Default spring configuration to use for animating the object into a target. */
+ var springConfig = PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY)
+
+ /**
+ * Spring configuration to use to spring the object into a target specifically when it's flung
+ * towards (rather than dragged near) it.
+ */
+ var flungIntoTargetSpringConfig = springConfig
+
+ init {
+ initHapticSettingObserver(context)
+ }
+
+ /**
+ * Adds the provided MagneticTarget to this object. The object will now be attracted to the
+ * target if it strays within its magnetic field or is flung towards it.
+ *
+ * If this target (or its magnetic field) overlaps another target added to this object, the
+ * prior target will take priority.
+ */
+ fun addTarget(target: MagneticTarget) {
+ associatedTargets.add(target)
+ target.updateLocationOnScreen()
+ }
+
+ /**
+ * Shortcut that accepts a View and a magnetic field radius and adds it as a magnetic target.
+ *
+ * @return The MagneticTarget instance for the given View. This can be used to change the
+ * target's magnetic field radius after it's been added. It can also be added to other
+ * magnetized objects.
+ */
+ fun addTarget(target: View, magneticFieldRadiusPx: Int): MagneticTarget {
+ return MagneticTarget(target, magneticFieldRadiusPx).also { addTarget(it) }
+ }
+
+ /**
+ * Removes the given target from this object. The target will no longer attract the object.
+ */
+ fun removeTarget(target: MagneticTarget) {
+ associatedTargets.remove(target)
+ }
+
+ /**
+ * Provide this method with all motion events that move the magnetized object. If the
+ * location of the motion events moves within the magnetic field of a target, or indicate a
+ * fling-to-target gesture, this method will return true and you should not move the object
+ * yourself until it returns false again.
+ *
+ * Note that even when this method returns true, you should continue to pass along new motion
+ * events so that we know when the events move back outside the magnetic field area.
+ *
+ * This method will always return false if you haven't set a [magnetListener].
+ */
+ fun maybeConsumeMotionEvent(ev: MotionEvent): Boolean {
+ // Short-circuit if we don't have a listener or any targets, since those are required.
+ if (associatedTargets.size == 0) {
+ return false
+ }
+
+ // When a gesture begins, recalculate target views' positions on the screen in case they
+ // have changed. Also, clear state.
+ if (ev.action == MotionEvent.ACTION_DOWN) {
+ updateTargetViews()
+
+ // Clear the velocity tracker and stuck target.
+ velocityTracker.clear()
+ targetObjectIsStuckTo = null
+
+ // Set the touch down coordinates and reset movedBeyondSlop.
+ touchDown.set(ev.rawX, ev.rawY)
+ movedBeyondSlop = false
+ }
+
+ // Always pass events to the VelocityTracker.
+ addMovement(ev)
+
+ // If we haven't yet moved beyond the slop distance, check if we have.
+ if (!movedBeyondSlop) {
+ val dragDistance = hypot(ev.rawX - touchDown.x, ev.rawY - touchDown.y)
+ if (dragDistance > touchSlop) {
+ // If we're beyond the slop distance, save that and continue.
+ movedBeyondSlop = true
+ } else {
+ // Otherwise, don't do anything yet.
+ return false
+ }
+ }
+
+ val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target ->
+ val distanceFromTargetCenter = hypot(
+ ev.rawX - target.centerOnScreen.x,
+ ev.rawY - target.centerOnScreen.y)
+ distanceFromTargetCenter < target.magneticFieldRadiusPx
+ }
+
+ // If we aren't currently stuck to a target, and we're in the magnetic field of a target,
+ // we're newly stuck.
+ val objectNewlyStuckToTarget =
+ !objectStuckToTarget && targetObjectIsInMagneticFieldOf != null
+
+ // If we are currently stuck to a target, we're in the magnetic field of a target, and that
+ // target isn't the one we're currently stuck to, then touch events have moved into a
+ // adjacent target's magnetic field.
+ val objectMovedIntoDifferentTarget =
+ objectStuckToTarget &&
+ targetObjectIsInMagneticFieldOf != null &&
+ targetObjectIsStuckTo != targetObjectIsInMagneticFieldOf
+
+ if (objectNewlyStuckToTarget || objectMovedIntoDifferentTarget) {
+ velocityTracker.computeCurrentVelocity(1000)
+ val velX = velocityTracker.xVelocity
+ val velY = velocityTracker.yVelocity
+
+ // If the object is moving too quickly within the magnetic field, do not stick it. This
+ // only applies to objects newly stuck to a target. If the object is moved into a new
+ // target, it wasn't moving at all (since it was stuck to the previous one).
+ if (objectNewlyStuckToTarget && abs(velX) > stickToTargetMaxXVelocity) {
+ return false
+ }
+
+ // This touch event is newly within the magnetic field - let the listener know, and
+ // animate sticking to the magnet.
+ targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf
+ cancelAnimations()
+ magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!)
+ animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false, null)
+
+ vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
+ } else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) {
+ velocityTracker.computeCurrentVelocity(1000)
+
+ // This touch event is newly outside the magnetic field - let the listener know. It will
+ // move the object out of the target using its own movement logic.
+ cancelAnimations()
+ magnetListener.onUnstuckFromTarget(
+ targetObjectIsStuckTo!!, velocityTracker.xVelocity, velocityTracker.yVelocity,
+ wasFlungOut = false)
+ targetObjectIsStuckTo = null
+
+ vibrateIfEnabled(VibrationEffect.EFFECT_TICK)
+ }
+
+ // First, check for relevant gestures concluding with an ACTION_UP.
+ if (ev.action == MotionEvent.ACTION_UP) {
+
+ velocityTracker.computeCurrentVelocity(1000 /* units */)
+ val velX = velocityTracker.xVelocity
+ val velY = velocityTracker.yVelocity
+
+ // Cancel the magnetic animation since we might still be springing into the magnetic
+ // target, but we're about to fling away or release.
+ cancelAnimations()
+
+ if (objectStuckToTarget) {
+ if (-velY > flingUnstuckFromTargetMinVelocity) {
+ // If the object is stuck, but it was forcefully flung away from the target in
+ // the upward direction, tell the listener so the object can be animated out of
+ // the target.
+ magnetListener.onUnstuckFromTarget(
+ targetObjectIsStuckTo!!, velX, velY, wasFlungOut = true)
+ } else {
+ // If the object is stuck and not flung away, it was released inside the target.
+ magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!)
+ vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
+ }
+
+ // Either way, we're no longer stuck.
+ targetObjectIsStuckTo = null
+ return true
+ }
+
+ // The target we're flinging towards, or null if we're not flinging towards any target.
+ val flungToTarget = associatedTargets.firstOrNull { target ->
+ isForcefulFlingTowardsTarget(target, ev.rawX, ev.rawY, velX, velY)
+ }
+
+ if (flungToTarget != null) {
+ // If this is a fling-to-target, animate the object to the magnet and then release
+ // it.
+ magnetListener.onStuckToTarget(flungToTarget)
+ targetObjectIsStuckTo = flungToTarget
+
+ animateStuckToTarget(flungToTarget, velX, velY, true) {
+ magnetListener.onReleasedInTarget(flungToTarget)
+ targetObjectIsStuckTo = null
+ vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
+ }
+
+ return true
+ }
+
+ // If it's not either of those things, we are not interested.
+ return false
+ }
+
+ return objectStuckToTarget // Always consume touch events if the object is stuck.
+ }
+
+ /** Plays the given vibration effect if haptics are enabled. */
+ @SuppressLint("MissingPermission")
+ private fun vibrateIfEnabled(effect: Int) {
+ if (hapticsEnabled && systemHapticsEnabled) {
+ vibrator.vibrate(effect.toLong())
+ }
+ }
+
+ /** Adds the movement to the velocity tracker using raw coordinates. */
+ private fun addMovement(event: MotionEvent) {
+ // Add movement to velocity tracker using raw screen X and Y coordinates instead
+ // of window coordinates because the window frame may be moving at the same time.
+ val deltaX = event.rawX - event.x
+ val deltaY = event.rawY - event.y
+ event.offsetLocation(deltaX, deltaY)
+ velocityTracker.addMovement(event)
+ event.offsetLocation(-deltaX, -deltaY)
+ }
+
+ /** Animates sticking the object to the provided target with the given start velocities. */
+ private fun animateStuckToTargetInternal(
+ target: MagneticTarget,
+ velX: Float,
+ velY: Float,
+ flung: Boolean,
+ after: (() -> Unit)? = null
+ ) {
+ target.updateLocationOnScreen()
+ getLocationOnScreen(underlyingObject, objectLocationOnScreen)
+
+ // Calculate the difference between the target's center coordinates and the object's.
+ // Animating the object's x/y properties by these values will center the object on top
+ // of the magnetic target.
+ val xDiff = target.centerOnScreen.x -
+ getWidth(underlyingObject) / 2f - objectLocationOnScreen[0]
+ val yDiff = target.centerOnScreen.y -
+ getHeight(underlyingObject) / 2f - objectLocationOnScreen[1]
+
+ val springConfig = if (flung) flungIntoTargetSpringConfig else springConfig
+
+ cancelAnimations()
+
+ // Animate to the center of the target.
+ animator
+ .spring(xProperty, xProperty.getValue(underlyingObject) + xDiff, velX,
+ springConfig)
+ .spring(yProperty, yProperty.getValue(underlyingObject) + yDiff, velY,
+ springConfig)
+
+ if (physicsAnimatorUpdateListener != null) {
+ animator.addUpdateListener(physicsAnimatorUpdateListener!!)
+ }
+
+ if (physicsAnimatorEndListener != null) {
+ animator.addEndListener(physicsAnimatorEndListener!!)
+ }
+
+ if (after != null) {
+ animator.withEndActions(after)
+ }
+
+ animator.start()
+ }
+
+ /**
+ * Whether or not the provided values match a 'fast fling' towards the provided target. If it
+ * does, we consider it a fling-to-target gesture.
+ */
+ private fun isForcefulFlingTowardsTarget(
+ target: MagneticTarget,
+ rawX: Float,
+ rawY: Float,
+ velX: Float,
+ velY: Float
+ ): Boolean {
+ if (!flingToTargetEnabled) {
+ return false
+ }
+
+ // Whether velocity is sufficient, depending on whether we're flinging into a target at the
+ // top or the bottom of the screen.
+ val velocitySufficient =
+ if (rawY < target.centerOnScreen.y) velY > flingToTargetMinVelocity
+ else velY < flingToTargetMinVelocity
+
+ if (!velocitySufficient) {
+ return false
+ }
+
+ // Whether the trajectory of the fling intersects the target area.
+ var targetCenterXIntercept = rawX
+
+ // Only do math if the X velocity is non-zero, otherwise X won't change.
+ if (velX != 0f) {
+ // Rise over run...
+ val slope = velY / velX
+ // ...y = mx + b, b = y / mx...
+ val yIntercept = rawY - slope * rawX
+
+ // ...calculate the x value when y = the target's y-coordinate.
+ targetCenterXIntercept = (target.centerOnScreen.y - yIntercept) / slope
+ }
+
+ // The width of the area we're looking for a fling towards.
+ val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent
+
+ // Velocity was sufficient, so return true if the intercept is within the target area.
+ return targetCenterXIntercept > target.centerOnScreen.x - targetAreaWidth / 2 &&
+ targetCenterXIntercept < target.centerOnScreen.x + targetAreaWidth / 2
+ }
+
+ /** Cancel animations on this object's x/y properties. */
+ internal fun cancelAnimations() {
+ animator.cancel(xProperty, yProperty)
+ }
+
+ /** Updates the locations on screen of all of the [associatedTargets]. */
+ internal fun updateTargetViews() {
+ associatedTargets.forEach { it.updateLocationOnScreen() }
+
+ // Update the touch slop, since the configuration may have changed.
+ if (associatedTargets.size > 0) {
+ touchSlop =
+ ViewConfiguration.get(associatedTargets[0].targetView.context).scaledTouchSlop
+ }
+ }
+
+ /**
+ * Represents a target view with a magnetic field radius and cached center-on-screen
+ * coordinates.
+ *
+ * Instances of MagneticTarget are passed to a MagnetizedObject's [addTarget], and can then
+ * attract the object if it's dragged near or flung towards it. MagneticTargets can be added to
+ * multiple objects.
+ */
+ class MagneticTarget(
+ val targetView: View,
+ var magneticFieldRadiusPx: Int
+ ) {
+ val centerOnScreen = PointF()
+
+ private val tempLoc = IntArray(2)
+
+ fun updateLocationOnScreen() {
+ targetView.post {
+ targetView.getLocationOnScreen(tempLoc)
+
+ // Add half of the target size to get the center, and subtract translation since the
+ // target could be animating in while we're doing this calculation.
+ centerOnScreen.set(
+ tempLoc[0] + targetView.width / 2f - targetView.translationX,
+ tempLoc[1] + targetView.height / 2f - targetView.translationY)
+ }
+ }
+ }
+
+ companion object {
+
+ /**
+ * Whether the HAPTIC_FEEDBACK_ENABLED setting is true.
+ *
+ * We put it in the companion object because we need to register a settings observer and
+ * [MagnetizedObject] doesn't have an obvious lifecycle so we don't have a good time to
+ * remove that observer. Since this settings is shared among all instances we just let all
+ * instances read from this value.
+ */
+ private var systemHapticsEnabled = false
+ private var hapticSettingObserverInitialized = false
+
+ private fun initHapticSettingObserver(context: Context) {
+ if (hapticSettingObserverInitialized) {
+ return
+ }
+
+ val hapticSettingObserver =
+ object : ContentObserver(Handler.getMain()) {
+ override fun onChange(selfChange: Boolean) {
+ systemHapticsEnabled =
+ Settings.System.getIntForUser(
+ context.contentResolver,
+ Settings.System.HAPTIC_FEEDBACK_ENABLED,
+ 0,
+ UserHandle.USER_CURRENT) != 0
+ }
+ }
+
+ context.contentResolver.registerContentObserver(
+ Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED),
+ true /* notifyForDescendants */, hapticSettingObserver)
+
+ // Trigger the observer once to initialize systemHapticsEnabled.
+ hapticSettingObserver.onChange(false /* selfChange */)
+ hapticSettingObserverInitialized = true
+ }
+
+ /**
+ * Magnetizes the given view. Magnetized views are attracted to one or more magnetic
+ * targets. Magnetic targets attract objects that are dragged near them, and hold them there
+ * unless they're moved away or released. Releasing objects inside a magnetic target
+ * typically performs an action on the object.
+ *
+ * Magnetized views can also be flung to targets, which will result in the view being pulled
+ * into the target and released as if it was dragged into it.
+ *
+ * To use the returned MagnetizedObject<View> instance, first set [magnetListener] to
+ * receive event callbacks. In your touch handler, pass all MotionEvents that move this view
+ * to [maybeConsumeMotionEvent]. If that method returns true, consider the event consumed by
+ * MagnetizedObject and don't move the view unless it begins returning false again.
+ *
+ * The view will be moved via translationX/Y properties, and its
+ * width/height will be determined via getWidth()/getHeight(). If you are animating
+ * something other than a view, or want to position your view using properties other than
+ * translationX/Y, implement an instance of [MagnetizedObject].
+ *
+ * Note that the magnetic library can't re-order your view automatically. If the view
+ * renders on top of the target views, it will obscure the target when it sticks to it.
+ * You'll want to bring the view to the front in [MagnetListener.onStuckToTarget].
+ */
+ @JvmStatic
+ fun <T : View> magnetizeView(view: T): MagnetizedObject<T> {
+ return object : MagnetizedObject<T>(
+ view.context,
+ view,
+ DynamicAnimation.TRANSLATION_X,
+ DynamicAnimation.TRANSLATION_Y) {
+ override fun getWidth(underlyingObject: T): Float {
+ return underlyingObject.width.toFloat()
+ }
+
+ override fun getHeight(underlyingObject: T): Float {
+ return underlyingObject.height.toFloat() }
+
+ override fun getLocationOnScreen(underlyingObject: T, loc: IntArray) {
+ underlyingObject.getLocationOnScreen(loc)
+ }
+ }
+ }
+ }
+} \ No newline at end of file
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
new file mode 100644
index 000000000000..9bb709f9a82a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback;
+
+import java.io.PrintWriter;
+
+/**
+ * Interface to engage one handed feature.
+ */
+public interface OneHanded {
+ /**
+ * Return one handed settings enabled or not.
+ */
+ boolean isOneHandedEnabled();
+
+ /**
+ * Return swipe to notification settings enabled or not.
+ */
+ boolean isSwipeToNotificationEnabled();
+
+ /**
+ * Enters one handed mode.
+ */
+ void startOneHanded();
+
+ /**
+ * Exits one handed mode.
+ */
+ void stopOneHanded();
+
+ /**
+ * Exits one handed mode with {@link OneHandedEvents}.
+ */
+ void stopOneHanded(int event);
+
+ /**
+ * Set navigation 3 button mode enabled or disabled by users.
+ */
+ void setThreeButtonModeEnabled(boolean enabled);
+
+ /**
+ * Register callback to be notified after {@link OneHandedDisplayAreaOrganizer}
+ * transition start or finish
+ */
+ void registerTransitionCallback(OneHandedTransitionCallback callback);
+
+ /**
+ * Register callback for one handed gesture, this gesture callbcak will be activated on
+ * 3 button navigation mode only
+ */
+ void registerGestureCallback(OneHandedGestureEventCallback callback);
+
+ /**
+ * Dump one handed status.
+ */
+ void dump(@NonNull PrintWriter pw);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java
new file mode 100644
index 000000000000..6749f7eec968
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import android.view.SurfaceControl;
+
+/**
+ * Additional callback interface for OneHanded animation
+ */
+public interface OneHandedAnimationCallback {
+ /**
+ * Called when OneHanded animation is started.
+ */
+ default void onOneHandedAnimationStart(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ /**
+ * Called when OneHanded animation is ended.
+ */
+ default void onOneHandedAnimationEnd(SurfaceControl.Transaction tx,
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ /**
+ * Called when OneHanded animation is cancelled.
+ */
+ default void onOneHandedAnimationCancel(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ /**
+ * Called when OneHanded animator is updating offset
+ */
+ default void onTutorialAnimationUpdate(int offset) {}
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java
new file mode 100644
index 000000000000..963909621a1b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Controller class of OneHanded animations (both from and to OneHanded mode).
+ */
+public class OneHandedAnimationController {
+ private static final float FRACTION_START = 0f;
+ private static final float FRACTION_END = 1f;
+
+ public static final int TRANSITION_DIRECTION_NONE = 0;
+ public static final int TRANSITION_DIRECTION_TRIGGER = 1;
+ public static final int TRANSITION_DIRECTION_EXIT = 2;
+
+ @IntDef(prefix = {"TRANSITION_DIRECTION_"}, value = {
+ TRANSITION_DIRECTION_NONE,
+ TRANSITION_DIRECTION_TRIGGER,
+ TRANSITION_DIRECTION_EXIT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TransitionDirection {
+ }
+
+ private final Interpolator mOvershootInterpolator;
+ private final OneHandedSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final HashMap<SurfaceControl, OneHandedTransitionAnimator> mAnimatorMap =
+ new HashMap<>();
+
+ /**
+ * Constructor of OneHandedAnimationController
+ */
+ public OneHandedAnimationController(Context context) {
+ mSurfaceTransactionHelper = new OneHandedSurfaceTransactionHelper(context);
+ mOvershootInterpolator = new OvershootInterpolator();
+ }
+
+ @SuppressWarnings("unchecked")
+ OneHandedTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds,
+ Rect endBounds) {
+ final OneHandedTransitionAnimator animator = mAnimatorMap.get(leash);
+ if (animator == null) {
+ mAnimatorMap.put(leash, setupOneHandedTransitionAnimator(
+ OneHandedTransitionAnimator.ofBounds(leash, startBounds, endBounds)));
+ } else if (animator.isRunning()) {
+ animator.updateEndValue(endBounds);
+ } else {
+ animator.cancel();
+ mAnimatorMap.put(leash, setupOneHandedTransitionAnimator(
+ OneHandedTransitionAnimator.ofBounds(leash, startBounds, endBounds)));
+ }
+ return mAnimatorMap.get(leash);
+ }
+
+ HashMap<SurfaceControl, OneHandedTransitionAnimator> getAnimatorMap() {
+ return mAnimatorMap;
+ }
+
+ boolean isAnimatorsConsumed() {
+ return mAnimatorMap.isEmpty();
+ }
+
+ void removeAnimator(SurfaceControl key) {
+ final OneHandedTransitionAnimator animator = mAnimatorMap.remove(key);
+ if (animator != null && animator.isRunning()) {
+ animator.cancel();
+ }
+ }
+
+ OneHandedTransitionAnimator setupOneHandedTransitionAnimator(
+ OneHandedTransitionAnimator animator) {
+ animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper);
+ animator.setInterpolator(mOvershootInterpolator);
+ animator.setFloatValues(FRACTION_START, FRACTION_END);
+ return animator;
+ }
+
+ /**
+ * Animator for OneHanded transition animation which supports both alpha and bounds animation.
+ *
+ * @param <T> Type of property to animate, either offset (float)
+ */
+ public abstract static class OneHandedTransitionAnimator<T> extends ValueAnimator implements
+ ValueAnimator.AnimatorUpdateListener,
+ ValueAnimator.AnimatorListener {
+
+ private final SurfaceControl mLeash;
+ private T mStartValue;
+ private T mEndValue;
+ private T mCurrentValue;
+
+ private final List<OneHandedAnimationCallback> mOneHandedAnimationCallbacks =
+ new ArrayList<>();
+ private OneHandedSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+
+ private @TransitionDirection int mTransitionDirection;
+ private int mTransitionOffset;
+
+ private OneHandedTransitionAnimator(SurfaceControl leash, T startValue, T endValue) {
+ mLeash = leash;
+ mStartValue = startValue;
+ mEndValue = endValue;
+ addListener(this);
+ addUpdateListener(this);
+ mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mTransitionDirection = TRANSITION_DIRECTION_NONE;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mCurrentValue = mStartValue;
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onOneHandedAnimationStart(this);
+ }
+ );
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentValue = mEndValue;
+ final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+ onEndTransaction(mLeash, tx);
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onOneHandedAnimationEnd(tx, this);
+ }
+ );
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentValue = mEndValue;
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onOneHandedAnimationCancel(this);
+ }
+ );
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(),
+ animation.getAnimatedFraction());
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onTutorialAnimationUpdate(((Rect) mCurrentValue).top);
+ }
+ );
+ }
+
+ void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ }
+
+ void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ }
+
+ abstract void applySurfaceControlTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, float fraction);
+
+ OneHandedSurfaceTransactionHelper getSurfaceTransactionHelper() {
+ return mSurfaceTransactionHelper;
+ }
+
+ void setSurfaceTransactionHelper(OneHandedSurfaceTransactionHelper helper) {
+ mSurfaceTransactionHelper = helper;
+ }
+
+ OneHandedTransitionAnimator<T> setOneHandedAnimationCallbacks(
+ OneHandedAnimationCallback callback) {
+ mOneHandedAnimationCallbacks.add(callback);
+ return this;
+ }
+
+ SurfaceControl getLeash() {
+ return mLeash;
+ }
+
+ Rect getDestinationBounds() {
+ return (Rect) mEndValue;
+ }
+
+ int getDestinationOffset() {
+ return ((Rect) mEndValue).top - ((Rect) mStartValue).top;
+ }
+
+ @TransitionDirection
+ int getTransitionDirection() {
+ return mTransitionDirection;
+ }
+
+ OneHandedTransitionAnimator<T> setTransitionDirection(int direction) {
+ mTransitionDirection = direction;
+ return this;
+ }
+
+ OneHandedTransitionAnimator<T> setTransitionOffset(int offset) {
+ mTransitionOffset = offset;
+ return this;
+ }
+
+ T getStartValue() {
+ return mStartValue;
+ }
+
+ T getEndValue() {
+ return mEndValue;
+ }
+
+ void setCurrentValue(T value) {
+ mCurrentValue = value;
+ }
+
+ /**
+ * Updates the {@link #mEndValue}.
+ */
+ void updateEndValue(T endValue) {
+ mEndValue = endValue;
+ }
+
+ SurfaceControl.Transaction newSurfaceControlTransaction() {
+ return mSurfaceControlTransactionFactory.getTransaction();
+ }
+
+ @VisibleForTesting
+ static OneHandedTransitionAnimator<Rect> ofBounds(SurfaceControl leash,
+ Rect startValue, Rect endValue) {
+
+ return new OneHandedTransitionAnimator<Rect>(leash, new Rect(startValue),
+ new Rect(endValue)) {
+
+ private final Rect mTmpRect = new Rect();
+
+ private int getCastedFractionValue(float start, float end, float fraction) {
+ return (int) (start * (1 - fraction) + end * fraction + .5f);
+ }
+
+ @Override
+ void applySurfaceControlTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, float fraction) {
+ final Rect start = getStartValue();
+ final Rect end = getEndValue();
+ mTmpRect.set(
+ getCastedFractionValue(start.left, end.left, fraction),
+ getCastedFractionValue(start.top, end.top, fraction),
+ getCastedFractionValue(start.right, end.right, fraction),
+ getCastedFractionValue(start.bottom, end.bottom, fraction));
+ setCurrentValue(mTmpRect);
+ getSurfaceTransactionHelper().crop(tx, leash, mTmpRect)
+ .round(tx, leash);
+ tx.apply();
+ }
+
+ @Override
+ void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ getSurfaceTransactionHelper()
+ .alpha(tx, leash, 1f)
+ .translate(tx, leash, getEndValue().top - getStartValue().top)
+ .round(tx, leash);
+ tx.apply();
+ }
+ };
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
new file mode 100644
index 000000000000..7d039d4d92f6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static android.os.UserHandle.USER_CURRENT;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.content.Context;
+import android.content.om.IOverlayManager;
+import android.content.om.OverlayInfo;
+import android.database.ContentObserver;
+import android.graphics.Point;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.util.Slog;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages and manipulates the one handed states, transitions, and gesture for phones.
+ */
+public class OneHandedController implements OneHanded {
+ private static final String TAG = "OneHandedController";
+
+ private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
+ "persist.debug.one_handed_offset_percentage";
+ private static final String ONE_HANDED_MODE_GESTURAL_OVERLAY =
+ "com.android.internal.systemui.onehanded.gestural";
+
+ static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
+
+ private boolean mIsOneHandedEnabled;
+ private boolean mIsSwipeToNotificationEnabled;
+ private boolean mTaskChangeToExit;
+ private float mOffSetFraction;
+
+ private final Context mContext;
+ private final DisplayController mDisplayController;
+ private final OneHandedGestureHandler mGestureHandler;
+ private final OneHandedTimeoutHandler mTimeoutHandler;
+ private final OneHandedTouchHandler mTouchHandler;
+ private final OneHandedTutorialHandler mTutorialHandler;
+ private final IOverlayManager mOverlayManager;
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+ private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer;
+
+ /**
+ * Handle rotation based on OnDisplayChangingListener callback
+ */
+ private final DisplayChangeController.OnDisplayChangingListener mRotationController =
+ (display, fromRotation, toRotation, wct) -> {
+ if (mDisplayAreaOrganizer != null) {
+ mDisplayAreaOrganizer.onRotateDisplay(fromRotation, toRotation, wct);
+ }
+ };
+
+ private final ContentObserver mEnabledObserver = new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final boolean enabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ mContext.getContentResolver());
+ OneHandedEvents.writeEvent(enabled
+ ? OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON
+ : OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF);
+
+ setOneHandedEnabled(enabled);
+
+ // Also checks swipe to notification settings since they all need gesture overlay.
+ setEnabledGesturalOverlay(
+ enabled || OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ mContext.getContentResolver()));
+ }
+ };
+
+ private final ContentObserver mTimeoutObserver = new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final int newTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout(
+ mContext.getContentResolver());
+ int metricsId = OneHandedEvents.OneHandedSettingsTogglesEvent.INVALID.getId();
+ switch (newTimeout) {
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER;
+ break;
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4;
+ break;
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8;
+ break;
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12;
+ break;
+ default:
+ // do nothing
+ break;
+ }
+ OneHandedEvents.writeEvent(metricsId);
+
+ if (mTimeoutHandler != null) {
+ mTimeoutHandler.setTimeout(newTimeout);
+ }
+ }
+ };
+
+ private final ContentObserver mTaskChangeExitObserver = new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final boolean enabled = OneHandedSettingsUtil.getSettingsTapsAppToExit(
+ mContext.getContentResolver());
+ OneHandedEvents.writeEvent(enabled
+ ? OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON
+ : OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF);
+
+ setTaskChangeToExit(enabled);
+ }
+ };
+
+ private final ContentObserver mSwipeToNotificationEnabledObserver =
+ new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final boolean enabled =
+ OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ mContext.getContentResolver());
+ setSwipeToNotificationEnabled(enabled);
+
+ // Also checks one handed mode settings since they all need gesture overlay.
+ setEnabledGesturalOverlay(
+ enabled || OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ mContext.getContentResolver()));
+ }
+ };
+
+ /**
+ * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported.
+ */
+ @Nullable
+ public static OneHandedController create(
+ Context context, DisplayController displayController) {
+ if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) {
+ Slog.w(TAG, "Device doesn't support OneHanded feature");
+ return null;
+ }
+
+ OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context);
+ OneHandedAnimationController animationController =
+ new OneHandedAnimationController(context);
+ OneHandedTouchHandler touchHandler = new OneHandedTouchHandler();
+ OneHandedGestureHandler gestureHandler = new OneHandedGestureHandler(
+ context, displayController);
+ OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer(
+ context, displayController, animationController, tutorialHandler);
+ IOverlayManager overlayManager = IOverlayManager.Stub.asInterface(
+ ServiceManager.getService(Context.OVERLAY_SERVICE));
+ return new OneHandedController(context, displayController, organizer, touchHandler,
+ tutorialHandler, gestureHandler, overlayManager);
+ }
+
+ @VisibleForTesting
+ OneHandedController(Context context,
+ DisplayController displayController,
+ OneHandedDisplayAreaOrganizer displayAreaOrganizer,
+ OneHandedTouchHandler touchHandler,
+ OneHandedTutorialHandler tutorialHandler,
+ OneHandedGestureHandler gestureHandler,
+ IOverlayManager overlayManager) {
+ mContext = context;
+ mDisplayAreaOrganizer = displayAreaOrganizer;
+ mDisplayController = displayController;
+ mTouchHandler = touchHandler;
+ mTutorialHandler = tutorialHandler;
+ mGestureHandler = gestureHandler;
+ mOverlayManager = overlayManager;
+
+ mOffSetFraction = SystemProperties.getInt(ONE_HANDED_MODE_OFFSET_PERCENTAGE, 50)
+ / 100.0f;
+ mIsOneHandedEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ context.getContentResolver());
+ mIsSwipeToNotificationEnabled =
+ OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ context.getContentResolver());
+ mTimeoutHandler = OneHandedTimeoutHandler.get();
+
+ mDisplayController.addDisplayChangingController(mRotationController);
+
+ setupCallback();
+ setupSettingObservers();
+ setupTimeoutListener();
+ setupGesturalOverlay();
+ updateSettings();
+ }
+
+ /**
+ * Set one handed enabled or disabled when user update settings
+ */
+ void setOneHandedEnabled(boolean enabled) {
+ mIsOneHandedEnabled = enabled;
+ updateOneHandedEnabled();
+ }
+
+ /**
+ * Set one handed enabled or disabled by when user update settings
+ */
+ void setTaskChangeToExit(boolean enabled) {
+ mTaskChangeToExit = enabled;
+ }
+
+ /**
+ * Sets whether to enable swipe bottom to notification gesture when user update settings.
+ */
+ void setSwipeToNotificationEnabled(boolean enabled) {
+ mIsSwipeToNotificationEnabled = enabled;
+ updateOneHandedEnabled();
+ }
+
+ @Override
+ public boolean isOneHandedEnabled() {
+ return mIsOneHandedEnabled;
+ }
+
+ @Override
+ public boolean isSwipeToNotificationEnabled() {
+ return mIsSwipeToNotificationEnabled;
+ }
+
+ @Override
+ public void startOneHanded() {
+ if (!mDisplayAreaOrganizer.isInOneHanded()) {
+ final int yOffSet = Math.round(getDisplaySize().y * mOffSetFraction);
+ mDisplayAreaOrganizer.scheduleOffset(0, yOffSet);
+ mTimeoutHandler.resetTimer();
+
+ OneHandedEvents.writeEvent(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN);
+ }
+ }
+
+ @Override
+ public void stopOneHanded() {
+ if (mDisplayAreaOrganizer.isInOneHanded()) {
+ mDisplayAreaOrganizer.scheduleOffset(0, 0);
+ mTimeoutHandler.removeTimer();
+ }
+ }
+
+ @Override
+ public void stopOneHanded(int event) {
+ if (!mTaskChangeToExit && event == OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT) {
+ //Task change exit not enable, do nothing and return here.
+ return;
+ }
+
+ if (mDisplayAreaOrganizer.isInOneHanded()) {
+ OneHandedEvents.writeEvent(event);
+ }
+
+ stopOneHanded();
+ }
+
+ @Override
+ public void setThreeButtonModeEnabled(boolean enabled) {
+ mGestureHandler.onThreeButtonModeEnabled(enabled);
+ }
+
+ @Override
+ public void registerTransitionCallback(OneHandedTransitionCallback callback) {
+ mDisplayAreaOrganizer.registerTransitionCallback(callback);
+ }
+
+ @Override
+ public void registerGestureCallback(OneHandedGestureEventCallback callback) {
+ mGestureHandler.setGestureEventListener(callback);
+ }
+
+ private void setupCallback() {
+ mTouchHandler.registerTouchEventListener(() ->
+ stopOneHanded(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT));
+ mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler);
+ mDisplayAreaOrganizer.registerTransitionCallback(mGestureHandler);
+ mDisplayAreaOrganizer.registerTransitionCallback(mTutorialHandler);
+ }
+
+ private void setupSettingObservers() {
+ OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_ENABLED,
+ mContext.getContentResolver(), mEnabledObserver);
+ OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_TIMEOUT,
+ mContext.getContentResolver(), mTimeoutObserver);
+ OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.TAPS_APP_TO_EXIT,
+ mContext.getContentResolver(), mTaskChangeExitObserver);
+ OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED,
+ mContext.getContentResolver(), mSwipeToNotificationEnabledObserver);
+ }
+
+ private void updateSettings() {
+ setOneHandedEnabled(OneHandedSettingsUtil
+ .getSettingsOneHandedModeEnabled(mContext.getContentResolver()));
+ mTimeoutHandler.setTimeout(OneHandedSettingsUtil
+ .getSettingsOneHandedModeTimeout(mContext.getContentResolver()));
+ setTaskChangeToExit(OneHandedSettingsUtil
+ .getSettingsTapsAppToExit(mContext.getContentResolver()));
+ setSwipeToNotificationEnabled(OneHandedSettingsUtil
+ .getSettingsSwipeToNotificationEnabled(mContext.getContentResolver()));
+ }
+
+ private void setupTimeoutListener() {
+ mTimeoutHandler.registerTimeoutListener(timeoutTime -> {
+ stopOneHanded(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT);
+ });
+ }
+
+ /**
+ * Query the current display real size from {@link DisplayController}
+ *
+ * @return {@link DisplayController#getDisplay(int)#getDisplaySize()}
+ */
+ private Point getDisplaySize() {
+ Point displaySize = new Point();
+ if (mDisplayController != null && mDisplayController.getDisplay(DEFAULT_DISPLAY) != null) {
+ mDisplayController.getDisplay(DEFAULT_DISPLAY).getRealSize(displaySize);
+ }
+ return displaySize;
+ }
+
+ private void updateOneHandedEnabled() {
+ if (mDisplayAreaOrganizer.isInOneHanded()) {
+ stopOneHanded();
+ }
+ // TODO Be aware to unregisterOrganizer() after animation finished
+ mDisplayAreaOrganizer.unregisterOrganizer();
+ if (mIsOneHandedEnabled) {
+ mDisplayAreaOrganizer.registerOrganizer(
+ OneHandedDisplayAreaOrganizer.FEATURE_ONE_HANDED);
+ }
+ mTouchHandler.onOneHandedEnabled(mIsOneHandedEnabled);
+ mGestureHandler.onOneHandedEnabled(mIsOneHandedEnabled || mIsSwipeToNotificationEnabled);
+ }
+
+ private void setupGesturalOverlay() {
+ if (!OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver())) {
+ return;
+ }
+
+ OverlayInfo info = null;
+ try {
+ // TODO(b/157958539) migrate new RRO config file after S+
+ mOverlayManager.setHighestPriority(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT);
+ info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT);
+ } catch (RemoteException e) { /* Do nothing */ }
+
+ if (info != null && !info.isEnabled()) {
+ // Enable the default gestural one handed overlay.
+ setEnabledGesturalOverlay(true);
+ }
+ }
+
+ @VisibleForTesting
+ private void setEnabledGesturalOverlay(boolean enabled) {
+ try {
+ mOverlayManager.setEnabled(ONE_HANDED_MODE_GESTURAL_OVERLAY, enabled, USER_CURRENT);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mOffSetFraction=");
+ pw.println(mOffSetFraction);
+
+ if (mDisplayAreaOrganizer != null) {
+ mDisplayAreaOrganizer.dump(pw);
+ }
+
+ if (mTouchHandler != null) {
+ mTouchHandler.dump(pw);
+ }
+
+ if (mTimeoutHandler != null) {
+ mTimeoutHandler.dump(pw);
+ }
+
+ if (mTutorialHandler != null) {
+ mTutorialHandler.dump(pw);
+ }
+
+ OneHandedSettingsUtil.dump(pw, innerPrefix, mContext.getContentResolver());
+
+ if (mOverlayManager != null) {
+ OverlayInfo info = null;
+ try {
+ info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY,
+ USER_CURRENT);
+ } catch (RemoteException e) { /* Do nothing */ }
+
+ if (info != null && !info.isEnabled()) {
+ pw.print(innerPrefix + "OverlayInfo=");
+ pw.println(info);
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
new file mode 100644
index 000000000000..17418f934691
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT;
+import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
+import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.os.SomeArgs;
+import com.android.wm.shell.common.DisplayController;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages OneHanded display areas such as offset.
+ *
+ * This class listens on {@link DisplayAreaOrganizer} callbacks for windowing mode change
+ * both to and from OneHanded and issues corresponding animation if applicable.
+ * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running
+ * and files a final {@link WindowContainerTransaction} at the end of the transition.
+ *
+ * This class is also responsible for translating one handed operations within SysUI component
+ */
+public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
+ private static final String TAG = "OneHandedDisplayAreaOrganizer";
+ private static final String ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION =
+ "persist.debug.one_handed_translate_animation_duration";
+
+ @VisibleForTesting
+ static final int MSG_RESET_IMMEDIATE = 1;
+ @VisibleForTesting
+ static final int MSG_OFFSET_ANIMATE = 2;
+ @VisibleForTesting
+ static final int MSG_OFFSET_FINISH = 3;
+
+ private final Rect mLastVisualDisplayBounds = new Rect();
+ private final Rect mDefaultDisplayBounds = new Rect();
+
+ private Handler mUpdateHandler;
+ private boolean mIsInOneHanded;
+ private int mEnterExitAnimationDurationMs;
+
+ @VisibleForTesting
+ ArrayMap<DisplayAreaInfo, SurfaceControl> mDisplayAreaMap = new ArrayMap();
+ private DisplayController mDisplayController;
+ private OneHandedAnimationController mAnimationController;
+ private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+ private OneHandedTutorialHandler mTutorialHandler;
+ private List<OneHandedTransitionCallback> mTransitionCallbacks = new ArrayList<>();
+
+ @VisibleForTesting
+ OneHandedAnimationCallback mOneHandedAnimationCallback =
+ new OneHandedAnimationCallback() {
+ @Override
+ public void onOneHandedAnimationStart(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ @Override
+ public void onOneHandedAnimationEnd(SurfaceControl.Transaction tx,
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ mAnimationController.removeAnimator(animator.getLeash());
+ if (mAnimationController.isAnimatorsConsumed()) {
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_FINISH,
+ obtainArgsFromAnimator(animator)));
+ }
+ }
+
+ @Override
+ public void onOneHandedAnimationCancel(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ mAnimationController.removeAnimator(animator.getLeash());
+ if (mAnimationController.isAnimatorsConsumed()) {
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_FINISH,
+ obtainArgsFromAnimator(animator)));
+ }
+ }
+ };
+
+ @SuppressWarnings("unchecked")
+ private Handler.Callback mUpdateCallback = (msg) -> {
+ SomeArgs args = (SomeArgs) msg.obj;
+ final Rect currentBounds = args.arg1 != null ? (Rect) args.arg1 : mDefaultDisplayBounds;
+ final WindowContainerTransaction wctFromRotate = (WindowContainerTransaction) args.arg2;
+ final int yOffset = args.argi2;
+ final int direction = args.argi3;
+
+ switch (msg.what) {
+ case MSG_RESET_IMMEDIATE:
+ resetWindowsOffset(wctFromRotate);
+ mDefaultDisplayBounds.set(currentBounds);
+ mLastVisualDisplayBounds.set(currentBounds);
+ finishOffset(0, TRANSITION_DIRECTION_EXIT);
+ break;
+ case MSG_OFFSET_ANIMATE:
+ final Rect toBounds = new Rect(mDefaultDisplayBounds.left,
+ mDefaultDisplayBounds.top + yOffset,
+ mDefaultDisplayBounds.right,
+ mDefaultDisplayBounds.bottom + yOffset);
+ offsetWindows(currentBounds, toBounds, direction, mEnterExitAnimationDurationMs);
+ break;
+ case MSG_OFFSET_FINISH:
+ finishOffset(yOffset, direction);
+ break;
+ }
+ args.recycle();
+ return true;
+ };
+
+ /**
+ * Constructor of OneHandedDisplayAreaOrganizer
+ */
+ public OneHandedDisplayAreaOrganizer(Context context,
+ DisplayController displayController,
+ OneHandedAnimationController animationController,
+ OneHandedTutorialHandler tutorialHandler) {
+ mUpdateHandler = new Handler(OneHandedThread.get().getLooper(), mUpdateCallback);
+ mAnimationController = animationController;
+ mDisplayController = displayController;
+ mDefaultDisplayBounds.set(getDisplayBounds());
+ mLastVisualDisplayBounds.set(getDisplayBounds());
+ mEnterExitAnimationDurationMs =
+ SystemProperties.getInt(ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION, 300);
+ mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mTutorialHandler = tutorialHandler;
+ }
+
+ @Override
+ public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
+ @NonNull SurfaceControl leash) {
+ Objects.requireNonNull(displayAreaInfo, "displayAreaInfo must not be null");
+ Objects.requireNonNull(leash, "leash must not be null");
+ synchronized (this) {
+ if (mDisplayAreaMap.get(displayAreaInfo) == null) {
+ // mDefaultDisplayBounds may out of date after removeDisplayChangingController()
+ mDefaultDisplayBounds.set(getDisplayBounds());
+ mDisplayAreaMap.put(displayAreaInfo, leash);
+ }
+ }
+ }
+
+ @Override
+ public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) {
+ Objects.requireNonNull(displayAreaInfo,
+ "Requires valid displayArea, and displayArea must not be null");
+ synchronized (this) {
+ if (!mDisplayAreaMap.containsKey(displayAreaInfo)) {
+ Log.w(TAG, "Unrecognized token: " + displayAreaInfo.token);
+ return;
+ }
+ mDisplayAreaMap.remove(displayAreaInfo);
+ }
+ }
+
+ @Override
+ public void unregisterOrganizer() {
+ super.unregisterOrganizer();
+ mUpdateHandler.post(() -> resetWindowsOffset(null));
+ }
+
+ /**
+ * Handler for display rotation changes by below policy which
+ * handles 90 degree display rotation changes {@link Surface.Rotation}.
+ *
+ * @param fromRotation starting rotation of the display.
+ * @param toRotation target rotation of the display (after rotating).
+ * @param wct A task transaction {@link WindowContainerTransaction} from
+ * {@link DisplayChangeController} to populate.
+ */
+ public void onRotateDisplay(int fromRotation, int toRotation, WindowContainerTransaction wct) {
+ // Stop one handed without animation and reset cropped size immediately
+ final Rect newBounds = new Rect(mDefaultDisplayBounds);
+ final boolean isOrientationDiff = Math.abs(fromRotation - toRotation) % 2 == 1;
+
+ if (isOrientationDiff) {
+ newBounds.set(newBounds.left, newBounds.top, newBounds.bottom, newBounds.right);
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = newBounds;
+ args.arg2 = wct;
+ args.argi1 = 0 /* xOffset */;
+ args.argi2 = 0 /* yOffset */;
+ args.argi3 = TRANSITION_DIRECTION_EXIT;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESET_IMMEDIATE, args));
+ }
+ }
+
+ /**
+ * Offset the windows by a given offset on Y-axis, triggered also from screen rotation.
+ * Directly perform manipulation/offset on the leash.
+ */
+ public void scheduleOffset(int xOffset, int yOffset) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = getLastVisualDisplayBounds();
+ args.argi1 = xOffset;
+ args.argi2 = yOffset;
+ args.argi3 = yOffset > 0 ? TRANSITION_DIRECTION_TRIGGER : TRANSITION_DIRECTION_EXIT;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args));
+ }
+
+ private void offsetWindows(Rect fromBounds, Rect toBounds, int direction, int durationMs) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleOffset() instead of this "
+ + "directly");
+ }
+ synchronized (this) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mDisplayAreaMap.forEach(
+ (key, leash) -> {
+ animateWindows(leash, fromBounds, toBounds, direction, durationMs);
+ wct.setBounds(key.token, toBounds);
+ });
+ applyTransaction(wct);
+ }
+ }
+
+ private void resetWindowsOffset(WindowContainerTransaction wct) {
+ synchronized (this) {
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ mDisplayAreaMap.forEach(
+ (key, leash) -> {
+ final OneHandedAnimationController.OneHandedTransitionAnimator animator =
+ mAnimationController.getAnimatorMap().remove(leash);
+ if (animator != null && animator.isRunning()) {
+ animator.cancel();
+ }
+ tx.setPosition(leash, 0, 0)
+ .setWindowCrop(leash, -1/* reset */, -1/* reset */);
+ // DisplayRotationController will applyTransaction() after finish rotating
+ if (wct != null) {
+ wct.setBounds(key.token, null/* reset */);
+ }
+ });
+ tx.apply();
+ }
+ }
+
+ private void animateWindows(SurfaceControl leash, Rect fromBounds, Rect toBounds,
+ @OneHandedAnimationController.TransitionDirection int direction, int durationMs) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleOffset() instead of "
+ + "this directly");
+ }
+ mUpdateHandler.post(() -> {
+ final OneHandedAnimationController.OneHandedTransitionAnimator animator =
+ mAnimationController.getAnimator(leash, fromBounds, toBounds);
+ if (animator != null) {
+ animator.setTransitionDirection(direction)
+ .setOneHandedAnimationCallbacks(mOneHandedAnimationCallback)
+ .setOneHandedAnimationCallbacks(mTutorialHandler.getAnimationCallback())
+ .setDuration(durationMs)
+ .start();
+ }
+ });
+ }
+
+ private void finishOffset(int offset,
+ @OneHandedAnimationController.TransitionDirection int direction) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException(
+ "Callers should call scheduleOffset() instead of this directly.");
+ }
+ // Only finishOffset() can update mIsInOneHanded to ensure the state is handle in sequence,
+ // the flag *MUST* be updated before dispatch mTransitionCallbacks
+ mIsInOneHanded = (offset > 0 || direction == TRANSITION_DIRECTION_TRIGGER);
+ mLastVisualDisplayBounds.offsetTo(0,
+ direction == TRANSITION_DIRECTION_TRIGGER ? offset : 0);
+ for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) {
+ final OneHandedTransitionCallback callback = mTransitionCallbacks.get(i);
+ if (direction == TRANSITION_DIRECTION_TRIGGER) {
+ callback.onStartFinished(getLastVisualDisplayBounds());
+ } else {
+ callback.onStopFinished(getLastVisualDisplayBounds());
+ }
+ }
+ }
+
+ /**
+ * The latest state of one handed mode
+ *
+ * @return true Currently is in one handed mode, otherwise is not in one handed mode
+ */
+ public boolean isInOneHanded() {
+ return mIsInOneHanded;
+ }
+
+ /**
+ * The latest visual bounds of displayArea translated
+ *
+ * @return Rect latest finish_offset
+ */
+ public Rect getLastVisualDisplayBounds() {
+ return mLastVisualDisplayBounds;
+ }
+
+ @Nullable
+ private Rect getDisplayBounds() {
+ Point realSize = new Point(0, 0);
+ if (mDisplayController != null && mDisplayController.getDisplay(DEFAULT_DISPLAY) != null) {
+ mDisplayController.getDisplay(DEFAULT_DISPLAY).getRealSize(realSize);
+ }
+ return new Rect(0, 0, realSize.x, realSize.y);
+ }
+
+ @VisibleForTesting
+ void setUpdateHandler(Handler updateHandler) {
+ mUpdateHandler = updateHandler;
+ }
+
+ /**
+ * Register transition callback
+ */
+ public void registerTransitionCallback(OneHandedTransitionCallback callback) {
+ mTransitionCallbacks.add(callback);
+ }
+
+ private SomeArgs obtainArgsFromAnimator(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = animator.getDestinationBounds();
+ args.argi1 = 0 /* xOffset */;
+ args.argi2 = animator.getDestinationOffset();
+ args.argi3 = animator.getTransitionDirection();
+ return args;
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mIsInOneHanded=");
+ pw.println(mIsInOneHanded);
+ pw.print(innerPrefix + "mDisplayAreaMap=");
+ pw.println(mDisplayAreaMap);
+ pw.print(innerPrefix + "mDefaultDisplayBounds=");
+ pw.println(mDefaultDisplayBounds);
+ pw.print(innerPrefix + "mLastVisualDisplayBounds=");
+ pw.println(mLastVisualDisplayBounds);
+ pw.print(innerPrefix + "getDisplayBounds()=");
+ pw.println(getDisplayBounds());
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java
new file mode 100644
index 000000000000..79ddd2b11e72
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
+
+/**
+ * Interesting events related to the One-Handed.
+ */
+public class OneHandedEvents {
+ private static final String TAG = "OneHandedEvents";
+
+ public static Callback sCallback;
+ @VisibleForTesting
+ static UiEventLogger sUiEventLogger = new UiEventLoggerImpl();
+
+ /**
+ * One-Handed event types
+ */
+ // Triggers
+ public static final int EVENT_ONE_HANDED_TRIGGER_GESTURE_IN = 0;
+ public static final int EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT = 1;
+ public static final int EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT = 2;
+ public static final int EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT = 3;
+ public static final int EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT = 4;
+ public static final int EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT = 5;
+ public static final int EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT = 6;
+ public static final int EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT = 7;
+ // Settings toggles
+ public static final int EVENT_ONE_HANDED_SETTINGS_ENABLED_ON = 8;
+ public static final int EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF = 9;
+ public static final int EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON = 10;
+ public static final int EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF = 11;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON = 12;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF = 13;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER = 14;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4 = 15;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8 = 16;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12 = 17;
+
+ private static final String[] EVENT_TAGS = {
+ "one_handed_trigger_gesture_in",
+ "one_handed_trigger_gesture_out",
+ "one_handed_trigger_overspace_out",
+ "one_handed_trigger_pop_ime_out",
+ "one_handed_trigger_rotation_out",
+ "one_handed_trigger_app_taps_out",
+ "one_handed_trigger_timeout_out",
+ "one_handed_trigger_screen_off_out",
+ "one_handed_settings_enabled_on",
+ "one_handed_settings_enabled_off",
+ "one_handed_settings_app_taps_exit_on",
+ "one_handed_settings_app_taps_exit_off",
+ "one_handed_settings_timeout_exit_on",
+ "one_handed_settings_timeout_exit_off",
+ "one_handed_settings_timeout_seconds_never",
+ "one_handed_settings_timeout_seconds_4",
+ "one_handed_settings_timeout_seconds_8",
+ "one_handed_settings_timeout_seconds_12"
+ };
+
+ /**
+ * Events definition that related to One-Handed gestures.
+ */
+ @VisibleForTesting
+ public enum OneHandedTriggerEvent implements UiEventLogger.UiEventEnum {
+ INVALID(0),
+ @UiEvent(doc = "One-Handed trigger in via NavigationBar area")
+ ONE_HANDED_TRIGGER_GESTURE_IN(366),
+
+ @UiEvent(doc = "One-Handed trigger out via NavigationBar area")
+ ONE_HANDED_TRIGGER_GESTURE_OUT(367),
+
+ @UiEvent(doc = "One-Handed trigger out via Overspace area")
+ ONE_HANDED_TRIGGER_OVERSPACE_OUT(368),
+
+ @UiEvent(doc = "One-Handed trigger out while IME pop up")
+ ONE_HANDED_TRIGGER_POP_IME_OUT(369),
+
+ @UiEvent(doc = "One-Handed trigger out while device rotation to landscape")
+ ONE_HANDED_TRIGGER_ROTATION_OUT(370),
+
+ @UiEvent(doc = "One-Handed trigger out when an Activity is launching")
+ ONE_HANDED_TRIGGER_APP_TAPS_OUT(371),
+
+ @UiEvent(doc = "One-Handed trigger out when one-handed mode times up")
+ ONE_HANDED_TRIGGER_TIMEOUT_OUT(372),
+
+ @UiEvent(doc = "One-Handed trigger out when screen off")
+ ONE_HANDED_TRIGGER_SCREEN_OFF_OUT(449);
+
+ private final int mId;
+
+ OneHandedTriggerEvent(int id) {
+ mId = id;
+ }
+
+ public int getId() {
+ return mId;
+ }
+ }
+
+ /**
+ * Events definition that related to Settings toggles.
+ */
+ @VisibleForTesting
+ public enum OneHandedSettingsTogglesEvent implements UiEventLogger.UiEventEnum {
+ INVALID(0),
+ @UiEvent(doc = "One-Handed mode enabled toggle on")
+ ONE_HANDED_SETTINGS_TOGGLES_ENABLED_ON(356),
+
+ @UiEvent(doc = "One-Handed mode enabled toggle off")
+ ONE_HANDED_SETTINGS_TOGGLES_ENABLED_OFF(357),
+
+ @UiEvent(doc = "One-Handed mode app-taps-exit toggle on")
+ ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_ON(358),
+
+ @UiEvent(doc = "One-Handed mode app-taps-exit toggle off")
+ ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_OFF(359),
+
+ @UiEvent(doc = "One-Handed mode timeout-exit toggle on")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_ON(360),
+
+ @UiEvent(doc = "One-Handed mode timeout-exit toggle off")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_OFF(361),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to never timeout")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_NEVER(362),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to 4 seconds")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_4(363),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to 8 seconds")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_8(364),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to 12 seconds")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_12(365);
+
+ private final int mId;
+
+ OneHandedSettingsTogglesEvent(int id) {
+ mId = id;
+ }
+
+ public int getId() {
+ return mId;
+ }
+ }
+
+
+ /**
+ * Logs an event to the system log, to sCallback if present, and to the logEvent destinations.
+ * @param tag One of the EVENT_* codes above.
+ */
+ public static void writeEvent(int tag) {
+ final long time = System.currentTimeMillis();
+ logEvent(tag);
+ if (sCallback != null) {
+ sCallback.writeEvent(time, tag);
+ }
+ }
+
+ /**
+ * Logs an event to the UiEvent (statsd) logging.
+ * @param event One of the EVENT_* codes above.
+ * @return String a readable description of the event. Begins "writeEvent <tag_description>"
+ * if the tag is valid.
+ */
+ public static String logEvent(int event) {
+ if (event >= EVENT_TAGS.length) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[event]);
+ switch (event) {
+ // Triggers
+ case EVENT_ONE_HANDED_TRIGGER_GESTURE_IN:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_IN);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_OVERSPACE_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_POP_IME_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_ROTATION_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_APP_TAPS_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_TIMEOUT_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_SCREEN_OFF_OUT);
+ break;
+ // Settings
+ case EVENT_ONE_HANDED_SETTINGS_ENABLED_ON:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_ON);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_OFF);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_ON);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_OFF);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_ON);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_OFF);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_NEVER);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_4);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_8);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_12);
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ return sb.toString();
+ }
+
+ /**
+ * An interface for logging an event to the system log, if Callback present.
+ */
+ public interface Callback {
+ /**
+ *
+ * @param time System current time.
+ * @param tag Event tag.
+ */
+ void writeEvent(long time, int tag);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java
new file mode 100644
index 000000000000..3b1e6cbe5ccd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.ViewConfiguration;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+
+/**
+ * The class manage swipe up and down gesture for 3-Button mode navigation,
+ * others(e.g, 2-button, full gesture mode) are handled by Launcher quick steps.
+ */
+public class OneHandedGestureHandler implements OneHandedTransitionCallback,
+ DisplayChangeController.OnDisplayChangingListener {
+ private static final String TAG = "OneHandedGestureHandler";
+ private static final boolean DEBUG_GESTURE = false;
+
+ private static final int ANGLE_MAX = 150;
+ private static final int ANGLE_MIN = 30;
+ private final float mDragDistThreshold;
+ private final float mSquaredSlop;
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+ private final PointF mStartDragPos = new PointF();
+ private boolean mPassedSlop;
+
+ private boolean mAllowGesture;
+ private boolean mIsEnabled;
+ private int mNavGestureHeight;
+ private boolean mIsThreeButtonModeEnabled;
+ private int mRotation = Surface.ROTATION_0;
+
+ @VisibleForTesting
+ InputMonitor mInputMonitor;
+ @VisibleForTesting
+ InputEventReceiver mInputEventReceiver;
+ private DisplayController mDisplayController;
+ @VisibleForTesting
+ @Nullable
+ OneHandedGestureEventCallback mGestureEventCallback;
+ private Rect mGestureRegion = new Rect();
+
+ /**
+ * Constructor of OneHandedGestureHandler, we only handle the gesture of
+ * {@link Display#DEFAULT_DISPLAY}
+ *
+ * @param context {@link Context}
+ * @param displayController {@link DisplayController}
+ */
+ public OneHandedGestureHandler(Context context, DisplayController displayController) {
+ mDisplayController = displayController;
+ displayController.addDisplayChangingController(this);
+ mNavGestureHeight = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.navigation_bar_gesture_height);
+ mDragDistThreshold = context.getResources().getDimensionPixelSize(
+ R.dimen.gestures_onehanded_drag_threshold);
+ final float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mSquaredSlop = slop * slop;
+ updateIsEnabled();
+ }
+
+ /**
+ * Notified by {@link OneHandedController}, when user update settings of Enabled or Disabled
+ *
+ * @param isEnabled is one handed settings enabled or not
+ */
+ public void onOneHandedEnabled(boolean isEnabled) {
+ if (DEBUG_GESTURE) {
+ Log.d(TAG, "onOneHandedEnabled, isEnabled = " + isEnabled);
+ }
+ mIsEnabled = isEnabled;
+ updateIsEnabled();
+ }
+
+ void onThreeButtonModeEnabled(boolean isEnabled) {
+ mIsThreeButtonModeEnabled = isEnabled;
+ updateIsEnabled();
+ }
+
+ /**
+ * Register {@link OneHandedGestureEventCallback} to receive onStart(), onStop() callback
+ */
+ public void setGestureEventListener(OneHandedGestureEventCallback callback) {
+ mGestureEventCallback = callback;
+ }
+
+ private void onMotionEvent(MotionEvent ev) {
+ int action = ev.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mAllowGesture = isWithinTouchRegion(ev.getX(), ev.getY())
+ && mRotation == Surface.ROTATION_0;
+ if (mAllowGesture) {
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+ }
+ if (DEBUG_GESTURE) {
+ Log.d(TAG, "ACTION_DOWN, mDownPos=" + mDownPos + ", mAllowGesture="
+ + mAllowGesture);
+ }
+ } else if (mAllowGesture) {
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ mLastPos.set(ev.getX(), ev.getY());
+ if (!mPassedSlop) {
+ if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+ > mSquaredSlop) {
+ mStartDragPos.set(mLastPos.x, mLastPos.y);
+ if (isValidStartAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)
+ || isValidExitAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) {
+ mPassedSlop = true;
+ mInputMonitor.pilferPointers();
+ }
+ }
+ } else {
+ float distance = (float) Math.hypot(mLastPos.x - mDownPos.x,
+ mLastPos.y - mDownPos.y);
+ if (distance > mDragDistThreshold) {
+ mGestureEventCallback.onStop();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mLastPos.y >= mDownPos.y && mPassedSlop) {
+ mGestureEventCallback.onStart();
+ }
+ mPassedSlop = false;
+ mAllowGesture = false;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mPassedSlop = false;
+ mAllowGesture = false;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private void disposeInputChannel() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ }
+
+ private boolean isWithinTouchRegion(float x, float y) {
+ if (DEBUG_GESTURE) {
+ Log.d(TAG, "isWithinTouchRegion(), mGestureRegion=" + mGestureRegion + ", downX=" + x
+ + ", downY=" + y);
+ }
+ return mGestureRegion.contains(Math.round(x), Math.round(y));
+ }
+
+ private void updateIsEnabled() {
+ disposeInputChannel();
+
+ if (mIsEnabled && mIsThreeButtonModeEnabled) {
+ final Point displaySize = new Point();
+ if (mDisplayController != null) {
+ final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY);
+ if (display != null) {
+ display.getRealSize(displaySize);
+ }
+ }
+ // Register input event receiver to monitor the touch region of NavBar gesture height
+ mGestureRegion.set(0, displaySize.y - mNavGestureHeight, displaySize.x,
+ displaySize.y);
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ "onehanded-gesture-offset", DEFAULT_DISPLAY);
+ mInputEventReceiver = new EventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper());
+ }
+ }
+
+ private void onInputEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ onMotionEvent((MotionEvent) ev);
+ }
+ }
+
+ @Override
+ public void onRotateDisplay(int displayId, int fromRotation, int toRotation,
+ WindowContainerTransaction t) {
+ mRotation = toRotation;
+ }
+
+ private class EventReceiver extends InputEventReceiver {
+ EventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper);
+ }
+
+ public void onInputEvent(InputEvent event) {
+ OneHandedGestureHandler.this.onInputEvent(event);
+ finishInputEvent(event, true);
+ }
+ }
+
+ private boolean isValidStartAngle(float deltaX, float deltaY) {
+ final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN);
+ }
+
+ private boolean isValidExitAngle(float deltaX, float deltaY) {
+ final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ return angle > ANGLE_MIN && angle < ANGLE_MAX;
+ }
+
+ private float squaredHypot(float x, float y) {
+ return x * x + y * y;
+ }
+
+ /**
+ * The touch(gesture) events to notify {@link OneHandedController} start or stop one handed
+ */
+ public interface OneHandedGestureEventCallback {
+ /**
+ * Handles the start gesture.
+ */
+ void onStart();
+
+ /**
+ * Handles the exit gesture.
+ */
+ void onStop();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java
new file mode 100644
index 000000000000..4d66f2961a29
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import android.annotation.IntDef;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * APIs for querying or updating one handed settings .
+ */
+public final class OneHandedSettingsUtil {
+ private static final String TAG = "OneHandedSettingsUtil";
+
+ @IntDef(prefix = {"ONE_HANDED_TIMEOUT_"}, value = {
+ ONE_HANDED_TIMEOUT_NEVER,
+ ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_LONG_IN_SECONDS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OneHandedTimeout {
+ }
+
+ /**
+ * Never stop one handed automatically
+ */
+ public static final int ONE_HANDED_TIMEOUT_NEVER = 0;
+ /**
+ * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS}
+ */
+ public static final int ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS = 4;
+ /**
+ * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS}
+ */
+ public static final int ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS = 8;
+ /**
+ * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_LONG_IN_SECONDS}
+ */
+ public static final int ONE_HANDED_TIMEOUT_LONG_IN_SECONDS = 12;
+
+ /**
+ * Register one handed preference settings observer
+ *
+ * @param key Setting key to monitor in observer
+ * @param resolver ContentResolver of context
+ * @param observer Observer from caller
+ * @return uri key for observing
+ */
+ public static Uri registerSettingsKeyObserver(String key, ContentResolver resolver,
+ ContentObserver observer) {
+ Uri uriKey = null;
+ uriKey = Settings.Secure.getUriFor(key);
+ if (resolver != null && uriKey != null) {
+ resolver.registerContentObserver(uriKey, false, observer);
+ }
+ return uriKey;
+ }
+
+ /**
+ * Unregister one handed preference settings observer
+ *
+ * @param resolver ContentResolver of context
+ * @param observer preference key change observer
+ */
+ public static void unregisterSettingsKeyObserver(ContentResolver resolver,
+ ContentObserver observer) {
+ if (resolver != null) {
+ resolver.unregisterContentObserver(observer);
+ }
+ }
+
+ /**
+ * Query one handed enable or disable flag from Settings provider.
+ *
+ * @return enable or disable one handed mode flag.
+ */
+ public static boolean getSettingsOneHandedModeEnabled(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, 0 /* Disabled */) == 1;
+ }
+
+ /**
+ * Query taps app to exit config from Settings provider.
+ *
+ * @return enable or disable taps app exit.
+ */
+ public static boolean getSettingsTapsAppToExit(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.TAPS_APP_TO_EXIT, 0) == 1;
+ }
+
+ /**
+ * Query timeout value from Settings provider.
+ * Default is {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS}
+ *
+ * @return timeout value in seconds.
+ */
+ public static @OneHandedTimeout int getSettingsOneHandedModeTimeout(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT, ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ }
+
+ /**
+ * Returns whether swipe bottom to notification gesture enabled or not.
+ */
+ public static boolean getSettingsSwipeToNotificationEnabled(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 0) == 1;
+ }
+
+ protected static void dump(PrintWriter pw, String prefix, ContentResolver resolver) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.print(innerPrefix + "isOneHandedModeEnable=");
+ pw.println(getSettingsOneHandedModeEnabled(resolver));
+ pw.print(innerPrefix + "oneHandedTimeOut=");
+ pw.println(getSettingsOneHandedModeTimeout(resolver));
+ pw.print(innerPrefix + "tapsAppToExit=");
+ pw.println(getSettingsTapsAppToExit(resolver));
+ }
+
+ private OneHandedSettingsUtil() {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java
new file mode 100644
index 000000000000..e7010db97d77
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+
+/**
+ * Abstracts the common operations on {@link SurfaceControl.Transaction} for OneHanded transition.
+ */
+public class OneHandedSurfaceTransactionHelper {
+ private final boolean mEnableCornerRadius;
+ private final float mCornerRadius;
+
+ public OneHandedSurfaceTransactionHelper(Context context) {
+ final Resources res = context.getResources();
+ mCornerRadius = res.getDimension(com.android.internal.R.dimen.rounded_corner_radius);
+ mEnableCornerRadius = res.getBoolean(R.bool.config_one_handed_enable_round_corner);
+ }
+
+ /**
+ * Operates the translation (setPosition) on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper translate(SurfaceControl.Transaction tx, SurfaceControl leash,
+ float offset) {
+ tx.setPosition(leash, 0, offset);
+ return this;
+ }
+
+ /**
+ * Operates the alpha on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash,
+ float alpha) {
+ tx.setAlpha(leash, alpha);
+ return this;
+ }
+
+ /**
+ * Operates the crop (setMatrix) on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect destinationBounds) {
+ tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
+ .setPosition(leash, destinationBounds.left, destinationBounds.top);
+ return this;
+ }
+
+ /**
+ * Operates the round corner radius on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash) {
+ if (mEnableCornerRadius) {
+ tx.setCornerRadius(leash, mCornerRadius);
+ }
+ return this;
+ }
+
+ interface SurfaceControlTransactionFactory {
+ SurfaceControl.Transaction getTransaction();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java
new file mode 100644
index 000000000000..24d33ede5d63
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * Similar to {@link com.android.internal.os.BackgroundThread}, this is a shared singleton
+ * foreground thread for each process for updating one handed.
+ */
+public class OneHandedThread extends HandlerThread {
+ private static OneHandedThread sInstance;
+ private static Handler sHandler;
+
+ private OneHandedThread() {
+ super("OneHanded");
+ }
+
+ private static void ensureThreadLocked() {
+ if (sInstance == null) {
+ sInstance = new OneHandedThread();
+ sInstance.start();
+ sHandler = new Handler(sInstance.getLooper());
+ }
+ }
+
+ /**
+ * @return the static update thread instance
+ */
+ public static OneHandedThread get() {
+ synchronized (OneHandedThread.class) {
+ ensureThreadLocked();
+ return sInstance;
+ }
+ }
+
+ /**
+ * @return the static update thread handler instance
+ */
+ public static Handler getHandler() {
+ synchronized (OneHandedThread.class) {
+ ensureThreadLocked();
+ return sHandler;
+ }
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java
new file mode 100644
index 000000000000..9c97cd7db71f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Timeout handler for stop one handed mode operations.
+ */
+public class OneHandedTimeoutHandler {
+ private static final String TAG = "OneHandedTimeoutHandler";
+ private static boolean sIsDragging = false;
+ // Default timeout is ONE_HANDED_TIMEOUT_MEDIUM
+ private static @OneHandedSettingsUtil.OneHandedTimeout int sTimeout =
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+ private static long sTimeoutMs = TimeUnit.SECONDS.toMillis(sTimeout);
+ private static OneHandedTimeoutHandler sInstance;
+ private static List<TimeoutListener> sListeners = new ArrayList<>();
+
+ @VisibleForTesting
+ static final int ONE_HANDED_TIMEOUT_STOP_MSG = 1;
+ @VisibleForTesting
+ static Handler sHandler;
+
+ /**
+ * Get the current config of timeout
+ *
+ * @return timeout of current config
+ */
+ public @OneHandedSettingsUtil.OneHandedTimeout int getTimeout() {
+ return sTimeout;
+ }
+
+ /**
+ * Listens for notify timeout events
+ */
+ public interface TimeoutListener {
+ /**
+ * Called whenever the config time out
+ *
+ * @param timeoutTime The time in seconds to trigger timeout
+ */
+ void onTimeout(int timeoutTime);
+ }
+
+ /**
+ * Set the specific timeout of {@link OneHandedSettingsUtil.OneHandedTimeout}
+ */
+ public static void setTimeout(@OneHandedSettingsUtil.OneHandedTimeout int timeout) {
+ sTimeout = timeout;
+ sTimeoutMs = TimeUnit.SECONDS.toMillis(sTimeout);
+ resetTimer();
+ }
+
+ /**
+ * Reset the timer when one handed trigger or user is operating in some conditions
+ */
+ public static void removeTimer() {
+ sHandler.removeMessages(ONE_HANDED_TIMEOUT_STOP_MSG);
+ }
+
+ /**
+ * Reset the timer when one handed trigger or user is operating in some conditions
+ */
+ public static void resetTimer() {
+ removeTimer();
+ if (sTimeout == OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) {
+ return;
+ }
+ if (sTimeout != OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) {
+ sHandler.sendEmptyMessageDelayed(ONE_HANDED_TIMEOUT_STOP_MSG, sTimeoutMs);
+ }
+ }
+
+ /**
+ * Register timeout listener to receive time out events
+ *
+ * @param listener the listener be sent events when times up
+ */
+ public static void registerTimeoutListener(TimeoutListener listener) {
+ sListeners.add(listener);
+ }
+
+ /**
+ * Private constructor due to Singleton pattern
+ */
+ private OneHandedTimeoutHandler() {
+ }
+
+ /**
+ * Singleton pattern to get {@link OneHandedTimeoutHandler} instance
+ *
+ * @return the static update thread instance
+ */
+ public static OneHandedTimeoutHandler get() {
+ synchronized (OneHandedTimeoutHandler.class) {
+ if (sInstance == null) {
+ sInstance = new OneHandedTimeoutHandler();
+ }
+ if (sHandler == null) {
+ sHandler = new Handler(Looper.myLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == ONE_HANDED_TIMEOUT_STOP_MSG) {
+ onStop();
+ }
+ }
+ };
+ if (sTimeout != OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) {
+ sHandler.sendEmptyMessageDelayed(ONE_HANDED_TIMEOUT_STOP_MSG, sTimeoutMs);
+ }
+ }
+ return sInstance;
+ }
+ }
+
+ private static void onStop() {
+ for (int i = sListeners.size() - 1; i >= 0; i--) {
+ final TimeoutListener listener = sListeners.get(i);
+ listener.onTimeout(sTimeout);
+ }
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "sTimeout=");
+ pw.println(sTimeout);
+ pw.print(innerPrefix + "sListeners=");
+ pw.println(sListeners);
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java
new file mode 100644
index 000000000000..721382d52717
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Looper;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages all the touch handling for One Handed on the Phone, including user tap outside region
+ * to exit, reset timer when user is in one-handed mode.
+ * Refer {@link OneHandedGestureHandler} to see start and stop one handed gesture
+ */
+public class OneHandedTouchHandler implements OneHandedTransitionCallback {
+ private static final String TAG = "OneHandedTouchHandler";
+ private final Rect mLastUpdatedBounds = new Rect();
+
+ private OneHandedTimeoutHandler mTimeoutHandler;
+
+ @VisibleForTesting
+ InputMonitor mInputMonitor;
+ @VisibleForTesting
+ InputEventReceiver mInputEventReceiver;
+ @VisibleForTesting
+ OneHandedTouchEventCallback mTouchEventCallback;
+
+ private boolean mIsEnabled;
+ private boolean mIsOnStopTransitioning;
+ private boolean mIsInOutsideRegion;
+
+ public OneHandedTouchHandler() {
+ mTimeoutHandler = OneHandedTimeoutHandler.get();
+ updateIsEnabled();
+ }
+
+ /**
+ * Notified by {@link OneHandedController}, when user update settings of Enabled or Disabled
+ *
+ * @param isEnabled is one handed settings enabled or not
+ */
+ public void onOneHandedEnabled(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ updateIsEnabled();
+ }
+
+ /**
+ * Register {@link OneHandedTouchEventCallback} to receive onEnter(), onExit() callback
+ */
+ public void registerTouchEventListener(OneHandedTouchEventCallback callback) {
+ mTouchEventCallback = callback;
+ }
+
+ private boolean onMotionEvent(MotionEvent ev) {
+ mIsInOutsideRegion = isWithinTouchOutsideRegion(ev.getX(), ev.getY());
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE: {
+ if (!mIsInOutsideRegion) {
+ mTimeoutHandler.resetTimer();
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ mTimeoutHandler.resetTimer();
+ if (mIsInOutsideRegion && !mIsOnStopTransitioning) {
+ mTouchEventCallback.onStop();
+ mIsOnStopTransitioning = true;
+ }
+ // Reset flag for next operation
+ mIsInOutsideRegion = false;
+ break;
+ }
+ }
+ return true;
+ }
+
+ private void disposeInputChannel() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ }
+
+ private boolean isWithinTouchOutsideRegion(float x, float y) {
+ return Math.round(y) < mLastUpdatedBounds.top;
+ }
+
+ private void onInputEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ onMotionEvent((MotionEvent) ev);
+ }
+ }
+
+ private void updateIsEnabled() {
+ disposeInputChannel();
+ if (mIsEnabled) {
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ "onehanded-touch", DEFAULT_DISPLAY);
+ mInputEventReceiver = new EventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper());
+ }
+ }
+
+ @Override
+ public void onStartFinished(Rect bounds) {
+ mLastUpdatedBounds.set(bounds);
+ }
+
+ @Override
+ public void onStopFinished(Rect bounds) {
+ mLastUpdatedBounds.set(bounds);
+ mIsOnStopTransitioning = false;
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mLastUpdatedBounds=");
+ pw.println(mLastUpdatedBounds);
+ }
+
+ private class EventReceiver extends InputEventReceiver {
+ EventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper);
+ }
+
+ public void onInputEvent(InputEvent event) {
+ OneHandedTouchHandler.this.onInputEvent(event);
+ finishInputEvent(event, true);
+ }
+ }
+
+ /**
+ * The touch(gesture) events to notify {@link OneHandedController} start or stop one handed
+ */
+ public interface OneHandedTouchEventCallback {
+ /**
+ * Handle the exit event.
+ */
+ void onStop();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java
new file mode 100644
index 000000000000..3af7c4b71d0a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import android.graphics.Rect;
+
+/**
+ * The start or stop one handed transition callback for gesture to get latest timing to handle
+ * touch region.(e.g: one handed activated, user tap out regions of displayArea to stop one handed)
+ */
+public interface OneHandedTransitionCallback {
+ /**
+ * Called when start one handed transition finished
+ */
+ default void onStartFinished(Rect bounds) {
+ }
+
+ /**
+ * Called when stop one handed transition finished
+ */
+ default void onStopFinished(Rect bounds) {
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java
new file mode 100644
index 000000000000..b6b518d69c55
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.R;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages the user tutorial handling for One Handed operations, including animations synchronized
+ * with one-handed translation.
+ * Refer {@link OneHandedGestureHandler} and {@link OneHandedTouchHandler} to see start and stop
+ * one handed gesture
+ */
+public class OneHandedTutorialHandler implements OneHandedTransitionCallback {
+ private static final String TAG = "OneHandedTutorialHandler";
+ private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
+ "persist.debug.one_handed_offset_percentage";
+ private static final int MAX_TUTORIAL_SHOW_COUNT = 2;
+ private final Rect mLastUpdatedBounds = new Rect();
+ private final WindowManager mWindowManager;
+ private final AccessibilityManager mAccessibilityManager;
+ private final String mPackageName;
+
+ private View mTutorialView;
+ private Point mDisplaySize = new Point();
+ private Handler mUpdateHandler;
+ private ContentResolver mContentResolver;
+ private boolean mCanShowTutorial;
+ private String mStartOneHandedDescription;
+ private String mStopOneHandedDescription;
+
+ /**
+ * Container of the tutorial panel showing at outside region when one handed starting
+ */
+ private ViewGroup mTargetViewContainer;
+ private int mTutorialAreaHeight;
+
+ private final OneHandedAnimationCallback mAnimationCallback = new OneHandedAnimationCallback() {
+ @Override
+ public void onTutorialAnimationUpdate(int offset) {
+ mUpdateHandler.post(() -> onAnimationUpdate(offset));
+ }
+ };
+
+ public OneHandedTutorialHandler(Context context) {
+ context.getDisplay().getRealSize(mDisplaySize);
+ mPackageName = context.getPackageName();
+ mContentResolver = context.getContentResolver();
+ mUpdateHandler = new Handler();
+ mWindowManager = context.getSystemService(WindowManager.class);
+ mAccessibilityManager = (AccessibilityManager)
+ context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ mTargetViewContainer = new FrameLayout(context);
+ mTargetViewContainer.setClipChildren(false);
+ mTutorialAreaHeight = Math.round(mDisplaySize.y
+ * (SystemProperties.getInt(ONE_HANDED_MODE_OFFSET_PERCENTAGE, 50) / 100.0f));
+ mTutorialView = LayoutInflater.from(context).inflate(R.layout.one_handed_tutorial, null);
+ mTargetViewContainer.addView(mTutorialView);
+ mCanShowTutorial = (Settings.Secure.getInt(mContentResolver,
+ Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0) >= MAX_TUTORIAL_SHOW_COUNT)
+ ? false : true;
+ mStartOneHandedDescription = context.getResources().getString(
+ R.string.accessibility_action_start_one_handed);
+ mStopOneHandedDescription = context.getResources().getString(
+ R.string.accessibility_action_stop_one_handed);
+ if (mCanShowTutorial) {
+ createOrUpdateTutorialTarget();
+ }
+ }
+
+ @Override
+ public void onStartFinished(Rect bounds) {
+ mUpdateHandler.post(() -> {
+ updateFinished(View.VISIBLE, 0f);
+ updateTutorialCount();
+ announcementForScreenReader(true);
+ });
+ }
+
+ @Override
+ public void onStopFinished(Rect bounds) {
+ mUpdateHandler.post(() -> {
+ updateFinished(View.INVISIBLE, -mTargetViewContainer.getHeight());
+ announcementForScreenReader(false);
+ });
+ }
+
+ private void updateFinished(int visible, float finalPosition) {
+ if (!canShowTutorial()) {
+ return;
+ }
+
+ mTargetViewContainer.setVisibility(visible);
+ mTargetViewContainer.setTranslationY(finalPosition);
+ }
+
+ private void updateTutorialCount() {
+ int showCount = Settings.Secure.getInt(mContentResolver,
+ Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0);
+ showCount = Math.min(MAX_TUTORIAL_SHOW_COUNT, showCount + 1);
+ mCanShowTutorial = showCount < MAX_TUTORIAL_SHOW_COUNT;
+ Settings.Secure.putInt(mContentResolver,
+ Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, showCount);
+ }
+
+ private void announcementForScreenReader(boolean isStartOneHanded) {
+ if (mAccessibilityManager.isTouchExplorationEnabled()) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setPackageName(mPackageName);
+ event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ event.getText().add(isStartOneHanded
+ ? mStartOneHandedDescription : mStopOneHandedDescription);
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+ }
+
+ /**
+ * Adds the tutorial target view to the WindowManager and update its layout, so it's ready
+ * to be animated in.
+ */
+ private void createOrUpdateTutorialTarget() {
+ mUpdateHandler.post(() -> {
+ if (!mTargetViewContainer.isAttachedToWindow()) {
+ mTargetViewContainer.setVisibility(View.INVISIBLE);
+
+ try {
+ mWindowManager.addView(mTargetViewContainer, getTutorialTargetLayoutParams());
+ } catch (IllegalStateException e) {
+ // This shouldn't happen, but if the target is already added, just update its
+ // layout params.
+ mWindowManager.updateViewLayout(
+ mTargetViewContainer, getTutorialTargetLayoutParams());
+ }
+ } else {
+ mWindowManager.updateViewLayout(mTargetViewContainer,
+ getTutorialTargetLayoutParams());
+ }
+ });
+ }
+
+ OneHandedAnimationCallback getAnimationCallback() {
+ return mAnimationCallback;
+ }
+
+ /**
+ * Returns layout params for the dismiss target, using the latest display metrics.
+ */
+ private WindowManager.LayoutParams getTutorialTargetLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ mDisplaySize.x, mTutorialAreaHeight, 0, 0,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+ lp.gravity = Gravity.TOP | Gravity.LEFT;
+ lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ lp.setFitInsetsTypes(0 /* types */);
+ lp.setTitle("one-handed-tutorial-overlay");
+
+ return lp;
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mLastUpdatedBounds=");
+ pw.println(mLastUpdatedBounds);
+ }
+
+ private boolean canShowTutorial() {
+ if (!mCanShowTutorial) {
+ mTargetViewContainer.setVisibility(View.GONE);
+ return false;
+ }
+
+ return true;
+ }
+
+ private void onAnimationUpdate(float value) {
+ if (!canShowTutorial()) {
+ return;
+ }
+ mTargetViewContainer.setVisibility(View.VISIBLE);
+ mTargetViewContainer.setTransitionGroup(true);
+ mTargetViewContainer.setTranslationY(value - mTargetViewContainer.getHeight());
+ }
+}
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
new file mode 100644
index 000000000000..993e0e7ed016
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2019 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 android.app.RemoteAction;
+import android.content.ComponentName;
+import android.content.pm.ParceledListSlice;
+import android.view.DisplayInfo;
+import android.view.IPinnedStackController;
+import android.view.IPinnedStackListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * PinnedStackListener that simply forwards all calls to each listener added via
+ * {@link #addListener}. This is necessary since calling
+ * {@link com.android.server.wm.WindowManagerService#registerPinnedStackListener} replaces any
+ * previously set listener.
+ */
+public class PinnedStackListenerForwarder extends IPinnedStackListener.Stub {
+ private List<PinnedStackListener> mListeners = new ArrayList<>();
+
+ /** Adds a listener to receive updates from the WindowManagerService. */
+ public void addListener(PinnedStackListener listener) {
+ mListeners.add(listener);
+ }
+
+ /** Removes a listener so it will no longer receive updates from the WindowManagerService. */
+ public void removeListener(PinnedStackListener listener) {
+ mListeners.remove(listener);
+ }
+
+ @Override
+ public void onListenerRegistered(IPinnedStackController controller) {
+ for (PinnedStackListener listener : mListeners) {
+ listener.onListenerRegistered(controller);
+ }
+ }
+
+ @Override
+ public void onMovementBoundsChanged(boolean fromImeAdjustment) {
+ for (PinnedStackListener listener : mListeners) {
+ listener.onMovementBoundsChanged(fromImeAdjustment);
+ }
+ }
+
+ @Override
+ public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+ for (PinnedStackListener listener : mListeners) {
+ listener.onImeVisibilityChanged(imeVisible, imeHeight);
+ }
+ }
+
+ @Override
+ public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
+ for (PinnedStackListener listener : mListeners) {
+ listener.onActionsChanged(actions);
+ }
+ }
+
+ @Override
+ public void onActivityHidden(ComponentName componentName) {
+ for (PinnedStackListener listener : mListeners) {
+ listener.onActivityHidden(componentName);
+ }
+ }
+
+ @Override
+ public void onDisplayInfoChanged(DisplayInfo displayInfo) {
+ for (PinnedStackListener listener : mListeners) {
+ listener.onDisplayInfoChanged(displayInfo);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged() {
+ for (PinnedStackListener listener : mListeners) {
+ listener.onConfigurationChanged();
+ }
+ }
+
+ @Override
+ public void onAspectRatioChanged(float aspectRatio) {
+ for (PinnedStackListener listener : mListeners) {
+ listener.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) {}
+
+ public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {}
+
+ public void onActivityHidden(ComponentName componentName) {}
+
+ public void onDisplayInfoChanged(DisplayInfo displayInfo) {}
+
+ public void onConfigurationChanged() {}
+
+ public void onAspectRatioChanged(float aspectRatio) {}
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
new file mode 100644
index 000000000000..3ded4091ec11
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -0,0 +1,262 @@
+/*
+ * 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 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 android.media.session.MediaController;
+
+import com.android.wm.shell.pip.phone.PipTouchHandler;
+import com.android.wm.shell.pip.tv.PipController;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Interface to engage picture in picture feature.
+ */
+public interface Pip {
+ /**
+ * Registers {@link com.android.wm.shell.pip.tv.PipController.Listener} that gets called.
+ * whenever receiving notification on changes in PIP.
+ */
+ default void addListener(PipController.Listener listener) {
+ }
+
+ /**
+ * Registers a {@link PipController.MediaListener} to PipController.
+ */
+ default void addMediaListener(PipController.MediaListener listener) {
+ }
+
+ /**
+ * Closes PIP (PIPed activity and PIP system UI).
+ */
+ default void closePip() {
+ }
+
+ /**
+ * Dump the current state and information if need.
+ *
+ * @param pw The stream to dump information to.
+ */
+ default void dump(PrintWriter pw) {
+ }
+
+ /**
+ * Expand PIP, it's possible that specific request to activate the window via Alt-tab.
+ */
+ default void expandPip() {
+ }
+
+ /**
+ * Get current play back state. (e.g: Used in TV)
+ *
+ * @return The state of defined in PipController.
+ */
+ default int getPlaybackState() {
+ return -1;
+ }
+
+ /**
+ * Get the touch handler which manages all the touch handling for PIP on the Phone,
+ * including moving, dismissing and expanding the PIP. (Do not used in TV)
+ *
+ * @return
+ */
+ default @Nullable PipTouchHandler getPipTouchHandler() {
+ return null;
+ }
+
+ /**
+ * Get MediaController.
+ *
+ * @return The MediaController instance.
+ */
+ default MediaController getMediaController() {
+ return null;
+ }
+
+ /**
+ * Hides the PIP menu.
+ */
+ default void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {}
+
+ /**
+ * Returns {@code true} if PIP is shown.
+ */
+ default boolean isPipShown() {
+ return false;
+ }
+
+ /**
+ * Moves the PIPed activity to the fullscreen and closes PIP system UI.
+ */
+ default void movePipToFullscreen() {
+ }
+
+ /**
+ * Called whenever an Activity is moved to the pinned stack from another stack.
+ */
+ default void onActivityPinned(String packageName) {
+ }
+
+ /**
+ * Called whenever an Activity is moved from the pinned stack to another stack
+ */
+ default void onActivityUnpinned(ComponentName topActivity) {
+ }
+
+ /**
+ * Called whenever IActivityManager.startActivity is called on an activity that is already
+ * running, but the task is either brought to the front or a new Intent is delivered to it.
+ *
+ * @param task information about the task the activity was relaunched into
+ * @param clearedTask whether or not the launch activity also cleared the task as a part of
+ * starting
+ */
+ default void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+ boolean clearedTask) {
+ }
+
+ /**
+ * Called when display size or font size of settings changed
+ */
+ default void onDensityOrFontScaleChanged() {
+ }
+
+ /**
+ * Called when overlay package change invoked.
+ */
+ default void onOverlayChanged() {
+ }
+
+ /**
+ * Registers the session listener for the current user.
+ */
+ default void registerSessionListenerForCurrentUser() {
+ }
+
+ /**
+ * Called when SysUI state changed.
+ *
+ * @param isSysUiStateValid Is SysUI state valid or not.
+ * @param flag Current SysUI state.
+ */
+ default void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) {
+ }
+
+ /**
+ * Called when task stack changed.
+ */
+ default void onTaskStackChanged() {
+ }
+
+ /**
+ * Removes a {@link PipController.Listener} from PipController.
+ */
+ default void removeListener(PipController.Listener listener) {
+ }
+
+ /**
+ * Removes a {@link PipController.MediaListener} from PipController.
+ */
+ default void removeMediaListener(PipController.MediaListener listener) {
+ }
+
+ /**
+ * Resize the Pip to the appropriate size for the input state.
+ *
+ * @param state In Pip state also used to determine the new size for the Pip.
+ */
+ default void resizePinnedStack(int state) {
+ }
+
+ /**
+ * Resumes resizing operation on the Pip that was previously suspended.
+ *
+ * @param reason The reason resizing operations on the Pip was suspended.
+ */
+ default void resumePipResizing(int reason) {
+ }
+
+ /**
+ * Sets both shelf visibility and its height.
+ *
+ * @param visible visibility of shelf.
+ * @param height to specify the height for shelf.
+ */
+ default void setShelfHeight(boolean visible, int height) {
+ }
+
+ /**
+ * Registers the pinned stack animation listener.
+ *
+ * @param callback The callback of pinned stack animation.
+ */
+ default void setPinnedStackAnimationListener(Consumer<Boolean> callback) {
+ }
+
+ /**
+ * Set the pinned stack with {@link PipAnimationController.AnimationType}
+ *
+ * @param animationType The pre-defined {@link PipAnimationController.AnimationType}
+ */
+ default void setPinnedStackAnimationType(int animationType) {
+ }
+
+ /**
+ * Called when showing Pip menu.
+ */
+ default void showPictureInPictureMenu() {}
+
+ /**
+ * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called.
+ *
+ * @param reason The reason for suspending resizing operations on the Pip.
+ */
+ default void suspendPipResizing(int reason) {
+ }
+
+ /**
+ * Called by Launcher when swiping an auto-pip enabled Activity to home starts
+ * @param componentName {@link ComponentName} represents the Activity entering PiP
+ * @param activityInfo {@link ActivityInfo} tied to the Activity
+ * @param pictureInPictureParams {@link PictureInPictureParams} tied to the Activity
+ * @param launcherRotation Rotation Launcher is in
+ * @param shelfHeight Shelf height when landing PiP window onto Launcher
+ * @return Destination bounds of PiP window based on the parameters passed in
+ */
+ default Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
+ PictureInPictureParams pictureInPictureParams,
+ int launcherRotation, int shelfHeight) {
+ return null;
+ }
+
+ /**
+ * Called by Launcher when swiping an auto-pip enable Activity to home finishes
+ * @param componentName {@link ComponentName} represents the Activity entering PiP
+ * @param destinationBounds Destination bounds of PiP window
+ */
+ default void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) {
+ return;
+ }
+}
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
new file mode 100644
index 000000000000..d82946269ee8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -0,0 +1,462 @@
+/*
+ * 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 android.animation.AnimationHandler;
+import android.animation.Animator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.wm.shell.animation.Interpolators;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Controller class of PiP animations (both from and to PiP mode).
+ */
+public class PipAnimationController {
+ private static final float FRACTION_START = 0f;
+ private static final float FRACTION_END = 1f;
+
+ public static final int ANIM_TYPE_BOUNDS = 0;
+ public static final int ANIM_TYPE_ALPHA = 1;
+
+ @IntDef(prefix = { "ANIM_TYPE_" }, value = {
+ ANIM_TYPE_BOUNDS,
+ ANIM_TYPE_ALPHA
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AnimationType {}
+
+ public static final int TRANSITION_DIRECTION_NONE = 0;
+ public static final int TRANSITION_DIRECTION_SAME = 1;
+ public static final int TRANSITION_DIRECTION_TO_PIP = 2;
+ 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;
+
+ @IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = {
+ TRANSITION_DIRECTION_NONE,
+ TRANSITION_DIRECTION_SAME,
+ TRANSITION_DIRECTION_TO_PIP,
+ TRANSITION_DIRECTION_LEAVE_PIP,
+ TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN,
+ TRANSITION_DIRECTION_REMOVE_STACK
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TransitionDirection {}
+
+ public static boolean isInPipDirection(@TransitionDirection int direction) {
+ return direction == TRANSITION_DIRECTION_TO_PIP;
+ }
+
+ public static boolean isOutPipDirection(@TransitionDirection int direction) {
+ return direction == TRANSITION_DIRECTION_LEAVE_PIP
+ || direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
+ }
+
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+
+ private PipTransitionAnimator mCurrentAnimator;
+
+ private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
+ ThreadLocal.withInitial(() -> {
+ AnimationHandler handler = new AnimationHandler();
+ handler.setProvider(new SfVsyncFrameCallbackProvider());
+ return handler;
+ });
+
+ public PipAnimationController(PipSurfaceTransactionHelper helper) {
+ mSurfaceTransactionHelper = helper;
+ }
+
+ @SuppressWarnings("unchecked")
+ @VisibleForTesting
+ public PipTransitionAnimator getAnimator(SurfaceControl leash,
+ Rect destinationBounds, float alphaStart, float alphaEnd) {
+ if (mCurrentAnimator == null) {
+ mCurrentAnimator = setupPipTransitionAnimator(
+ PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd));
+ } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA
+ && mCurrentAnimator.isRunning()) {
+ mCurrentAnimator.updateEndValue(alphaEnd);
+ } else {
+ mCurrentAnimator.cancel();
+ mCurrentAnimator = setupPipTransitionAnimator(
+ PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd));
+ }
+ return mCurrentAnimator;
+ }
+
+ @SuppressWarnings("unchecked")
+ @VisibleForTesting
+ public PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds,
+ Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction) {
+ if (mCurrentAnimator == null) {
+ mCurrentAnimator = setupPipTransitionAnimator(
+ PipTransitionAnimator.ofBounds(leash, 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
+ // we update with the new destination bounds, but don't interrupt the existing animation
+ // with a new bounds
+ mCurrentAnimator.setDestinationBounds(endBounds);
+ } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_BOUNDS
+ && mCurrentAnimator.isRunning()) {
+ mCurrentAnimator.setDestinationBounds(endBounds);
+ // construct new Rect instances in case they are recycled
+ mCurrentAnimator.updateEndValue(new Rect(endBounds));
+ } else {
+ mCurrentAnimator.cancel();
+ mCurrentAnimator = setupPipTransitionAnimator(
+ PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect,
+ direction));
+ }
+ return mCurrentAnimator;
+ }
+
+ PipTransitionAnimator getCurrentAnimator() {
+ return mCurrentAnimator;
+ }
+
+ private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) {
+ animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ animator.setFloatValues(FRACTION_START, FRACTION_END);
+ animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
+ return animator;
+ }
+
+ /**
+ * Additional callback interface for PiP animation
+ */
+ public static class PipAnimationCallback {
+ /**
+ * Called when PiP animation is started.
+ */
+ public void onPipAnimationStart(PipTransitionAnimator animator) {}
+
+ /**
+ * Called when PiP animation is ended.
+ */
+ public void onPipAnimationEnd(SurfaceControl.Transaction tx,
+ PipTransitionAnimator animator) {}
+
+ /**
+ * Called when PiP animation is cancelled.
+ */
+ public void onPipAnimationCancel(PipTransitionAnimator animator) {}
+ }
+
+ /**
+ * Animator for PiP transition animation which supports both alpha and bounds animation.
+ * @param <T> Type of property to animate, either alpha (float) or bounds (Rect)
+ */
+ public abstract static class PipTransitionAnimator<T> extends ValueAnimator implements
+ ValueAnimator.AnimatorUpdateListener,
+ ValueAnimator.AnimatorListener {
+ private final SurfaceControl mLeash;
+ private final @AnimationType int mAnimationType;
+ private final Rect mDestinationBounds = new Rect();
+
+ protected T mCurrentValue;
+ protected T mStartValue;
+ private T mEndValue;
+ private PipAnimationCallback mPipAnimationCallback;
+ private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+ private PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private @TransitionDirection int mTransitionDirection;
+
+ private PipTransitionAnimator(SurfaceControl leash, @AnimationType int animationType,
+ Rect destinationBounds, T startValue, T endValue) {
+ mLeash = leash;
+ mAnimationType = animationType;
+ mDestinationBounds.set(destinationBounds);
+ mStartValue = startValue;
+ mEndValue = endValue;
+ addListener(this);
+ addUpdateListener(this);
+ mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mTransitionDirection = TRANSITION_DIRECTION_NONE;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mCurrentValue = mStartValue;
+ onStartTransaction(mLeash, newSurfaceControlTransaction());
+ if (mPipAnimationCallback != null) {
+ mPipAnimationCallback.onPipAnimationStart(this);
+ }
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(),
+ animation.getAnimatedFraction());
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentValue = mEndValue;
+ final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+ onEndTransaction(mLeash, tx, mTransitionDirection);
+ if (mPipAnimationCallback != null) {
+ mPipAnimationCallback.onPipAnimationEnd(tx, this);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (mPipAnimationCallback != null) {
+ mPipAnimationCallback.onPipAnimationCancel(this);
+ }
+ }
+
+ @Override public void onAnimationRepeat(Animator animation) {}
+
+ @VisibleForTesting
+ @AnimationType public int getAnimationType() {
+ return mAnimationType;
+ }
+
+ @VisibleForTesting
+ public PipTransitionAnimator<T> setPipAnimationCallback(PipAnimationCallback callback) {
+ mPipAnimationCallback = callback;
+ return this;
+ }
+ @VisibleForTesting
+ @TransitionDirection public int getTransitionDirection() {
+ return mTransitionDirection;
+ }
+
+ @VisibleForTesting
+ public PipTransitionAnimator<T> setTransitionDirection(@TransitionDirection int direction) {
+ if (direction != TRANSITION_DIRECTION_SAME) {
+ mTransitionDirection = direction;
+ }
+ return this;
+ }
+
+ T getStartValue() {
+ return mStartValue;
+ }
+
+ @VisibleForTesting
+ public T getEndValue() {
+ return mEndValue;
+ }
+
+ Rect getDestinationBounds() {
+ return mDestinationBounds;
+ }
+
+ void setDestinationBounds(Rect destinationBounds) {
+ mDestinationBounds.set(destinationBounds);
+ if (mAnimationType == ANIM_TYPE_ALPHA) {
+ onStartTransaction(mLeash, newSurfaceControlTransaction());
+ }
+ }
+
+ void setCurrentValue(T value) {
+ mCurrentValue = value;
+ }
+
+ boolean shouldApplyCornerRadius() {
+ return !isOutPipDirection(mTransitionDirection);
+ }
+
+ boolean inScaleTransition() {
+ if (mAnimationType != ANIM_TYPE_BOUNDS) return false;
+ final int direction = getTransitionDirection();
+ return !isInPipDirection(direction) && !isOutPipDirection(direction);
+ }
+
+ /**
+ * Updates the {@link #mEndValue}.
+ *
+ * NOTE: Do not forget to call {@link #setDestinationBounds(Rect)} for bounds animation.
+ * This is typically used when we receive a shelf height adjustment during the bounds
+ * animation. In which case we can update the end bounds and keep the existing animation
+ * running instead of cancelling it.
+ */
+ public void updateEndValue(T endValue) {
+ mEndValue = endValue;
+ }
+
+ SurfaceControl.Transaction newSurfaceControlTransaction() {
+ return mSurfaceControlTransactionFactory.getTransaction();
+ }
+
+ @VisibleForTesting
+ public void setSurfaceControlTransactionFactory(
+ PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
+ mSurfaceControlTransactionFactory = factory;
+ }
+
+ PipSurfaceTransactionHelper getSurfaceTransactionHelper() {
+ return mSurfaceTransactionHelper;
+ }
+
+ void setSurfaceTransactionHelper(PipSurfaceTransactionHelper helper) {
+ mSurfaceTransactionHelper = helper;
+ }
+
+ void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {}
+
+ void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
+ @TransitionDirection int transitionDirection) {}
+
+ abstract void applySurfaceControlTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, float fraction);
+
+ static PipTransitionAnimator<Float> ofAlpha(SurfaceControl leash,
+ Rect destinationBounds, float startValue, float endValue) {
+ return new PipTransitionAnimator<Float>(leash, ANIM_TYPE_ALPHA,
+ destinationBounds, startValue, endValue) {
+ @Override
+ void applySurfaceControlTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, float fraction) {
+ final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction;
+ setCurrentValue(alpha);
+ getSurfaceTransactionHelper().alpha(tx, leash, alpha);
+ tx.apply();
+ }
+
+ @Override
+ void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ if (getTransitionDirection() == TRANSITION_DIRECTION_REMOVE_STACK) {
+ // while removing the pip stack, no extra work needs to be done here.
+ return;
+ }
+ getSurfaceTransactionHelper()
+ .resetScale(tx, leash, getDestinationBounds())
+ .crop(tx, leash, getDestinationBounds())
+ .round(tx, leash, shouldApplyCornerRadius());
+ tx.show(leash);
+ tx.apply();
+ }
+
+ @Override
+ public void updateEndValue(Float endValue) {
+ super.updateEndValue(endValue);
+ mStartValue = mCurrentValue;
+ }
+ };
+ }
+
+ static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash,
+ 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
+ final Rect initialSourceValue;
+ if (isOutPipDirection(direction)) {
+ initialSourceValue = new Rect(endValue);
+ } else {
+ initialSourceValue = new Rect(startValue);
+ }
+
+ final Rect sourceHintRectInsets;
+ if (sourceHintRect == null) {
+ sourceHintRectInsets = null;
+ } else {
+ sourceHintRectInsets = new Rect(sourceHintRect.left - initialSourceValue.left,
+ sourceHintRect.top - initialSourceValue.top,
+ initialSourceValue.right - sourceHintRect.right,
+ initialSourceValue.bottom - sourceHintRect.bottom);
+ }
+ final Rect sourceInsets = new Rect(0, 0, 0, 0);
+
+ // construct new Rect instances in case they are recycled
+ return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS,
+ endValue, 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 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);
+ }
+ } else {
+ final Rect insets;
+ if (isOutPipDirection(direction)) {
+ insets = mInsetsEvaluator.evaluate(fraction, sourceHintRectInsets,
+ sourceInsets);
+ } else {
+ insets = mInsetsEvaluator.evaluate(fraction, sourceInsets,
+ sourceHintRectInsets);
+ }
+ getSurfaceTransactionHelper().scaleAndCrop(tx, leash,
+ initialSourceValue, bounds, insets);
+ }
+ tx.apply();
+ }
+
+ @Override
+ void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ getSurfaceTransactionHelper()
+ .alpha(tx, leash, 1f)
+ .round(tx, leash, shouldApplyCornerRadius());
+ tx.show(leash);
+ tx.apply();
+ }
+
+ @Override
+ void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
+ int transitionDirection) {
+ // NOTE: intentionally does not apply the transaction here.
+ // this end transaction should get executed synchronously with the final
+ // WindowContainerTransaction in task organizer
+ final Rect destBounds = getDestinationBounds();
+ getSurfaceTransactionHelper().resetScale(tx, leash, destBounds);
+ if (transitionDirection == TRANSITION_DIRECTION_LEAVE_PIP) {
+ // Leaving to fullscreen, reset crop to null.
+ tx.setPosition(leash, destBounds.left, destBounds.top);
+ tx.setWindowCrop(leash, 0, 0);
+ } else {
+ getSurfaceTransactionHelper().crop(tx, leash, destBounds);
+ }
+ }
+
+ @Override
+ public void updateEndValue(Rect endValue) {
+ super.updateEndValue(endValue);
+ if (mStartValue != null && mCurrentValue != null) {
+ mStartValue.set(mCurrentValue);
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsHandler.java
new file mode 100644
index 000000000000..08318186a105
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsHandler.java
@@ -0,0 +1,507 @@
+/*
+ * 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.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+
+import android.annotation.NonNull;
+import android.app.ActivityTaskManager;
+import android.app.ActivityTaskManager.RootTaskInfo;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Size;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.DisplayLayout;
+
+import java.io.PrintWriter;
+
+/**
+ * Handles bounds calculation for PIP on Phone and other form factors, it keeps tracking variant
+ * state changes originated from Window Manager and is the source of truth for PiP window bounds.
+ */
+public class PipBoundsHandler {
+
+ private static final String TAG = PipBoundsHandler.class.getSimpleName();
+ private static final float INVALID_SNAP_FRACTION = -1f;
+
+ private final @NonNull PipBoundsState mPipBoundsState;
+ private final PipSnapAlgorithm mSnapAlgorithm;
+ private final DisplayInfo mDisplayInfo = new DisplayInfo();
+ private DisplayLayout mDisplayLayout;
+
+ private float mDefaultAspectRatio;
+ private float mMinAspectRatio;
+ private float mMaxAspectRatio;
+ private int mDefaultStackGravity;
+ private int mDefaultMinSize;
+ private Point mScreenEdgeInsets;
+ private int mCurrentMinSize;
+ private Size mOverrideMinimalSize;
+
+ private boolean mIsImeShowing;
+ private int mImeHeight;
+ private boolean mIsShelfShowing;
+ private int mShelfHeight;
+
+ public PipBoundsHandler(Context context, @NonNull PipBoundsState pipBoundsState) {
+ mPipBoundsState = pipBoundsState;
+ mSnapAlgorithm = new PipSnapAlgorithm(context);
+ mDisplayLayout = new DisplayLayout();
+ reloadResources(context);
+ // Initialize the aspect ratio to the default aspect ratio. Don't do this in reload
+ // resources as it would clobber mAspectRatio when entering PiP from fullscreen which
+ // triggers a configuration change and the resources to be reloaded.
+ mPipBoundsState.setAspectRatio(mDefaultAspectRatio);
+ }
+
+ /**
+ * TODO: move the resources to SysUI package.
+ */
+ private void reloadResources(Context context) {
+ final Resources res = context.getResources();
+ mDefaultAspectRatio = res.getFloat(
+ com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio);
+ mDefaultStackGravity = res.getInteger(
+ com.android.internal.R.integer.config_defaultPictureInPictureGravity);
+ mDefaultMinSize = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.default_minimal_size_pip_resizable_task);
+ mCurrentMinSize = mDefaultMinSize;
+ final String screenEdgeInsetsDpString = res.getString(
+ com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets);
+ final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
+ ? Size.parseSize(screenEdgeInsetsDpString)
+ : null;
+ mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
+ : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()),
+ dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics()));
+ mMinAspectRatio = res.getFloat(
+ com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
+ mMaxAspectRatio = res.getFloat(
+ com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
+ }
+
+ /**
+ * Sets or update latest {@link DisplayLayout} when new display added or rotation callbacks
+ * from {@link DisplayController.OnDisplaysChangedListener}
+ * @param newDisplayLayout latest {@link DisplayLayout}
+ */
+ public void setDisplayLayout(DisplayLayout newDisplayLayout) {
+ mDisplayLayout.set(newDisplayLayout);
+ }
+
+ /**
+ * Get the current saved display info.
+ */
+ public DisplayInfo getDisplayInfo() {
+ return mDisplayInfo;
+ }
+
+ /**
+ * Update the Min edge size for {@link PipSnapAlgorithm} to calculate corresponding bounds
+ * @param minEdgeSize
+ */
+ public void setMinEdgeSize(int minEdgeSize) {
+ mCurrentMinSize = minEdgeSize;
+ }
+
+ /**
+ * Sets both shelf visibility and its height if applicable.
+ * @return {@code true} if the internal shelf state is changed, {@code false} otherwise.
+ */
+ public boolean setShelfHeight(boolean shelfVisible, int shelfHeight) {
+ final boolean shelfShowing = shelfVisible && shelfHeight > 0;
+ if (shelfShowing == mIsShelfShowing && shelfHeight == mShelfHeight) {
+ return false;
+ }
+
+ mIsShelfShowing = shelfVisible;
+ mShelfHeight = shelfHeight;
+ return true;
+ }
+
+ /**
+ * Responds to IPinnedStackListener on IME visibility change.
+ */
+ public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+ mIsImeShowing = imeVisible;
+ mImeHeight = imeHeight;
+ }
+
+ /**
+ * Responds to IPinnedStackListener on movement bounds change.
+ * Note that both inset and normal bounds will be calculated here rather than in the caller.
+ */
+ public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
+ Rect animatingBounds, DisplayInfo displayInfo) {
+ getInsetBounds(insetBounds);
+ final Rect defaultBounds = getDefaultBounds(INVALID_SNAP_FRACTION, null);
+ normalBounds.set(defaultBounds);
+ if (animatingBounds.isEmpty()) {
+ animatingBounds.set(defaultBounds);
+ }
+ if (isValidPictureInPictureAspectRatio(mPipBoundsState.getAspectRatio())) {
+ transformBoundsToAspectRatio(normalBounds, mPipBoundsState.getAspectRatio(),
+ false /* useCurrentMinEdgeSize */, false /* useCurrentSize */);
+ }
+ displayInfo.copyFrom(mDisplayInfo);
+ }
+
+ /**
+ * The {@link PipSnapAlgorithm} is couple on display bounds
+ * @return {@link PipSnapAlgorithm}.
+ */
+ public PipSnapAlgorithm getSnapAlgorithm() {
+ return mSnapAlgorithm;
+ }
+
+ public Rect getDisplayBounds() {
+ return new Rect(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
+ }
+
+ public int getDisplayRotation() {
+ return mDisplayInfo.rotation;
+ }
+
+ /**
+ * Responds to IPinnedStackListener on {@link DisplayInfo} change.
+ * It will normally follow up with a
+ * {@link #onMovementBoundsChanged(Rect, Rect, Rect, DisplayInfo)} callback.
+ */
+ public void onDisplayInfoChanged(DisplayInfo displayInfo) {
+ mDisplayInfo.copyFrom(displayInfo);
+ }
+
+ /**
+ * Responds to IPinnedStackListener on configuration change.
+ */
+ public void onConfigurationChanged(Context context) {
+ reloadResources(context);
+ }
+
+ /**
+ * See {@link #getDestinationBounds(Rect, Size, boolean)}
+ */
+ public Rect getDestinationBounds(Rect bounds, Size minimalSize) {
+ return getDestinationBounds(bounds, minimalSize, false /* useCurrentMinEdgeSize */);
+ }
+
+ /**
+ * @return {@link Rect} of the destination PiP window bounds.
+ */
+ public Rect getDestinationBounds(Rect bounds, Size minimalSize, boolean useCurrentMinEdgeSize) {
+ boolean isReentryBounds = false;
+ final Rect destinationBounds;
+ if (bounds == null) {
+ // Calculating initial entry bounds
+ final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState();
+
+ final Rect defaultBounds;
+ if (state != null) {
+ // Restore to reentry bounds.
+ defaultBounds = getDefaultBounds(state.getSnapFraction(), state.getSize());
+ isReentryBounds = true;
+ } else {
+ // Get actual default bounds.
+ defaultBounds = getDefaultBounds(INVALID_SNAP_FRACTION, null /* size */);
+ mOverrideMinimalSize = minimalSize;
+ }
+
+ destinationBounds = new Rect(defaultBounds);
+ } else {
+ // Just adjusting bounds (e.g. on aspect ratio changed).
+ destinationBounds = new Rect(bounds);
+ }
+ if (isValidPictureInPictureAspectRatio(mPipBoundsState.getAspectRatio())) {
+ transformBoundsToAspectRatio(destinationBounds, mPipBoundsState.getAspectRatio(),
+ useCurrentMinEdgeSize, isReentryBounds);
+ }
+ return destinationBounds;
+ }
+
+ public float getDefaultAspectRatio() {
+ return mDefaultAspectRatio;
+ }
+
+ public void onOverlayChanged(Context context, Display display) {
+ mDisplayLayout = new DisplayLayout(context, display);
+ }
+
+ /**
+ * Updatest the display info and display layout on rotation change. This is needed even when we
+ * aren't in PIP because the rotation layout is used to calculate the proper insets for the
+ * next enter animation into PIP.
+ */
+ public void onDisplayRotationChangedNotInPip(Context context, int toRotation) {
+ // Update the display layout, note that we have to do this on every rotation even if we
+ // aren't in PIP since we need to update the display layout to get the right resources
+ mDisplayLayout.rotateTo(context.getResources(), toRotation);
+
+ // Populate the new {@link #mDisplayInfo}.
+ // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation,
+ // therefore, the width/height may require a swap first.
+ // Moving forward, we should get the new dimensions after rotation from DisplayLayout.
+ mDisplayInfo.rotation = toRotation;
+ updateDisplayInfoIfNeeded();
+ }
+
+ /**
+ * Updates the display info, calculating and returning the new stack and movement bounds in the
+ * new orientation of the device if necessary.
+ *
+ * @return {@code true} if internal {@link DisplayInfo} is rotated, {@code false} otherwise.
+ */
+ public boolean onDisplayRotationChanged(Context context, Rect outBounds, Rect oldBounds,
+ Rect outInsetBounds,
+ int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) {
+ // Bail early if the event is not sent to current {@link #mDisplayInfo}
+ if ((displayId != mDisplayInfo.displayId) || (fromRotation == toRotation)) {
+ return false;
+ }
+
+ // Bail early if the pinned task is staled.
+ final RootTaskInfo pinnedTaskInfo;
+ try {
+ pinnedTaskInfo = ActivityTaskManager.getService()
+ .getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
+ if (pinnedTaskInfo == null) return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get RootTaskInfo for pinned task", e);
+ return false;
+ }
+
+ // Calculate the snap fraction of the current stack along the old movement bounds
+ final Rect postChangeStackBounds = new Rect(oldBounds);
+ final float snapFraction = getSnapFraction(postChangeStackBounds);
+
+ // Update the display layout
+ mDisplayLayout.rotateTo(context.getResources(), toRotation);
+
+ // Populate the new {@link #mDisplayInfo}.
+ // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation,
+ // therefore, the width/height may require a swap first.
+ // Moving forward, we should get the new dimensions after rotation from DisplayLayout.
+ mDisplayInfo.rotation = toRotation;
+ updateDisplayInfoIfNeeded();
+
+ // Calculate the stack bounds in the new orientation based on same fraction along the
+ // rotated movement bounds.
+ final Rect postChangeMovementBounds = getMovementBounds(postChangeStackBounds,
+ false /* adjustForIme */);
+ mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
+ snapFraction);
+
+ getInsetBounds(outInsetBounds);
+ outBounds.set(postChangeStackBounds);
+ t.setBounds(pinnedTaskInfo.token, outBounds);
+ return true;
+ }
+
+ private void updateDisplayInfoIfNeeded() {
+ final boolean updateNeeded;
+ if ((mDisplayInfo.rotation == ROTATION_0) || (mDisplayInfo.rotation == ROTATION_180)) {
+ updateNeeded = (mDisplayInfo.logicalWidth > mDisplayInfo.logicalHeight);
+ } else {
+ updateNeeded = (mDisplayInfo.logicalWidth < mDisplayInfo.logicalHeight);
+ }
+ if (updateNeeded) {
+ final int newLogicalHeight = mDisplayInfo.logicalWidth;
+ mDisplayInfo.logicalWidth = mDisplayInfo.logicalHeight;
+ mDisplayInfo.logicalHeight = newLogicalHeight;
+ }
+ }
+
+ /**
+ * @return whether the given {@param aspectRatio} is valid.
+ */
+ private boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
+ return Float.compare(mMinAspectRatio, aspectRatio) <= 0
+ && Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
+ }
+
+ /**
+ * Sets the current bound with the currently store aspect ratio.
+ * @param stackBounds
+ */
+ public void transformBoundsToAspectRatio(Rect stackBounds) {
+ transformBoundsToAspectRatio(stackBounds, mPipBoundsState.getAspectRatio(),
+ true /* useCurrentMinEdgeSize */, true /* useCurrentSize */);
+ }
+
+ /**
+ * Set the current bounds (or the default bounds if there are no current bounds) with the
+ * specified aspect ratio.
+ */
+ private void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
+ boolean useCurrentMinEdgeSize, boolean useCurrentSize) {
+ // Save the snap fraction and adjust the size based on the new aspect ratio.
+ final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
+ getMovementBounds(stackBounds));
+ final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize;
+ final Size size;
+ if (useCurrentMinEdgeSize || useCurrentSize) {
+ size = mSnapAlgorithm.getSizeForAspectRatio(
+ new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize);
+ } else {
+ size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize,
+ mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
+ }
+
+ final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
+ final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
+ stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
+ // apply the override minimal size if applicable, this minimal size is specified by app
+ if (mOverrideMinimalSize != null) {
+ transformBoundsToMinimalSize(stackBounds, aspectRatio, mOverrideMinimalSize);
+ }
+ mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
+ }
+
+ /**
+ * Transforms a given bounds to meet the minimal size constraints.
+ * This function assumes the given {@param stackBounds} qualifies {@param aspectRatio}.
+ */
+ private void transformBoundsToMinimalSize(Rect stackBounds, float aspectRatio,
+ Size minimalSize) {
+ if (minimalSize == null) return;
+ final Size adjustedMinimalSize;
+ final float minimalSizeAspectRatio =
+ minimalSize.getWidth() / (float) minimalSize.getHeight();
+ if (minimalSizeAspectRatio > aspectRatio) {
+ // minimal size is wider, fixed the width and increase the height
+ adjustedMinimalSize = new Size(
+ minimalSize.getWidth(), (int) (minimalSize.getWidth() / aspectRatio));
+ } else {
+ adjustedMinimalSize = new Size(
+ (int) (minimalSize.getHeight() * aspectRatio), minimalSize.getHeight());
+ }
+ final Rect containerBounds = new Rect(stackBounds);
+ Gravity.apply(mDefaultStackGravity,
+ adjustedMinimalSize.getWidth(), adjustedMinimalSize.getHeight(),
+ containerBounds, stackBounds);
+ }
+
+ /**
+ * @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are
+ * provided, then it will apply the default bounds to the provided snap fraction and size.
+ */
+ private Rect getDefaultBounds(float snapFraction, Size size) {
+ final Rect defaultBounds = new Rect();
+ if (snapFraction != INVALID_SNAP_FRACTION && size != null) {
+ defaultBounds.set(0, 0, size.getWidth(), size.getHeight());
+ final Rect movementBounds = getMovementBounds(defaultBounds);
+ mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
+ } else {
+ final Rect insetBounds = new Rect();
+ getInsetBounds(insetBounds);
+ size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio,
+ mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
+ Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds,
+ 0, Math.max(mIsImeShowing ? mImeHeight : 0,
+ mIsShelfShowing ? mShelfHeight : 0),
+ defaultBounds);
+ }
+ return defaultBounds;
+ }
+
+ /**
+ * Populates the bounds on the screen that the PIP can be visible in.
+ */
+ protected void getInsetBounds(Rect outRect) {
+ Rect insets = mDisplayLayout.stableInsets();
+ outRect.set(insets.left + mScreenEdgeInsets.x,
+ insets.top + mScreenEdgeInsets.y,
+ mDisplayInfo.logicalWidth - insets.right - mScreenEdgeInsets.x,
+ mDisplayInfo.logicalHeight - insets.bottom - mScreenEdgeInsets.y);
+ }
+
+ /**
+ * @return the movement bounds for the given {@param stackBounds} and the current state of the
+ * controller.
+ */
+ private Rect getMovementBounds(Rect stackBounds) {
+ return getMovementBounds(stackBounds, true /* adjustForIme */);
+ }
+
+ /**
+ * @return the movement bounds for the given {@param stackBounds} and the current state of the
+ * controller.
+ */
+ private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
+ final Rect movementBounds = new Rect();
+ getInsetBounds(movementBounds);
+
+ // Apply the movement bounds adjustments based on the current state.
+ mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds,
+ (adjustForIme && mIsImeShowing) ? mImeHeight : 0);
+ return movementBounds;
+ }
+
+ /**
+ * @return the default snap fraction to apply instead of the default gravity when calculating
+ * the default stack bounds when first entering PiP.
+ */
+ public float getSnapFraction(Rect stackBounds) {
+ return mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds));
+ }
+
+ /**
+ * Applies the given snap fraction to the given stack bounds.
+ */
+ public void applySnapFraction(Rect stackBounds, float snapFraction) {
+ final Rect movementBounds = getMovementBounds(stackBounds);
+ mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction);
+ }
+
+ /**
+ * @return the pixels for a given dp value.
+ */
+ private int dpToPx(float dpValue, DisplayMetrics dm) {
+ return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
+ }
+
+ /**
+ * Dumps internal states.
+ */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mDisplayInfo=" + mDisplayInfo);
+ pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio);
+ pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio);
+ pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio);
+ pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity);
+ pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
+ pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
+ pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
+ pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight);
+ pw.println(innerPrefix + "mSnapAlgorithm" + mSnapAlgorithm);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
new file mode 100644
index 000000000000..aba2a3a29fe2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
@@ -0,0 +1,139 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.graphics.Rect;
+import android.util.Size;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/**
+ * Singleton source of truth for the current state of PIP bounds.
+ */
+public final class PipBoundsState {
+ private static final String TAG = PipBoundsState.class.getSimpleName();
+
+ private final @NonNull Rect mBounds = new Rect();
+ private float mAspectRatio;
+ private PipReentryState mPipReentryState;
+ private ComponentName mLastPipComponentName;
+
+ void setBounds(@NonNull Rect bounds) {
+ mBounds.set(bounds);
+ }
+
+ @NonNull
+ public Rect getBounds() {
+ return new Rect(mBounds);
+ }
+
+ public void setAspectRatio(float aspectRatio) {
+ mAspectRatio = aspectRatio;
+ }
+
+ public float getAspectRatio() {
+ return mAspectRatio;
+ }
+
+ /**
+ * Save the reentry state to restore to when re-entering PIP mode.
+ *
+ * TODO(b/169373982): consider refactoring this so that this class alone can use mBounds and
+ * calculate the snap fraction to save for re-entry.
+ */
+ public void saveReentryState(@NonNull Rect bounds, float fraction) {
+ mPipReentryState = new PipReentryState(new Size(bounds.width(), bounds.height()), fraction);
+ }
+
+ /**
+ * Returns the saved reentry state.
+ */
+ @Nullable
+ public PipReentryState getReentryState() {
+ return mPipReentryState;
+ }
+
+ /**
+ * Set the last {@link ComponentName} to enter PIP mode.
+ */
+ public void setLastPipComponentName(ComponentName lastPipComponentName) {
+ final boolean changed = !Objects.equals(mLastPipComponentName, lastPipComponentName);
+ mLastPipComponentName = lastPipComponentName;
+ if (changed) {
+ clearReentryState();
+ }
+ }
+
+ public ComponentName getLastPipComponentName() {
+ return mLastPipComponentName;
+ }
+
+ @VisibleForTesting
+ void clearReentryState() {
+ mPipReentryState = null;
+ }
+
+ static final class PipReentryState {
+ private static final String TAG = PipReentryState.class.getSimpleName();
+
+ private final @NonNull Size mSize;
+ private final float mSnapFraction;
+
+ PipReentryState(@NonNull Size size, float snapFraction) {
+ mSize = size;
+ mSnapFraction = snapFraction;
+ }
+
+ @NonNull
+ Size getSize() {
+ return mSize;
+ }
+
+ float getSnapFraction() {
+ return mSnapFraction;
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mSize=" + mSize);
+ pw.println(innerPrefix + "mSnapFraction=" + mSnapFraction);
+ }
+ }
+
+ /**
+ * Dumps internal state.
+ */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mBounds=" + mBounds);
+ pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName);
+ pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio);
+ if (mPipReentryState == null) {
+ pw.println(innerPrefix + "mPipReentryState=null");
+ } else {
+ mPipReentryState.dump(pw, innerPrefix);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java
new file mode 100644
index 000000000000..820930c463f2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java
@@ -0,0 +1,204 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.util.Size;
+
+/**
+ * Calculates the snap targets and the snap position for the PIP given a position and a velocity.
+ * All bounds are relative to the display top/left.
+ */
+public class PipSnapAlgorithm {
+
+ private final float mDefaultSizePercent;
+ private final float mMinAspectRatioForMinSize;
+ private final float mMaxAspectRatioForMinSize;
+
+ public PipSnapAlgorithm(Context context) {
+ Resources res = context.getResources();
+ mDefaultSizePercent = res.getFloat(
+ com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent);
+ mMaxAspectRatioForMinSize = res.getFloat(
+ com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
+ mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
+ }
+
+ /**
+ * @return returns a fraction that describes where along the {@param movementBounds} the
+ * {@param stackBounds} are. If the {@param stackBounds} are not currently on the
+ * {@param movementBounds} exactly, then they will be snapped to the movement bounds.
+ *
+ * The fraction is defined in a clockwise fashion against the {@param movementBounds}:
+ *
+ * 0 1
+ * 4 +---+ 1
+ * | |
+ * 3 +---+ 2
+ * 3 2
+ */
+ public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
+ final Rect tmpBounds = new Rect();
+ snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds);
+ final float widthFraction = (float) (tmpBounds.left - movementBounds.left) /
+ movementBounds.width();
+ final float heightFraction = (float) (tmpBounds.top - movementBounds.top) /
+ movementBounds.height();
+ if (tmpBounds.top == movementBounds.top) {
+ return widthFraction;
+ } else if (tmpBounds.left == movementBounds.right) {
+ return 1f + heightFraction;
+ } else if (tmpBounds.top == movementBounds.bottom) {
+ return 2f + (1f - widthFraction);
+ } else {
+ return 3f + (1f - heightFraction);
+ }
+ }
+
+ /**
+ * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction.
+ * See {@link #getSnapFraction(Rect, Rect)}.
+ *
+ * The fraction is define in a clockwise fashion against the {@param movementBounds}:
+ *
+ * 0 1
+ * 4 +---+ 1
+ * | |
+ * 3 +---+ 2
+ * 3 2
+ */
+ public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
+ if (snapFraction < 1f) {
+ int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
+ stackBounds.offsetTo(offset, movementBounds.top);
+ } else if (snapFraction < 2f) {
+ snapFraction -= 1f;
+ int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
+ stackBounds.offsetTo(movementBounds.right, offset);
+ } else if (snapFraction < 3f) {
+ snapFraction -= 2f;
+ int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
+ stackBounds.offsetTo(offset, movementBounds.bottom);
+ } else {
+ snapFraction -= 3f;
+ int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
+ stackBounds.offsetTo(movementBounds.left, offset);
+ }
+ }
+
+ /**
+ * Adjusts {@param movementBoundsOut} so that it is the movement bounds for the given
+ * {@param stackBounds}.
+ */
+ public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
+ int bottomOffset) {
+ // Adjust the right/bottom to ensure the stack bounds never goes offscreen
+ movementBoundsOut.set(insetBounds);
+ movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right -
+ stackBounds.width());
+ movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom -
+ stackBounds.height());
+ movementBoundsOut.bottom -= bottomOffset;
+ }
+
+ /**
+ * @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge
+ * is at least {@param minEdgeSize}.
+ */
+ public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
+ int displayHeight) {
+ final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
+ final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
+
+ final int width;
+ final int height;
+ if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
+ // Beyond these points, we can just use the min size as the shorter edge
+ if (aspectRatio <= 1) {
+ // Portrait, width is the minimum size
+ width = minSize;
+ height = Math.round(width / aspectRatio);
+ } else {
+ // Landscape, height is the minimum size
+ height = minSize;
+ width = Math.round(height * aspectRatio);
+ }
+ } else {
+ // Within these points, we ensure that the bounds fit within the radius of the limits
+ // at the points
+ final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
+ final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
+ height = (int) Math.round(Math.sqrt((radius * radius) /
+ (aspectRatio * aspectRatio + 1)));
+ width = Math.round(height * aspectRatio);
+ }
+ return new Size(width, height);
+ }
+
+ /**
+ * @return the adjusted size so that it conforms to the given aspectRatio, ensuring that the
+ * minimum edge is at least minEdgeSize.
+ */
+ public Size getSizeForAspectRatio(Size size, float aspectRatio, float minEdgeSize) {
+ final int smallestSize = Math.min(size.getWidth(), size.getHeight());
+ final int minSize = (int) Math.max(minEdgeSize, smallestSize);
+
+ final int width;
+ final int height;
+ if (aspectRatio <= 1) {
+ // Portrait, width is the minimum size.
+ width = minSize;
+ height = Math.round(width / aspectRatio);
+ } else {
+ // Landscape, height is the minimum size
+ height = minSize;
+ width = Math.round(height * aspectRatio);
+ }
+ return new Size(width, height);
+ }
+
+ /**
+ * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes
+ * the new bounds out to {@param boundsOut}.
+ */
+ public void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) {
+ final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
+ stackBounds.left));
+ final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
+ stackBounds.top));
+ boundsOut.set(stackBounds);
+
+ // Otherwise, just find the closest edge
+ final int fromLeft = Math.abs(stackBounds.left - movementBounds.left);
+ final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
+ final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
+ final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
+ final int shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom));
+ if (shortest == fromLeft) {
+ boundsOut.offsetTo(movementBounds.left, boundedTop);
+ } else if (shortest == fromTop) {
+ boundsOut.offsetTo(boundedLeft, movementBounds.top);
+ } else if (shortest == fromRight) {
+ boundsOut.offsetTo(movementBounds.right, boundedTop);
+ } else {
+ boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
new file mode 100644
index 000000000000..b9a5536de743
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -0,0 +1,147 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+
+/**
+ * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition.
+ */
+public class PipSurfaceTransactionHelper {
+
+ private final boolean mEnableCornerRadius;
+ private int mCornerRadius;
+
+ /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */
+ private final Matrix mTmpTransform = new Matrix();
+ private final float[] mTmpFloat9 = new float[9];
+ private final RectF mTmpSourceRectF = new RectF();
+ private final RectF mTmpDestinationRectF = new RectF();
+ private final Rect mTmpDestinationRect = new Rect();
+
+ public PipSurfaceTransactionHelper(Context context) {
+ final Resources res = context.getResources();
+ mEnableCornerRadius = res.getBoolean(R.bool.config_pipEnableRoundCorner);
+ }
+
+ /**
+ * Called when display size or font size of settings changed
+ *
+ * @param context the current context
+ */
+ public void onDensityOrFontScaleChanged(Context context) {
+ if (mEnableCornerRadius) {
+ final Resources res = context.getResources();
+ mCornerRadius = res.getDimensionPixelSize(R.dimen.pip_corner_radius);
+ }
+ }
+
+ /**
+ * Operates the alpha on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash,
+ float alpha) {
+ tx.setAlpha(leash, alpha);
+ return this;
+ }
+
+ /**
+ * Operates the crop (and position) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect destinationBounds) {
+ tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
+ .setPosition(leash, destinationBounds.left, destinationBounds.top);
+ return this;
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, Rect destinationBounds) {
+ mTmpSourceRectF.set(sourceBounds);
+ mTmpDestinationRectF.set(destinationBounds);
+ mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
+ tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
+ .setPosition(leash, mTmpDestinationRectF.left, mTmpDestinationRectF.top);
+ return this;
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx,
+ SurfaceControl leash,
+ Rect sourceBounds, Rect destinationBounds, Rect insets) {
+ mTmpSourceRectF.set(sourceBounds);
+ mTmpDestinationRect.set(sourceBounds);
+ mTmpDestinationRect.inset(insets);
+ // Scale by the shortest edge and offset such that the top/left of the scaled inset source
+ // rect aligns with the top/left of the destination bounds
+ final float scale = sourceBounds.width() <= sourceBounds.height()
+ ? (float) destinationBounds.width() / sourceBounds.width()
+ : (float) destinationBounds.height() / sourceBounds.height();
+ final float left = destinationBounds.left - insets.left * scale;
+ final float top = destinationBounds.top - insets.top * scale;
+ mTmpTransform.setScale(scale, scale);
+ tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
+ .setWindowCrop(leash, mTmpDestinationRect)
+ .setPosition(leash, left, top);
+ return this;
+ }
+
+ /**
+ * Resets the scale (setMatrix) on a given transaction and leash if there's any
+ *
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx,
+ SurfaceControl leash,
+ Rect destinationBounds) {
+ tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9)
+ .setPosition(leash, destinationBounds.left, destinationBounds.top);
+ return this;
+ }
+
+ /**
+ * Operates the round corner radius on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash,
+ boolean applyCornerRadius) {
+ if (mEnableCornerRadius) {
+ tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0);
+ }
+ return this;
+ }
+
+ public interface SurfaceControlTransactionFactory {
+ SurfaceControl.Transaction getTransaction();
+ }
+}
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
new file mode 100644
index 000000000000..a28477574605
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -0,0 +1,1200 @@
+/*
+ * 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.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP;
+import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
+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_TO_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection;
+import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.PictureInPictureParams;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.WindowManager;
+import android.window.TaskOrganizer;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransactionCallback;
+
+import com.android.internal.jank.InteractionJankMonitor;
+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;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+/**
+ * Manages PiP tasks such as resize and offset.
+ *
+ * This class listens on {@link TaskOrganizer} callbacks for windowing mode change
+ * both to and from PiP and issues corresponding animation if applicable.
+ * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running
+ * and files a final {@link WindowContainerTransaction} at the end of the transition.
+ *
+ * This class is also responsible for general resize/offset PiP operations within SysUI component,
+ * see also {@link PipMotionHelper}.
+ */
+public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
+ DisplayController.OnDisplaysChangedListener {
+ private static final String TAG = PipTaskOrganizer.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final int MSG_RESIZE_IMMEDIATE = 1;
+ private static final int MSG_RESIZE_ANIMATE = 2;
+ private static final int MSG_OFFSET_ANIMATE = 3;
+ private static final int MSG_FINISH_RESIZE = 4;
+ private static final int MSG_RESIZE_USER = 5;
+
+ // Not a complete set of states but serves what we want right now.
+ private enum State {
+ UNDEFINED(0),
+ TASK_APPEARED(1),
+ ENTERING_PIP(2),
+ ENTERED_PIP(3),
+ EXITING_PIP(4);
+
+ private final int mStateValue;
+
+ State(int value) {
+ mStateValue = value;
+ }
+
+ private boolean isInPip() {
+ return mStateValue >= TASK_APPEARED.mStateValue
+ && mStateValue != EXITING_PIP.mStateValue;
+ }
+
+ /**
+ * Resize request can be initiated in other component, ignore if we are no longer in PIP,
+ * still waiting for animation or we're exiting from it.
+ *
+ * @return {@code true} if the resize request should be blocked/ignored.
+ */
+ private boolean shouldBlockResizeRequest() {
+ return mStateValue < ENTERING_PIP.mStateValue
+ || mStateValue == EXITING_PIP.mStateValue;
+ }
+ }
+
+ private final Handler mMainHandler;
+ private final Handler mUpdateHandler;
+ private final PipBoundsState mPipBoundsState;
+ private final PipBoundsHandler mPipBoundsHandler;
+ private final PipAnimationController mPipAnimationController;
+ private final PipUiEventLogger mPipUiEventLoggerLogger;
+ private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
+ private final int mEnterExitAnimationDuration;
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final Map<IBinder, Configuration> mInitialState = new HashMap<>();
+ private final Optional<SplitScreen> mSplitScreenOptional;
+ protected final ShellTaskOrganizer mTaskOrganizer;
+ private SurfaceControlViewHost mPipViewHost;
+ private SurfaceControl mPipMenuSurface;
+
+ // These callbacks are called on the update thread
+ private final PipAnimationController.PipAnimationCallback mPipAnimationCallback =
+ new PipAnimationController.PipAnimationCallback() {
+ @Override
+ public void onPipAnimationStart(PipAnimationController.PipTransitionAnimator animator) {
+ final int direction = animator.getTransitionDirection();
+ if (direction == TRANSITION_DIRECTION_TO_PIP) {
+ InteractionJankMonitor.getInstance().begin(
+ InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000);
+ }
+ sendOnPipTransitionStarted(direction);
+ }
+
+ @Override
+ public void onPipAnimationEnd(SurfaceControl.Transaction tx,
+ PipAnimationController.PipTransitionAnimator animator) {
+ final int direction = animator.getTransitionDirection();
+ finishResize(tx, animator.getDestinationBounds(), direction,
+ animator.getAnimationType());
+ sendOnPipTransitionFinished(direction);
+ if (direction == TRANSITION_DIRECTION_TO_PIP) {
+ InteractionJankMonitor.getInstance().end(
+ InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
+ }
+ }
+
+ @Override
+ public void onPipAnimationCancel(PipAnimationController.PipTransitionAnimator animator) {
+ sendOnPipTransitionCancelled(animator.getTransitionDirection());
+ }
+ };
+
+ @SuppressWarnings("unchecked")
+ private final Handler.Callback mUpdateCallbacks = (msg) -> {
+ SomeArgs args = (SomeArgs) msg.obj;
+ Consumer<Rect> updateBoundsCallback = (Consumer<Rect>) args.arg1;
+ switch (msg.what) {
+ case MSG_RESIZE_IMMEDIATE: {
+ Rect toBounds = (Rect) args.arg2;
+ resizePip(toBounds);
+ if (updateBoundsCallback != null) {
+ updateBoundsCallback.accept(toBounds);
+ }
+ break;
+ }
+ case MSG_RESIZE_ANIMATE: {
+ Rect currentBounds = (Rect) args.arg2;
+ Rect toBounds = (Rect) args.arg3;
+ Rect sourceHintRect = (Rect) args.arg4;
+ int duration = args.argi2;
+ animateResizePip(currentBounds, toBounds, sourceHintRect,
+ args.argi1 /* direction */, duration);
+ if (updateBoundsCallback != null) {
+ updateBoundsCallback.accept(toBounds);
+ }
+ break;
+ }
+ case MSG_OFFSET_ANIMATE: {
+ Rect originalBounds = (Rect) args.arg2;
+ final int offset = args.argi1;
+ final int duration = args.argi2;
+ offsetPip(originalBounds, 0 /* xOffset */, offset, duration);
+ Rect toBounds = new Rect(originalBounds);
+ toBounds.offset(0, offset);
+ if (updateBoundsCallback != null) {
+ updateBoundsCallback.accept(toBounds);
+ }
+ break;
+ }
+ case MSG_FINISH_RESIZE: {
+ SurfaceControl.Transaction tx = (SurfaceControl.Transaction) args.arg2;
+ Rect toBounds = (Rect) args.arg3;
+ finishResize(tx, toBounds, args.argi1 /* direction */, -1);
+ if (updateBoundsCallback != null) {
+ updateBoundsCallback.accept(toBounds);
+ }
+ break;
+ }
+ case MSG_RESIZE_USER: {
+ Rect startBounds = (Rect) args.arg2;
+ Rect toBounds = (Rect) args.arg3;
+ userResizePip(startBounds, toBounds);
+ if (updateBoundsCallback != null) {
+ updateBoundsCallback.accept(toBounds);
+ }
+ break;
+ }
+ }
+ args.recycle();
+ return true;
+ };
+
+ private ActivityManager.RunningTaskInfo mTaskInfo;
+ private WindowContainerToken mToken;
+ private SurfaceControl mLeash;
+ private State mState = State.UNDEFINED;
+ private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+ private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+ private PictureInPictureParams mPictureInPictureParams;
+ private IntConsumer mOnDisplayIdChangeCallback;
+
+ /**
+ * If set to {@code true}, the entering animation will be skipped and we will wait for
+ * {@link #onFixedRotationFinished(int)} callback to actually enter PiP.
+ */
+ private boolean mShouldDeferEnteringPip;
+
+ /**
+ * If set to {@code true}, no entering PiP transition would be kicked off and most likely
+ * it's due to the fact that Launcher is handling the transition directly when swiping
+ * auto PiP-able Activity to home.
+ * See also {@link #startSwipePipToHome(ComponentName, ActivityInfo, PictureInPictureParams)}.
+ */
+ private boolean mShouldIgnoreEnteringPipTransition;
+
+ public PipTaskOrganizer(Context context, @NonNull PipBoundsState pipBoundsState,
+ @NonNull PipBoundsHandler boundsHandler,
+ @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper,
+ Optional<SplitScreen> splitScreenOptional,
+ @NonNull DisplayController displayController,
+ @NonNull PipUiEventLogger pipUiEventLogger,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer) {
+ mMainHandler = new Handler(Looper.getMainLooper());
+ mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks);
+ mPipBoundsState = pipBoundsState;
+ mPipBoundsHandler = boundsHandler;
+ mEnterExitAnimationDuration = context.getResources()
+ .getInteger(R.integer.config_pipResizeAnimationDuration);
+ mSurfaceTransactionHelper = surfaceTransactionHelper;
+ mPipAnimationController = new PipAnimationController(mSurfaceTransactionHelper);
+ mPipUiEventLoggerLogger = pipUiEventLogger;
+ mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mSplitScreenOptional = splitScreenOptional;
+ mTaskOrganizer = shellTaskOrganizer;
+ mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP);
+ displayController.addDisplayWindowListener(this);
+ }
+
+ public Handler getUpdateHandler() {
+ return mUpdateHandler;
+ }
+
+ public Rect getCurrentOrAnimatingBounds() {
+ PipAnimationController.PipTransitionAnimator animator =
+ mPipAnimationController.getCurrentAnimator();
+ if (animator != null && animator.isRunning()) {
+ return new Rect(animator.getDestinationBounds());
+ }
+ return mPipBoundsState.getBounds();
+ }
+
+ public boolean isInPip() {
+ return mState.isInPip();
+ }
+
+ public boolean isDeferringEnterPipAnimation() {
+ return mState.isInPip() && mShouldDeferEnteringPip;
+ }
+
+ /**
+ * Registers {@link PipTransitionCallback} to receive transition callbacks.
+ */
+ public void registerPipTransitionCallback(PipTransitionCallback callback) {
+ mPipTransitionCallbacks.add(callback);
+ }
+
+ /**
+ * Registers a callback when a display change has been detected when we enter PiP.
+ */
+ public void registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback) {
+ mOnDisplayIdChangeCallback = onDisplayIdChangeCallback;
+ }
+
+ /**
+ * Sets the preferred animation type for one time.
+ * This is typically used to set the animation type to
+ * {@link PipAnimationController#ANIM_TYPE_ALPHA}.
+ */
+ public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) {
+ mOneShotAnimationType = animationType;
+ }
+
+ /**
+ * Callback when Launcher starts swipe-pip-to-home operation.
+ * @return {@link Rect} for destination bounds.
+ */
+ public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
+ PictureInPictureParams pictureInPictureParams) {
+ mShouldIgnoreEnteringPipTransition = true;
+ sendOnPipTransitionStarted(componentName, TRANSITION_DIRECTION_TO_PIP);
+ mPipBoundsState.setLastPipComponentName(componentName);
+ mPipBoundsState.setAspectRatio(getAspectRatioOrDefault(pictureInPictureParams));
+ return mPipBoundsHandler.getDestinationBounds(null /* bounds */,
+ getMinimalSize(activityInfo));
+ }
+
+ /**
+ * Callback when launcher finishes swipe-pip-to-home operation.
+ * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards.
+ */
+ public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) {
+ // do nothing if there is no startSwipePipToHome being called before
+ if (mShouldIgnoreEnteringPipTransition) {
+ mPipBoundsState.setBounds(destinationBounds);
+ }
+ }
+
+ /**
+ * Expands PiP to the previous bounds, this is done in two phases using
+ * {@link WindowContainerTransaction}
+ * - setActivityWindowingMode to either fullscreen or split-secondary at beginning of the
+ * transaction. without changing the windowing mode of the Task itself. This makes sure the
+ * activity render it's final configuration while the Task is still in PiP.
+ * - setWindowingMode to undefined at the end of transition
+ * @param animationDurationMs duration in millisecond for the exiting PiP transition
+ */
+ public void exitPip(int animationDurationMs) {
+ if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) {
+ Log.wtf(TAG, "Not allowed to exitPip in current state"
+ + " mState=" + mState + " mToken=" + mToken);
+ return;
+ }
+
+ final Configuration initialConfig = mInitialState.remove(mToken.asBinder());
+ if (initialConfig == null) {
+ Log.wtf(TAG, "Token not in record, this should not happen mToken=" + mToken);
+ return;
+ }
+ mPipUiEventLoggerLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN);
+ final boolean orientationDiffers = initialConfig.windowConfiguration.getRotation()
+ != mPipBoundsHandler.getDisplayRotation();
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final Rect destinationBounds = initialConfig.windowConfiguration.getBounds();
+ final int direction = syncWithSplitScreenBounds(destinationBounds)
+ ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN
+ : TRANSITION_DIRECTION_LEAVE_PIP;
+ if (orientationDiffers) {
+ mState = State.EXITING_PIP;
+ // Send started callback though animation is ignored.
+ sendOnPipTransitionStarted(direction);
+ // Don't bother doing an animation if the display rotation differs or if it's in
+ // a non-supported windowing mode
+ applyWindowingModeChangeOnExit(wct, direction);
+ mTaskOrganizer.applyTransaction(wct);
+ // Send finished callback though animation is ignored.
+ sendOnPipTransitionFinished(direction);
+ } else {
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds,
+ mPipBoundsState.getBounds());
+ tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height());
+ // We set to fullscreen here for now, but later it will be set to UNDEFINED for
+ // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit.
+ wct.setActivityWindowingMode(mToken,
+ direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN
+ ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
+ : WINDOWING_MODE_FULLSCREEN);
+ wct.setBounds(mToken, destinationBounds);
+ wct.setBoundsChangeTransaction(mToken, tx);
+ mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() {
+ @Override
+ public void onTransactionReady(int id, SurfaceControl.Transaction t) {
+ t.apply();
+ // Make sure to grab the latest source hint rect as it could have been updated
+ // right after applying the windowing mode change.
+ final Rect sourceHintRect = getValidSourceHintRect(mPictureInPictureParams,
+ destinationBounds);
+ scheduleAnimateResizePip(mPipBoundsState.getBounds(), destinationBounds,
+ sourceHintRect, direction, animationDurationMs,
+ null /* updateBoundsCallback */);
+ mState = State.EXITING_PIP;
+ }
+ });
+ }
+ }
+
+ private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) {
+ // Reset the final windowing mode.
+ wct.setWindowingMode(mToken, getOutPipWindowingMode());
+ // Simply reset the activity mode set prior to the animation running.
+ wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+ mSplitScreenOptional.ifPresent(splitScreen -> {
+ if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) {
+ wct.reparent(mToken, splitScreen.getSecondaryRoot(), true /* onTop */);
+ }
+ });
+ }
+
+ /**
+ * Removes PiP immediately.
+ */
+ public void removePip() {
+ if (!mState.isInPip() || mToken == null) {
+ Log.wtf(TAG, "Not allowed to removePip in current state"
+ + " mState=" + mState + " mToken=" + mToken);
+ return;
+ }
+
+ // removePipImmediately is expected when the following animation finishes.
+ mUpdateHandler.post(() -> mPipAnimationController
+ .getAnimator(mLeash, mPipBoundsState.getBounds(), 1f, 0f)
+ .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK)
+ .setPipAnimationCallback(mPipAnimationCallback)
+ .setDuration(mEnterExitAnimationDuration)
+ .start());
+ mInitialState.remove(mToken.asBinder());
+ mState = State.EXITING_PIP;
+ }
+
+ private void removePipImmediately() {
+ try {
+ // Reset the task bounds first to ensure the activity configuration is reset as well
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(mToken, null);
+ mTaskOrganizer.applyTransaction(wct);
+
+ ActivityTaskManager.getService().removeRootTasksInWindowingModes(
+ new int[]{ WINDOWING_MODE_PINNED });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to remove PiP", e);
+ }
+ }
+
+ @Override
+ public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) {
+ Objects.requireNonNull(info, "Requires RunningTaskInfo");
+ mTaskInfo = info;
+ mToken = mTaskInfo.token;
+ mState = State.TASK_APPEARED;
+ mLeash = leash;
+ mInitialState.put(mToken.asBinder(), new Configuration(mTaskInfo.configuration));
+ mPictureInPictureParams = mTaskInfo.pictureInPictureParams;
+ mPipBoundsState.setLastPipComponentName(mTaskInfo.topActivity);
+
+ mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo);
+ mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER);
+
+ // If the displayId of the task is different than what PipBoundsHandler has, then update
+ // it. This is possible if we entered PiP on an external display.
+ if (info.displayId != mPipBoundsHandler.getDisplayInfo().displayId
+ && mOnDisplayIdChangeCallback != null) {
+ mOnDisplayIdChangeCallback.accept(info.displayId);
+ }
+
+ if (mShouldIgnoreEnteringPipTransition) {
+ // animation is finished in the Launcher and here we directly apply the final touch.
+ applyEnterPipSyncTransaction(mPipBoundsState.getBounds(),
+ () -> sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP));
+ mShouldIgnoreEnteringPipTransition = false;
+ return;
+ }
+
+ if (mShouldDeferEnteringPip) {
+ if (DEBUG) Log.d(TAG, "Defer entering PiP animation, fixed rotation is ongoing");
+ // if deferred, hide the surface till fixed rotation is completed
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ tx.setAlpha(mLeash, 0f);
+ tx.show(mLeash);
+ tx.apply();
+ return;
+ }
+
+ mPipBoundsState.setAspectRatio(getAspectRatioOrDefault(mPictureInPictureParams));
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(null /* bounds */,
+ getMinimalSize(mTaskInfo.topActivityInfo));
+ Objects.requireNonNull(destinationBounds, "Missing destination bounds");
+ final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
+
+ if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
+ final Rect sourceHintRect = getValidSourceHintRect(info.pictureInPictureParams,
+ currentBounds);
+ scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect,
+ TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration,
+ null /* updateBoundsCallback */);
+ mState = State.ENTERING_PIP;
+ } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ enterPipWithAlphaAnimation(destinationBounds, mEnterExitAnimationDuration);
+ mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+ } else {
+ throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType);
+ }
+ }
+
+ /**
+ * Returns the source hint rect if it is valid (if provided and is contained by the current
+ * task bounds).
+ */
+ private Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds) {
+ final Rect sourceHintRect = params != null
+ && params.hasSourceBoundsHint()
+ ? params.getSourceRectHint()
+ : null;
+ if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) {
+ return sourceHintRect;
+ }
+ return null;
+ }
+
+ private void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) {
+ // If we are fading the PIP in, then we should move the pip to the final location as
+ // soon as possible, but set the alpha immediately since the transaction can take a
+ // while to process
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ tx.setAlpha(mLeash, 0f);
+ tx.apply();
+ applyEnterPipSyncTransaction(destinationBounds, () -> {
+ mUpdateHandler.post(() -> mPipAnimationController
+ .getAnimator(mLeash, destinationBounds, 0f, 1f)
+ .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
+ .setPipAnimationCallback(mPipAnimationCallback)
+ .setDuration(durationMs)
+ .start());
+ // mState is set right after the animation is kicked off to block any resize
+ // requests such as offsetPip that may have been called prior to the transition.
+ mState = State.ENTERING_PIP;
+ });
+ }
+
+ private void applyEnterPipSyncTransaction(Rect destinationBounds, Runnable runnable) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+ wct.setBounds(mToken, destinationBounds);
+ wct.scheduleFinishEnterPip(mToken, destinationBounds);
+ mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() {
+ @Override
+ public void onTransactionReady(int id, SurfaceControl.Transaction t) {
+ t.apply();
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+ });
+ }
+
+ private void sendOnPipTransitionStarted(
+ @PipAnimationController.TransitionDirection int direction) {
+ sendOnPipTransitionStarted(mTaskInfo.baseActivity, direction);
+ }
+
+ private void sendOnPipTransitionStarted(ComponentName componentName,
+ @PipAnimationController.TransitionDirection int direction) {
+ if (direction == TRANSITION_DIRECTION_TO_PIP) {
+ mState = State.ENTERING_PIP;
+ }
+ final Rect pipBounds = mPipBoundsState.getBounds();
+ runOnMainHandler(() -> {
+ for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
+ final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
+ callback.onPipTransitionStarted(componentName, direction, pipBounds);
+ }
+ });
+ }
+
+ private void sendOnPipTransitionFinished(
+ @PipAnimationController.TransitionDirection int direction) {
+ if (direction == TRANSITION_DIRECTION_TO_PIP) {
+ mState = State.ENTERED_PIP;
+ }
+ runOnMainHandler(() -> {
+ for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
+ final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
+ callback.onPipTransitionFinished(mTaskInfo.baseActivity, direction);
+ }
+ });
+ }
+
+ private void sendOnPipTransitionCancelled(
+ @PipAnimationController.TransitionDirection int direction) {
+ runOnMainHandler(() -> {
+ for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
+ final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
+ callback.onPipTransitionCanceled(mTaskInfo.baseActivity, direction);
+ }
+ });
+ }
+
+ private void runOnMainHandler(Runnable r) {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ r.run();
+ } else {
+ mMainHandler.post(r);
+ }
+ }
+
+ /**
+ * Setup the ViewHost and attach the provided menu view to the ViewHost.
+ * @return The input token belonging to the PipMenuView.
+ */
+ public IBinder attachPipMenuViewHost(View menuView, WindowManager.LayoutParams lp) {
+ if (mPipMenuSurface != null) {
+ Log.e(TAG, "PIP Menu View already created and attached.");
+ return null;
+ }
+
+ if (mLeash == null) {
+ Log.e(TAG, "PiP Leash is not yet ready.");
+ return null;
+ }
+
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new RuntimeException("PipMenuView needs to be attached on the main thread.");
+ }
+ final Context context = menuView.getContext();
+ mPipViewHost = new SurfaceControlViewHost(context, context.getDisplay(),
+ (android.os.Binder) null);
+ mPipMenuSurface = mPipViewHost.getSurfacePackage().getSurfaceControl();
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ transaction.reparent(mPipMenuSurface, mLeash);
+ transaction.show(mPipMenuSurface);
+ transaction.setRelativeLayer(mPipMenuSurface, mLeash, 1);
+ transaction.apply();
+ mPipViewHost.setView(menuView, lp);
+
+ return mPipViewHost.getSurfacePackage().getInputToken();
+ }
+
+
+ /**
+ * Releases the PIP Menu's View host, remove it from PIP task surface.
+ */
+ public void detachPipMenuViewHost() {
+ if (mPipMenuSurface != null) {
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ transaction.remove(mPipMenuSurface);
+ transaction.apply();
+ mPipMenuSurface = null;
+ mPipViewHost = null;
+ }
+ }
+
+ /**
+ * Return whether the PiP Menu has been attached to the leash yet.
+ */
+ public boolean isPipMenuViewHostAttached() {
+ return mPipViewHost != null;
+ }
+
+
+ /**
+ * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}.
+ * Meanwhile this callback is invoked whenever the task is removed. For instance:
+ * - as a result of removeRootTasksInWindowingModes from WM
+ * - activity itself is died
+ * Nevertheless, we simply update the internal state here as all the heavy lifting should
+ * have been done in WM.
+ */
+ @Override
+ public void onTaskVanished(ActivityManager.RunningTaskInfo info) {
+ if (!mState.isInPip()) {
+ return;
+ }
+ final WindowContainerToken token = info.token;
+ Objects.requireNonNull(token, "Requires valid WindowContainerToken");
+ if (token.asBinder() != mToken.asBinder()) {
+ Log.wtf(TAG, "Unrecognized token: " + token);
+ return;
+ }
+ mShouldDeferEnteringPip = false;
+ mShouldIgnoreEnteringPipTransition = false;
+ mPictureInPictureParams = null;
+ mState = State.UNDEFINED;
+ mPipUiEventLoggerLogger.setTaskInfo(null);
+ }
+
+ @Override
+ public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
+ Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken");
+ mPipBoundsState.setLastPipComponentName(info.topActivity);
+ final PictureInPictureParams newParams = info.pictureInPictureParams;
+ if (newParams == null || !applyPictureInPictureParams(newParams)) {
+ Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams);
+ return;
+ }
+ // Aspect ratio changed, re-calculate destination bounds.
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ mPipBoundsState.getBounds(), getMinimalSize(info.topActivityInfo),
+ true /* userCurrentMinEdgeSize */);
+ Objects.requireNonNull(destinationBounds, "Missing destination bounds");
+ scheduleAnimateResizePip(destinationBounds, mEnterExitAnimationDuration,
+ null /* updateBoundsCallback */);
+ }
+
+ @Override
+ public void onFixedRotationStarted(int displayId, int newRotation) {
+ mShouldDeferEnteringPip = true;
+ }
+
+ @Override
+ public void onFixedRotationFinished(int displayId) {
+ if (mShouldDeferEnteringPip && mState.isInPip()) {
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo));
+ // schedule a regular animation to ensure all the callbacks are still being sent
+ enterPipWithAlphaAnimation(destinationBounds, 0 /* durationMs */);
+ }
+ mShouldDeferEnteringPip = false;
+ }
+
+ /**
+ * Called when display size or font size of settings changed
+ */
+ public void onDensityOrFontScaleChanged(Context context) {
+ mSurfaceTransactionHelper.onDensityOrFontScaleChanged(context);
+ }
+
+ /**
+ * TODO(b/152809058): consolidate the display info handling logic in SysUI
+ *
+ * @param destinationBoundsOut the current destination bounds will be populated to this param
+ */
+ @SuppressWarnings("unchecked")
+ public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation,
+ boolean fromImeAdjustment, boolean fromShelfAdjustment,
+ WindowContainerTransaction wct) {
+ final PipAnimationController.PipTransitionAnimator animator =
+ mPipAnimationController.getCurrentAnimator();
+ if (animator == null || !animator.isRunning()
+ || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) {
+ if (mState.isInPip() && fromRotation) {
+ // If we are rotating while there is a current animation, immediately cancel the
+ // animation (remove the listeners so we don't trigger the normal finish resize
+ // call that should only happen on the update thread)
+ int direction = TRANSITION_DIRECTION_NONE;
+ if (animator != null) {
+ direction = animator.getTransitionDirection();
+ animator.removeAllUpdateListeners();
+ animator.removeAllListeners();
+ animator.cancel();
+ // Do notify the listeners that this was canceled
+ sendOnPipTransitionCancelled(direction);
+ sendOnPipTransitionFinished(direction);
+ }
+ mPipBoundsState.setBounds(destinationBoundsOut);
+
+ // Create a reset surface transaction for the new bounds and update the window
+ // container transaction
+ final SurfaceControl.Transaction tx = createFinishResizeSurfaceTransaction(
+ destinationBoundsOut);
+ prepareFinishResizeTransaction(destinationBoundsOut, direction, tx, wct);
+ } else {
+ // There could be an animation on-going. If there is one on-going, last-reported
+ // bounds isn't yet updated. We'll use the animator's bounds instead.
+ if (animator != null && animator.isRunning()) {
+ if (!animator.getDestinationBounds().isEmpty()) {
+ destinationBoundsOut.set(animator.getDestinationBounds());
+ }
+ } else {
+ if (!mPipBoundsState.getBounds().isEmpty()) {
+ destinationBoundsOut.set(mPipBoundsState.getBounds());
+ }
+ }
+ }
+ return;
+ }
+
+ final Rect currentDestinationBounds = animator.getDestinationBounds();
+ destinationBoundsOut.set(currentDestinationBounds);
+ if (!fromImeAdjustment && !fromShelfAdjustment
+ && mPipBoundsHandler.getDisplayBounds().contains(currentDestinationBounds)) {
+ // no need to update the destination bounds, bail early
+ return;
+ }
+
+ final Rect newDestinationBounds = mPipBoundsHandler.getDestinationBounds(null /* bounds */,
+ getMinimalSize(mTaskInfo.topActivityInfo));
+ if (newDestinationBounds.equals(currentDestinationBounds)) return;
+ if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) {
+ animator.updateEndValue(newDestinationBounds);
+ }
+ animator.setDestinationBounds(newDestinationBounds);
+ destinationBoundsOut.set(newDestinationBounds);
+ }
+
+ /**
+ * @return {@code true} if the aspect ratio is changed since no other parameters within
+ * {@link PictureInPictureParams} would affect the bounds.
+ */
+ private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) {
+ final Rational currentAspectRatio =
+ mPictureInPictureParams != null ? mPictureInPictureParams.getAspectRatioRational()
+ : null;
+ final boolean aspectRatioChanged = !Objects.equals(currentAspectRatio,
+ params.getAspectRatioRational());
+ mPictureInPictureParams = params;
+ if (aspectRatioChanged) {
+ mPipBoundsState.setAspectRatio(params.getAspectRatio());
+ }
+ return aspectRatioChanged;
+ }
+
+ /**
+ * Animates resizing of the pinned stack given the duration.
+ */
+ public void scheduleAnimateResizePip(Rect toBounds, int duration,
+ Consumer<Rect> updateBoundsCallback) {
+ if (mShouldDeferEnteringPip) {
+ Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred");
+ return;
+ }
+ scheduleAnimateResizePip(mPipBoundsState.getBounds(), toBounds, null /* sourceHintRect */,
+ TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback);
+ }
+
+ private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds,
+ Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction,
+ int durationMs, Consumer<Rect> updateBoundsCallback) {
+ if (!mState.isInPip()) {
+ // TODO: tend to use shouldBlockResizeRequest here as well but need to consider
+ // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window
+ // container transaction callback and we want to set the mState immediately.
+ return;
+ }
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = updateBoundsCallback;
+ args.arg2 = currentBounds;
+ args.arg3 = destinationBounds;
+ args.arg4 = sourceHintRect;
+ args.argi1 = direction;
+ args.argi2 = durationMs;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args));
+ }
+
+ /**
+ * Directly perform manipulation/resize on the leash. This will not perform any
+ * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called.
+ */
+ public void scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = updateBoundsCallback;
+ args.arg2 = toBounds;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_IMMEDIATE, args));
+ }
+
+ /**
+ * Directly perform a scaled matrix transformation on the leash. This will not perform any
+ * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called.
+ */
+ public void scheduleUserResizePip(Rect startBounds, Rect toBounds,
+ Consumer<Rect> updateBoundsCallback) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = updateBoundsCallback;
+ args.arg2 = startBounds;
+ args.arg3 = toBounds;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_USER, args));
+ }
+
+ /**
+ * Finish an intermediate resize operation. This is expected to be called after
+ * {@link #scheduleResizePip}.
+ */
+ public void scheduleFinishResizePip(Rect destinationBounds) {
+ scheduleFinishResizePip(destinationBounds, null /* updateBoundsCallback */);
+ }
+
+ /**
+ * Same as {@link #scheduleFinishResizePip} but with a callback.
+ */
+ public void scheduleFinishResizePip(Rect destinationBounds,
+ Consumer<Rect> updateBoundsCallback) {
+ scheduleFinishResizePip(destinationBounds, TRANSITION_DIRECTION_NONE, updateBoundsCallback);
+ }
+
+ private void scheduleFinishResizePip(Rect destinationBounds,
+ @PipAnimationController.TransitionDirection int direction,
+ Consumer<Rect> updateBoundsCallback) {
+ if (mState.shouldBlockResizeRequest()) {
+ return;
+ }
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = updateBoundsCallback;
+ args.arg2 = createFinishResizeSurfaceTransaction(
+ destinationBounds);
+ args.arg3 = destinationBounds;
+ args.argi1 = direction;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_FINISH_RESIZE, args));
+ }
+
+ private SurfaceControl.Transaction createFinishResizeSurfaceTransaction(
+ Rect destinationBounds) {
+ final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+ mSurfaceTransactionHelper
+ .crop(tx, mLeash, destinationBounds)
+ .resetScale(tx, mLeash, destinationBounds)
+ .round(tx, mLeash, mState.isInPip());
+ return tx;
+ }
+
+ /**
+ * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation.
+ */
+ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration,
+ Consumer<Rect> updateBoundsCallback) {
+ if (mState.shouldBlockResizeRequest()) {
+ return;
+ }
+ if (mShouldDeferEnteringPip) {
+ Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred");
+ return;
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = updateBoundsCallback;
+ args.arg2 = originalBounds;
+ // offset would be zero if triggered from screen rotation.
+ args.argi1 = offset;
+ args.argi2 = duration;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args));
+ }
+
+ private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleOffsetPip() instead of this "
+ + "directly");
+ }
+ if (mTaskInfo == null) {
+ Log.w(TAG, "mTaskInfo is not set");
+ return;
+ }
+ final Rect destinationBounds = new Rect(originalBounds);
+ destinationBounds.offset(xOffset, yOffset);
+ animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */,
+ TRANSITION_DIRECTION_SAME, durationMs);
+ }
+
+ private void resizePip(Rect destinationBounds) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleResizePip() instead of this "
+ + "directly");
+ }
+ // Could happen when exitPip
+ if (mToken == null || mLeash == null) {
+ Log.w(TAG, "Abort animation, invalid leash");
+ return;
+ }
+ mPipBoundsState.setBounds(destinationBounds);
+ final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+ mSurfaceTransactionHelper
+ .crop(tx, mLeash, destinationBounds)
+ .round(tx, mLeash, mState.isInPip());
+ tx.apply();
+ }
+
+ private void userResizePip(Rect startBounds, Rect destinationBounds) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleUserResizePip() instead of "
+ + "this directly");
+ }
+ // Could happen when exitPip
+ if (mToken == null || mLeash == null) {
+ Log.w(TAG, "Abort animation, invalid leash");
+ return;
+ }
+
+ if (startBounds.isEmpty() || destinationBounds.isEmpty()) {
+ Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting.");
+ return;
+ }
+
+ final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+ mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds);
+ tx.apply();
+ }
+
+ private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds,
+ @PipAnimationController.TransitionDirection int direction,
+ @PipAnimationController.AnimationType int type) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleResizePip() instead of this "
+ + "directly");
+ }
+ mPipBoundsState.setBounds(destinationBounds);
+ if (direction == TRANSITION_DIRECTION_REMOVE_STACK) {
+ removePipImmediately();
+ return;
+ } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) {
+ return;
+ }
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ prepareFinishResizeTransaction(destinationBounds, direction, tx, wct);
+ applyFinishBoundsResize(wct, direction);
+ runOnMainHandler(() -> {
+ if (mPipViewHost != null) {
+ mPipViewHost.relayout(PipMenuActivityController.getPipMenuLayoutParams(
+ destinationBounds.width(), destinationBounds.height()));
+ }
+ });
+ }
+
+ private void prepareFinishResizeTransaction(Rect destinationBounds,
+ @PipAnimationController.TransitionDirection int direction,
+ SurfaceControl.Transaction tx,
+ WindowContainerTransaction wct) {
+ final Rect taskBounds;
+ if (isInPipDirection(direction)) {
+ // If we are animating from fullscreen using a bounds animation, then reset the
+ // activity windowing mode set by WM, and set the task bounds to the final bounds
+ taskBounds = destinationBounds;
+ wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+ wct.scheduleFinishEnterPip(mToken, destinationBounds);
+ } else if (isOutPipDirection(direction)) {
+ // If we are animating to fullscreen, then we need to reset the override bounds
+ // on the task to ensure that the task "matches" the parent's bounds.
+ taskBounds = (direction == TRANSITION_DIRECTION_LEAVE_PIP)
+ ? null : destinationBounds;
+ applyWindowingModeChangeOnExit(wct, direction);
+ } else {
+ // Just a resize in PIP
+ taskBounds = destinationBounds;
+ }
+
+ wct.setBounds(mToken, taskBounds);
+ wct.setBoundsChangeTransaction(mToken, tx);
+ }
+
+ /**
+ * Applies the window container transaction to finish a bounds resize.
+ *
+ * Called by {@link #finishResize(SurfaceControl.Transaction, Rect, int, int)}} once it has
+ * finished preparing the transaction. It allows subclasses to modify the transaction before
+ * applying it.
+ */
+ public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct,
+ @PipAnimationController.TransitionDirection int direction) {
+ mTaskOrganizer.applyTransaction(wct);
+ }
+
+ /**
+ * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined
+ * and can be overridden to restore to an alternate windowing mode.
+ */
+ public int getOutPipWindowingMode() {
+ // By default, simply reset the windowing mode to undefined.
+ return WINDOWING_MODE_UNDEFINED;
+ }
+
+ private void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect,
+ @PipAnimationController.TransitionDirection int direction, int durationMs) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of "
+ + "this directly");
+ }
+ // Could happen when exitPip
+ if (mToken == null || mLeash == null) {
+ Log.w(TAG, "Abort animation, invalid leash");
+ return;
+ }
+ mPipAnimationController
+ .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect, direction)
+ .setTransitionDirection(direction)
+ .setPipAnimationCallback(mPipAnimationCallback)
+ .setDuration(durationMs)
+ .start();
+ }
+
+ private Size getMinimalSize(ActivityInfo activityInfo) {
+ if (activityInfo == null || activityInfo.windowLayout == null) {
+ return null;
+ }
+ final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
+ // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout>
+ // without minWidth/minHeight
+ if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) {
+ return new Size(windowLayout.minWidth, windowLayout.minHeight);
+ }
+ return null;
+ }
+
+ private float getAspectRatioOrDefault(@Nullable PictureInPictureParams params) {
+ return params == null || !params.hasSetAspectRatio()
+ ? mPipBoundsHandler.getDefaultAspectRatio()
+ : params.getAspectRatio();
+ }
+
+ /**
+ * Sync with {@link SplitScreen} on destination bounds if PiP is going to split screen.
+ *
+ * @param destinationBoundsOut contain the updated destination bounds if applicable
+ * @return {@code true} if destinationBounds is altered for split screen
+ */
+ private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut) {
+ if (!mSplitScreenOptional.isPresent()) {
+ return false;
+ }
+
+ SplitScreen splitScreen = mSplitScreenOptional.get();
+ if (!splitScreen.isDividerVisible()) {
+ // fail early if system is not in split screen mode
+ return false;
+ }
+
+ // PiP window will go to split-secondary mode instead of fullscreen, populates the
+ // split screen bounds here.
+ destinationBoundsOut.set(splitScreen.getDividerView()
+ .getNonMinimizedSplitScreenSecondaryBounds());
+ return true;
+ }
+
+ /**
+ * Dumps internal states.
+ */
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mTaskInfo=" + mTaskInfo);
+ pw.println(innerPrefix + "mToken=" + mToken
+ + " binder=" + (mToken != null ? mToken.asBinder() : null));
+ pw.println(innerPrefix + "mLeash=" + mLeash);
+ pw.println(innerPrefix + "mState=" + mState);
+ pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType);
+ pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
+ pw.println(innerPrefix + "mInitialState:");
+ for (Map.Entry<IBinder, Configuration> e : mInitialState.entrySet()) {
+ pw.println(innerPrefix + " binder=" + e.getKey()
+ + " winConfig=" + e.getValue().windowConfiguration);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_PIP);
+ }
+
+ /**
+ * Callback interface for PiP transitions (both from and to PiP mode)
+ */
+ public interface PipTransitionCallback {
+ /**
+ * Callback when the pip transition is started.
+ */
+ void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds);
+
+ /**
+ * Callback when the pip transition is finished.
+ */
+ void onPipTransitionFinished(ComponentName activity, int direction);
+
+ /**
+ * Callback when the pip transition is cancelled.
+ */
+ void onPipTransitionCanceled(ComponentName activity, int direction);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java
new file mode 100644
index 000000000000..de3bb2950c0a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java
@@ -0,0 +1,114 @@
+/*
+ * 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 android.app.TaskInfo;
+import android.content.pm.PackageManager;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+/**
+ * Helper class that ends PiP log to UiEvent, see also go/uievent
+ */
+public class PipUiEventLogger {
+
+ private static final int INVALID_PACKAGE_UID = -1;
+
+ private final UiEventLogger mUiEventLogger;
+ private final PackageManager mPackageManager;
+
+ private String mPackageName;
+ private int mPackageUid = INVALID_PACKAGE_UID;
+
+ public PipUiEventLogger(UiEventLogger uiEventLogger, PackageManager packageManager) {
+ mUiEventLogger = uiEventLogger;
+ mPackageManager = packageManager;
+ }
+
+ public void setTaskInfo(TaskInfo taskInfo) {
+ if (taskInfo == null) {
+ mPackageName = null;
+ mPackageUid = INVALID_PACKAGE_UID;
+ } else {
+ mPackageName = taskInfo.topActivity.getPackageName();
+ mPackageUid = getUid(mPackageName, taskInfo.userId);
+ }
+ }
+
+ /**
+ * Sends log via UiEvent, reference go/uievent for how to debug locally
+ */
+ public void log(PipUiEventEnum event) {
+ if (mPackageName == null || mPackageUid == INVALID_PACKAGE_UID) {
+ return;
+ }
+ mUiEventLogger.log(event, mPackageUid, mPackageName);
+ }
+
+ private int getUid(String packageName, int userId) {
+ int uid = INVALID_PACKAGE_UID;
+ try {
+ uid = mPackageManager.getApplicationInfoAsUser(
+ packageName, 0 /* ApplicationInfoFlags */, userId).uid;
+ } catch (PackageManager.NameNotFoundException e) {
+ // do nothing.
+ }
+ return uid;
+ }
+
+ /**
+ * Enums for logging the PiP events to UiEvent
+ */
+ public enum PipUiEventEnum implements UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "Activity enters picture-in-picture mode")
+ PICTURE_IN_PICTURE_ENTER(603),
+
+ @UiEvent(doc = "Expands from picture-in-picture to fullscreen")
+ PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN(604),
+
+ @UiEvent(doc = "Removes picture-in-picture by tap close button")
+ PICTURE_IN_PICTURE_TAP_TO_REMOVE(605),
+
+ @UiEvent(doc = "Removes picture-in-picture by drag to dismiss area")
+ PICTURE_IN_PICTURE_DRAG_TO_REMOVE(606),
+
+ @UiEvent(doc = "Shows picture-in-picture menu")
+ PICTURE_IN_PICTURE_SHOW_MENU(607),
+
+ @UiEvent(doc = "Hides picture-in-picture menu")
+ PICTURE_IN_PICTURE_HIDE_MENU(608),
+
+ @UiEvent(doc = "Changes the aspect ratio of picture-in-picture window. This is inherited"
+ + " from previous Tron-based logging and currently not in use.")
+ PICTURE_IN_PICTURE_CHANGE_ASPECT_RATIO(609),
+
+ @UiEvent(doc = "User resize of the picture-in-picture window")
+ PICTURE_IN_PICTURE_RESIZE(610);
+
+ private final int mId;
+
+ PipUiEventEnum(int id) {
+ mId = id;
+ }
+
+ @Override
+ public int getId() {
+ return mId;
+ }
+ }
+}
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
new file mode 100644
index 000000000000..18b6922f3067
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
@@ -0,0 +1,274 @@
+/*
+ * 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.annotation.NonNull;
+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.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.accessibility.IAccessibilityInteractionConnection;
+import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipSnapAlgorithm;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+
+import java.util.ArrayList;
+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 interface AccessibilityCallbacks {
+ void onAccessibilityShowMenu();
+ }
+
+ private static final long ACCESSIBILITY_NODE_ID = 1;
+ private List<AccessibilityNodeInfo> mAccessibilityNodeInfoList;
+
+ private Context mContext;
+ private Handler mHandler;
+ private final @NonNull PipBoundsState mPipBoundsState;
+ private PipMotionHelper mMotionHelper;
+ private PipTaskOrganizer mTaskOrganizer;
+ private PipSnapAlgorithm mSnapAlgorithm;
+ private Runnable mUpdateMovementBoundCallback;
+ private AccessibilityCallbacks mCallbacks;
+
+ private final Rect mNormalBounds = new Rect();
+ private final Rect mExpandedBounds = new Rect();
+ private final Rect mNormalMovementBounds = new Rect();
+ private final Rect mExpandedMovementBounds = new Rect();
+ private Rect mTmpBounds = new Rect();
+
+ public PipAccessibilityInteractionConnection(Context context,
+ @NonNull PipBoundsState pipBoundsState, PipMotionHelper motionHelper,
+ PipTaskOrganizer taskOrganizer, PipSnapAlgorithm snapAlgorithm,
+ AccessibilityCallbacks callbacks, Runnable updateMovementBoundCallback,
+ Handler handler) {
+ mContext = context;
+ mHandler = handler;
+ mPipBoundsState = pipBoundsState;
+ mMotionHelper = motionHelper;
+ mTaskOrganizer = taskOrganizer;
+ mSnapAlgorithm = snapAlgorithm;
+ mUpdateMovementBoundCallback = updateMovementBoundCallback;
+ mCallbacks = callbacks;
+ }
+
+ @Override
+ public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,
+ Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) {
+ try {
+ callback.setFindAccessibilityNodeInfosResult(
+ (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID)
+ ? getNodeList() : null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+
+ @Override
+ public void performAccessibilityAction(long accessibilityNodeId, int action,
+ Bundle arguments, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid) {
+ // We only support one view. A request for anything else is invalid
+ boolean result = false;
+ if (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) {
+
+ // R constants are not final so this cannot be put in the switch-case.
+ if (action == R.id.action_pip_resize) {
+ if (mMotionHelper.getBounds().width() == mNormalBounds.width()
+ && mMotionHelper.getBounds().height() == mNormalBounds.height()) {
+ setToExpandedBounds();
+ } else {
+ setToNormalBounds();
+ }
+ result = true;
+ } else {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK:
+ mHandler.post(() -> {
+ mCallbacks.onAccessibilityShowMenu();
+ });
+ result = true;
+ break;
+ case AccessibilityNodeInfo.ACTION_DISMISS:
+ mMotionHelper.dismissPip();
+ result = true;
+ break;
+ case com.android.internal.R.id.accessibilityActionMoveWindow:
+ int newX = arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_X);
+ int newY = arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_Y);
+ Rect pipBounds = new Rect();
+ pipBounds.set(mMotionHelper.getBounds());
+ mTmpBounds.offsetTo(newX, newY);
+ mMotionHelper.movePip(mTmpBounds);
+ result = true;
+ break;
+ case AccessibilityNodeInfo.ACTION_EXPAND:
+ mMotionHelper.expandLeavePip();
+ result = true;
+ break;
+ default:
+ // Leave result as false
+ }
+ }
+ }
+ try {
+ callback.setPerformAccessibilityActionResult(result, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+
+ private void setToExpandedBounds() {
+ float savedSnapFraction = mSnapAlgorithm.getSnapFraction(
+ mPipBoundsState.getBounds(), mNormalMovementBounds);
+ mSnapAlgorithm.applySnapFraction(mExpandedBounds, mExpandedMovementBounds,
+ savedSnapFraction);
+ mTaskOrganizer.scheduleFinishResizePip(mExpandedBounds, (Rect bounds) -> {
+ mMotionHelper.synchronizePinnedStackBounds();
+ mUpdateMovementBoundCallback.run();
+ });
+ }
+
+ private void setToNormalBounds() {
+ float savedSnapFraction = mSnapAlgorithm.getSnapFraction(
+ mPipBoundsState.getBounds(), mExpandedMovementBounds);
+ mSnapAlgorithm.applySnapFraction(mNormalBounds, mNormalMovementBounds, savedSnapFraction);
+ mTaskOrganizer.scheduleFinishResizePip(mNormalBounds, (Rect bounds) -> {
+ mMotionHelper.synchronizePinnedStackBounds();
+ mUpdateMovementBoundCallback.run();
+ });
+ }
+
+ @Override
+ public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId,
+ String viewId, Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
+ // We have no view with a proper ID
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+
+ @Override
+ public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text,
+ Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
+ // We have no view with text
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+
+ @Override
+ public 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
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+
+ @Override
+ public 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
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+
+ @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.
+ */
+ void onMovementBoundsChanged(Rect normalBounds, Rect expandedBounds, Rect normalMovementBounds,
+ Rect expandedMovementBounds) {
+ mNormalBounds.set(normalBounds);
+ mExpandedBounds.set(expandedBounds);
+ mNormalMovementBounds.set(normalMovementBounds);
+ mExpandedMovementBounds.set(expandedMovementBounds);
+ }
+
+ /**
+ * Update the Root node with PIP Accessibility action items.
+ */
+ public static AccessibilityNodeInfo obtainRootAccessibilityNodeInfo(Context context) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID,
+ AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_MOVE_WINDOW);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_resize,
+ context.getString(R.string.accessibility_action_pip_resize)));
+ info.setImportantForAccessibility(true);
+ info.setClickable(true);
+ info.setVisibleToUser(true);
+ return info;
+ }
+
+ private List<AccessibilityNodeInfo> getNodeList() {
+ if (mAccessibilityNodeInfoList == null) {
+ mAccessibilityNodeInfoList = new ArrayList<>(1);
+ }
+ AccessibilityNodeInfo info = obtainRootAccessibilityNodeInfo(mContext);
+ mAccessibilityNodeInfoList.clear();
+ mAccessibilityNodeInfoList.add(info);
+ return mAccessibilityNodeInfoList;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java
new file mode 100644
index 000000000000..6b6b5211b10a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java
@@ -0,0 +1,97 @@
+/*
+ * 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 static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE;
+
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.OnOpChangedListener;
+import android.app.IActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Handler;
+import android.util.Pair;
+
+public class PipAppOpsListener {
+ private static final String TAG = PipAppOpsListener.class.getSimpleName();
+
+ private Context mContext;
+ private Handler mHandler;
+ private IActivityManager mActivityManager;
+ private AppOpsManager mAppOpsManager;
+ private Callback mCallback;
+
+ private AppOpsManager.OnOpChangedListener mAppOpsChangedListener = new OnOpChangedListener() {
+ @Override
+ public void onOpChanged(String op, String packageName) {
+ try {
+ // Dismiss the PiP once the user disables the app ops setting for that package
+ final Pair<ComponentName, Integer> topPipActivityInfo =
+ PipUtils.getTopPipActivity(mContext, mActivityManager);
+ if (topPipActivityInfo.first != null) {
+ final ApplicationInfo appInfo = mContext.getPackageManager()
+ .getApplicationInfoAsUser(packageName, 0, topPipActivityInfo.second);
+ if (appInfo.packageName.equals(topPipActivityInfo.first.getPackageName()) &&
+ mAppOpsManager.checkOpNoThrow(OP_PICTURE_IN_PICTURE, appInfo.uid,
+ packageName) != MODE_ALLOWED) {
+ mHandler.post(() -> mCallback.dismissPip());
+ }
+ }
+ } catch (NameNotFoundException e) {
+ // Unregister the listener if the package can't be found
+ unregisterAppOpsListener();
+ }
+ }
+ };
+
+ public PipAppOpsListener(Context context, IActivityManager activityManager,
+ Callback callback) {
+ mContext = context;
+ mHandler = new Handler(mContext.getMainLooper());
+ mActivityManager = activityManager;
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ mCallback = callback;
+ }
+
+ public void onActivityPinned(String packageName) {
+ // Register for changes to the app ops setting for this package while it is in PiP
+ registerAppOpsListener(packageName);
+ }
+
+ public void onActivityUnpinned() {
+ // Unregister for changes to the previously PiP'ed package
+ unregisterAppOpsListener();
+ }
+
+ private void registerAppOpsListener(String packageName) {
+ mAppOpsManager.startWatchingMode(OP_PICTURE_IN_PICTURE, packageName,
+ mAppOpsChangedListener);
+ }
+
+ private void unregisterAppOpsListener() {
+ mAppOpsManager.stopWatchingMode(mAppOpsChangedListener);
+ }
+
+ /** Callback for PipAppOpsListener to request changes to the PIP window. */
+ public interface Callback {
+ /** Dismisses the PIP window. */
+ void dismissPip();
+ }
+}
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
new file mode 100644
index 000000000000..edc68e5221f1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -0,0 +1,486 @@
+/*
+ * 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 static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
+
+import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
+
+import android.app.ActivityManager;
+import android.app.PictureInPictureParams;
+import android.app.RemoteAction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+import android.view.DisplayInfo;
+import android.view.IPinnedStackController;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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.pip.PinnedStackListenerForwarder;
+import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.pip.PipBoundsHandler;
+import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Manages the picture-in-picture (PIP) UI and states for Phones.
+ */
+public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback {
+ private static final String TAG = "PipController";
+
+ private Context mContext;
+ private Handler mHandler = new Handler();
+
+ private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
+ private final Rect mTmpInsetBounds = new Rect();
+ private final Rect mTmpNormalBounds = new Rect();
+ protected final Rect mReentryBounds = new Rect();
+
+ private DisplayController mDisplayController;
+ private PipAppOpsListener mAppOpsListener;
+ private PipBoundsHandler mPipBoundsHandler;
+ private @NonNull PipBoundsState mPipBoundsState;
+ private PipMediaController mMediaController;
+ private PipTouchHandler mTouchHandler;
+ private Consumer<Boolean> mPinnedStackAnimationRecentsCallback;
+ private WindowManagerShellWrapper mWindowManagerShellWrapper;
+
+ private boolean mIsInFixedRotation;
+
+ protected PipMenuActivityController mMenuController;
+ protected PipTaskOrganizer mPipTaskOrganizer;
+
+ /**
+ * Handler for display rotation changes.
+ */
+ private final DisplayChangeController.OnDisplayChangingListener mRotationController = (
+ int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> {
+ if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isDeferringEnterPipAnimation()) {
+ // Skip if we aren't in PIP or haven't actually entered PIP yet. We still need to update
+ // the display layout in the bounds handler in this case.
+ mPipBoundsHandler.onDisplayRotationChangedNotInPip(mContext, toRotation);
+ return;
+ }
+ // If there is an animation running (ie. from a shelf offset), then ensure that we calculate
+ // the bounds for the next orientation using the destination bounds of the animation
+ // TODO: Technically this should account for movement animation bounds as well
+ Rect currentBounds = mPipTaskOrganizer.getCurrentOrAnimatingBounds();
+ final boolean changed = mPipBoundsHandler.onDisplayRotationChanged(mContext,
+ mTmpNormalBounds, currentBounds, mTmpInsetBounds, displayId, fromRotation,
+ toRotation, t);
+ if (changed) {
+ // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the
+ // movement bounds
+ mTouchHandler.adjustBoundsForRotation(mTmpNormalBounds,
+ mPipBoundsState.getBounds(), mTmpInsetBounds);
+
+ // The bounds are being applied to a specific snap fraction, so reset any known offsets
+ // for the previous orientation before updating the movement bounds.
+ // We perform the resets if and only if this callback is due to screen rotation but
+ // not during the fixed rotation. In fixed rotation case, app is about to enter PiP
+ // and we need the offsets preserved to calculate the destination bounds.
+ if (!mIsInFixedRotation) {
+ mPipBoundsHandler.setShelfHeight(false, 0);
+ mPipBoundsHandler.onImeVisibilityChanged(false, 0);
+ mTouchHandler.onShelfVisibilityChanged(false, 0);
+ mTouchHandler.onImeVisibilityChanged(false, 0);
+ }
+
+ updateMovementBounds(mTmpNormalBounds, true /* fromRotation */,
+ false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t);
+ }
+ };
+
+ private DisplayController.OnDisplaysChangedListener mFixedRotationListener =
+ new DisplayController.OnDisplaysChangedListener() {
+ @Override
+ public void onFixedRotationStarted(int displayId, int newRotation) {
+ mIsInFixedRotation = true;
+ }
+
+ @Override
+ public void onFixedRotationFinished(int displayId) {
+ mIsInFixedRotation = false;
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ mPipBoundsHandler.setDisplayLayout(
+ mDisplayController.getDisplayLayout(displayId));
+ }
+ };
+
+ /**
+ * Handler for messages from the PIP controller.
+ */
+ private class PipControllerPinnedStackListener extends
+ PinnedStackListenerForwarder.PinnedStackListener {
+ @Override
+ public void onListenerRegistered(IPinnedStackController controller) {
+ mHandler.post(() -> mTouchHandler.setPinnedStackController(controller));
+ }
+
+ @Override
+ public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+ mHandler.post(() -> {
+ mPipBoundsHandler.onImeVisibilityChanged(imeVisible, imeHeight);
+ mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight);
+ });
+ }
+
+ @Override
+ public void onMovementBoundsChanged(boolean fromImeAdjustment) {
+ mHandler.post(() -> updateMovementBounds(null /* toBounds */,
+ false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */,
+ null /* windowContainerTransaction */));
+ }
+
+ @Override
+ public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
+ mHandler.post(() -> mMenuController.setAppActions(actions));
+ }
+
+ @Override
+ public void onActivityHidden(ComponentName componentName) {
+ mHandler.post(() -> {
+ 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) {
+ mHandler.post(() -> mPipBoundsHandler.onDisplayInfoChanged(displayInfo));
+ }
+
+ @Override
+ public void onConfigurationChanged() {
+ mHandler.post(() -> {
+ mPipBoundsHandler.onConfigurationChanged(mContext);
+ mTouchHandler.onConfigurationChanged();
+ });
+ }
+
+ @Override
+ public void onAspectRatioChanged(float aspectRatio) {
+ // TODO(b/169373982): Remove this callback as it is redundant with PipTaskOrg params
+ // change.
+ mHandler.post(() -> {
+ mPipBoundsState.setAspectRatio(aspectRatio);
+ mTouchHandler.onAspectRatioChanged();
+ });
+ }
+ }
+
+ protected PipController(Context context,
+ DisplayController displayController,
+ PipAppOpsListener pipAppOpsListener,
+ PipBoundsHandler pipBoundsHandler,
+ @NonNull PipBoundsState pipBoundsState,
+ PipMediaController pipMediaController,
+ PipMenuActivityController pipMenuActivityController,
+ PipTaskOrganizer pipTaskOrganizer,
+ PipTouchHandler pipTouchHandler,
+ WindowManagerShellWrapper windowManagerShellWrapper
+ ) {
+ // Ensure that we are the primary user's SystemUI.
+ final int processUser = UserManager.get(context).getUserHandle();
+ if (processUser != UserHandle.USER_SYSTEM) {
+ throw new IllegalStateException("Non-primary Pip component not currently supported.");
+ }
+
+ mContext = context;
+ mWindowManagerShellWrapper = windowManagerShellWrapper;
+ mDisplayController = displayController;
+ mPipBoundsHandler = pipBoundsHandler;
+ mPipBoundsState = pipBoundsState;
+ mPipTaskOrganizer = pipTaskOrganizer;
+ mPipTaskOrganizer.registerPipTransitionCallback(this);
+ mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> {
+ final DisplayInfo newDisplayInfo = new DisplayInfo();
+ displayController.getDisplay(displayId).getDisplayInfo(newDisplayInfo);
+ mPipBoundsHandler.onDisplayInfoChanged(newDisplayInfo);
+ updateMovementBounds(null /* toBounds */, false /* fromRotation */,
+ false /* fromImeAdjustment */, false /* fromShelfAdustment */,
+ null /* wct */);
+ });
+ mMediaController = pipMediaController;
+ mMenuController = pipMenuActivityController;
+ mTouchHandler = pipTouchHandler;
+ mAppOpsListener = pipAppOpsListener;
+ displayController.addDisplayChangingController(mRotationController);
+ displayController.addDisplayWindowListener(mFixedRotationListener);
+
+ // 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();
+ context.getDisplay().getDisplayInfo(displayInfo);
+ mPipBoundsHandler.onDisplayInfoChanged(displayInfo);
+
+ try {
+ mWindowManagerShellWrapper.addPinnedStackListener(
+ new PipControllerPinnedStackListener());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to register pinned stack listener", e);
+ }
+ }
+
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ mHandler.post(() -> {
+ mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext);
+ });
+ }
+
+ @Override
+ public void onActivityPinned(String packageName) {
+ mHandler.post(() -> {
+ mTouchHandler.onActivityPinned();
+ mMediaController.onActivityPinned();
+ mMenuController.onActivityPinned();
+ mAppOpsListener.onActivityPinned(packageName);
+ });
+ }
+
+ @Override
+ public void onActivityUnpinned(ComponentName topActivity) {
+ mHandler.post(() -> {
+ mMenuController.onActivityUnpinned();
+ mTouchHandler.onActivityUnpinned(topActivity);
+ mAppOpsListener.onActivityUnpinned();
+ });
+ }
+
+ @Override
+ public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+ boolean clearedTask) {
+ if (task.configuration.windowConfiguration.getWindowingMode()
+ != WINDOWING_MODE_PINNED) {
+ return;
+ }
+ mTouchHandler.getMotionHelper().expandLeavePip(clearedTask /* skipAnimation */);
+ }
+
+ @Override
+ public void onOverlayChanged() {
+ mHandler.post(() -> {
+ mPipBoundsHandler.onOverlayChanged(mContext, mContext.getDisplay());
+ updateMovementBounds(null /* toBounds */,
+ false /* fromRotation */, false /* fromImeAdjustment */,
+ false /* fromShelfAdjustment */,
+ null /* windowContainerTransaction */);
+ });
+ }
+
+ @Override
+ public void registerSessionListenerForCurrentUser() {
+ mMediaController.registerSessionListenerForCurrentUser();
+ }
+
+ @Override
+ public void onSystemUiStateChanged(boolean isValidState, int flag) {
+ mTouchHandler.onSystemUiStateChanged(isValidState);
+ }
+
+ /**
+ * Expands the PIP.
+ */
+ @Override
+ public void expandPip() {
+ mTouchHandler.getMotionHelper().expandLeavePip(false /* skipAnimation */);
+ }
+
+ @Override
+ public PipTouchHandler getPipTouchHandler() {
+ return mTouchHandler;
+ }
+
+ /**
+ * Hides the PIP menu.
+ */
+ @Override
+ public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {
+ mMenuController.hideMenu(onStartCallback, onEndCallback);
+ }
+
+ /**
+ * Sent from KEYCODE_WINDOW handler in PhoneWindowManager, to request the menu to be shown.
+ */
+ public void showPictureInPictureMenu() {
+ mTouchHandler.showPictureInPictureMenu();
+ }
+
+ /**
+ * Sets a customized touch gesture that replaces the default one.
+ */
+ public void setTouchGesture(PipTouchGesture gesture) {
+ mTouchHandler.setTouchGesture(gesture);
+ }
+
+ /**
+ * Sets both shelf visibility and its height.
+ */
+ @Override
+ public void setShelfHeight(boolean visible, int height) {
+ mHandler.post(() -> setShelfHeightLocked(visible, height));
+ }
+
+ private void setShelfHeightLocked(boolean visible, int height) {
+ final int shelfHeight = visible ? height : 0;
+ final boolean changed = mPipBoundsHandler.setShelfHeight(visible, shelfHeight);
+ if (changed) {
+ mTouchHandler.onShelfVisibilityChanged(visible, shelfHeight);
+ updateMovementBounds(mPipBoundsState.getBounds(),
+ false /* fromRotation */, false /* fromImeAdjustment */,
+ true /* fromShelfAdjustment */, null /* windowContainerTransaction */);
+ }
+ }
+
+ @Override
+ public void setPinnedStackAnimationType(int animationType) {
+ mHandler.post(() -> mPipTaskOrganizer.setOneShotAnimationType(animationType));
+ }
+
+ @Override
+ public void setPinnedStackAnimationListener(Consumer<Boolean> callback) {
+ mHandler.post(() -> mPinnedStackAnimationRecentsCallback = callback);
+ }
+
+ @Override
+ public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
+ PictureInPictureParams pictureInPictureParams,
+ int launcherRotation, int shelfHeight) {
+ setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight);
+ mPipBoundsHandler.onDisplayRotationChangedNotInPip(mContext, launcherRotation);
+ return mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo,
+ pictureInPictureParams);
+ }
+
+ @Override
+ public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) {
+ mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds);
+ }
+
+ @Override
+ public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {
+ if (isOutPipDirection(direction)) {
+ // Exiting PIP, save the reentry bounds to restore to when re-entering.
+ updateReentryBounds(pipBounds);
+ final float snapFraction = mPipBoundsHandler.getSnapFraction(mReentryBounds);
+ mPipBoundsState.saveReentryState(mReentryBounds, snapFraction);
+ }
+ // Disable touches while the animation is running
+ mTouchHandler.setTouchEnabled(false);
+ if (mPinnedStackAnimationRecentsCallback != null) {
+ mPinnedStackAnimationRecentsCallback.accept(true);
+ }
+ }
+
+ /**
+ * Update the bounds used to save the re-entry size and snap fraction when exiting PIP.
+ */
+ public void updateReentryBounds(Rect bounds) {
+ final Rect reentryBounds = mTouchHandler.getUserResizeBounds();
+ float snapFraction = mPipBoundsHandler.getSnapFraction(bounds);
+ mPipBoundsHandler.applySnapFraction(reentryBounds, snapFraction);
+ mReentryBounds.set(reentryBounds);
+ }
+
+ @Override
+ public void onPipTransitionFinished(ComponentName activity, int direction) {
+ onPipTransitionFinishedOrCanceled(direction);
+ }
+
+ @Override
+ public void onPipTransitionCanceled(ComponentName activity, int direction) {
+ onPipTransitionFinishedOrCanceled(direction);
+ }
+
+ private void onPipTransitionFinishedOrCanceled(int direction) {
+ // Re-enable touches after the animation completes
+ mTouchHandler.setTouchEnabled(true);
+ mTouchHandler.onPinnedStackAnimationEnded(direction);
+ mMenuController.onPinnedStackAnimationEnded();
+ }
+
+ private void updateMovementBounds(@Nullable Rect toBounds, boolean fromRotation,
+ boolean fromImeAdjustment, boolean fromShelfAdjustment,
+ WindowContainerTransaction wct) {
+ // Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before
+ // passing to mTouchHandler/mPipTaskOrganizer
+ final Rect outBounds = new Rect(toBounds);
+ mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds,
+ outBounds, mTmpDisplayInfo);
+ // mTouchHandler would rely on the bounds populated from mPipTaskOrganizer
+ mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment,
+ fromShelfAdjustment, wct);
+ mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds,
+ outBounds, fromImeAdjustment, fromShelfAdjustment,
+ mTmpDisplayInfo.rotation);
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG);
+ mMenuController.dump(pw, innerPrefix);
+ mTouchHandler.dump(pw, innerPrefix);
+ mPipBoundsHandler.dump(pw, innerPrefix);
+ mPipTaskOrganizer.dump(pw, innerPrefix);
+ mPipBoundsState.dump(pw, innerPrefix);
+ }
+
+ /**
+ * Instantiates {@link PipController}, returns {@code null} if the feature not supported.
+ */
+ @Nullable
+ public static PipController create(Context context, DisplayController displayController,
+ PipAppOpsListener pipAppOpsListener, PipBoundsHandler pipBoundsHandler,
+ PipBoundsState pipBoundsState, PipMediaController pipMediaController,
+ PipMenuActivityController pipMenuActivityController,
+ PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler,
+ WindowManagerShellWrapper windowManagerShellWrapper) {
+ if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
+ Slog.w(TAG, "Device doesn't support Pip feature");
+ return null;
+ }
+
+ return new PipController(context, displayController, pipAppOpsListener, pipBoundsHandler,
+ pipBoundsState, pipMediaController, pipMenuActivityController,
+ pipTaskOrganizer, pipTouchHandler, windowManagerShellWrapper);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
new file mode 100644
index 000000000000..bebe5f965251
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
@@ -0,0 +1,295 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.common.DismissCircleView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.pip.PipUiEventLogger;
+
+import kotlin.Unit;
+
+/**
+ * Handler of all Magnetized Object related code for PiP.
+ */
+public class PipDismissTargetHandler {
+
+ /* The multiplier to apply scale the target size by when applying the magnetic field radius */
+ private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f;
+
+ /** Duration of the dismiss scrim fading in/out. */
+ private static final int DISMISS_TRANSITION_DURATION_MS = 200;
+
+ /**
+ * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move
+ * PIP.
+ */
+ private final MagnetizedObject<Rect> mMagnetizedPip;
+
+ /**
+ * Container for the dismiss circle, so that it can be animated within the container via
+ * translation rather than within the WindowManager via slow layout animations.
+ */
+ private final ViewGroup mTargetViewContainer;
+
+ /** Circle view used to render the dismiss target. */
+ private final DismissCircleView mTargetView;
+
+ /**
+ * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius.
+ */
+ private final MagnetizedObject.MagneticTarget mMagneticTarget;
+
+ /** PhysicsAnimator instance for animating the dismiss target in/out. */
+ private final PhysicsAnimator<View> mMagneticTargetAnimator;
+
+ /** Default configuration to use for springing the dismiss target in/out. */
+ private final PhysicsAnimator.SpringConfig mTargetSpringConfig =
+ new PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+
+ /**
+ * Runnable that can be posted delayed to show the target. This needs to be saved as a member
+ * variable so we can pass it to removeCallbacks.
+ */
+ private Runnable mShowTargetAction = this::showDismissTargetMaybe;
+
+ // Allow dragging the PIP to a location to close it
+ private final boolean mEnableDismissDragToEdge;
+
+ private int mDismissAreaHeight;
+
+ private final Context mContext;
+ private final PipMotionHelper mMotionHelper;
+ private final PipUiEventLogger mPipUiEventLogger;
+ private final WindowManager mWindowManager;
+ private final Handler mHandler;
+
+ public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger,
+ PipMotionHelper motionHelper, Handler handler) {
+ mContext = context;
+ mPipUiEventLogger = pipUiEventLogger;
+ mMotionHelper = motionHelper;
+ mHandler = handler;
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+
+ Resources res = context.getResources();
+ mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge);
+ mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
+
+ mTargetView = new DismissCircleView(context);
+ mTargetViewContainer = new FrameLayout(context);
+ mTargetViewContainer.setBackgroundDrawable(
+ context.getDrawable(R.drawable.floating_dismiss_gradient_transition));
+ mTargetViewContainer.setClipChildren(false);
+ mTargetViewContainer.addView(mTargetView);
+
+ mMagnetizedPip = mMotionHelper.getMagnetizedPip();
+ mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0);
+ updateMagneticTargetSize();
+
+ mMagnetizedPip.setAnimateStuckToTarget(
+ (target, velX, velY, flung, after) -> {
+ if (mEnableDismissDragToEdge) {
+ mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after);
+ }
+ return Unit.INSTANCE;
+ });
+ mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ // Show the dismiss target, in case the initial touch event occurred within the
+ // magnetic field radius.
+ if (mEnableDismissDragToEdge) {
+ showDismissTargetMaybe();
+ }
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ float velX, float velY, boolean wasFlungOut) {
+ if (wasFlungOut) {
+ mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */);
+ hideDismissTargetMaybe();
+ } else {
+ mMotionHelper.setSpringingToTouch(true);
+ }
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ mMotionHelper.notifyDismissalPending();
+
+ handler.post(() -> {
+ mMotionHelper.animateDismiss();
+ hideDismissTargetMaybe();
+ });
+
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE);
+ }
+ });
+
+ mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView);
+ }
+
+ /**
+ * Potentially start consuming future motion events if PiP is currently near the magnetized
+ * object.
+ */
+ public boolean maybeConsumeMotionEvent(MotionEvent ev) {
+ return mMagnetizedPip.maybeConsumeMotionEvent(ev);
+ }
+
+ /**
+ * Update the magnet size.
+ */
+ public void updateMagneticTargetSize() {
+ if (mTargetView == null) {
+ return;
+ }
+
+ final Resources res = mContext.getResources();
+ final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
+ mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
+ final FrameLayout.LayoutParams newParams =
+ new FrameLayout.LayoutParams(targetSize, targetSize);
+ newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+ newParams.bottomMargin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.floating_dismiss_bottom_margin);
+ mTargetView.setLayoutParams(newParams);
+
+ // Set the magnetic field radius equal to the target size from the center of the target
+ mMagneticTarget.setMagneticFieldRadiusPx(
+ (int) (targetSize * MAGNETIC_FIELD_RADIUS_MULTIPLIER));
+ }
+
+ /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */
+ public void createOrUpdateDismissTarget() {
+ if (!mTargetViewContainer.isAttachedToWindow()) {
+ mHandler.removeCallbacks(mShowTargetAction);
+ mMagneticTargetAnimator.cancel();
+
+ mTargetViewContainer.setVisibility(View.INVISIBLE);
+
+ try {
+ mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams());
+ } catch (IllegalStateException e) {
+ // This shouldn't happen, but if the target is already added, just update its layout
+ // params.
+ mWindowManager.updateViewLayout(
+ mTargetViewContainer, getDismissTargetLayoutParams());
+ }
+ } else {
+ mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams());
+ }
+ }
+
+ /** Returns layout params for the dismiss target, using the latest display metrics. */
+ private WindowManager.LayoutParams getDismissTargetLayoutParams() {
+ final Point windowSize = new Point();
+ mWindowManager.getDefaultDisplay().getRealSize(windowSize);
+
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ mDismissAreaHeight,
+ 0, windowSize.y - mDismissAreaHeight,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+
+ lp.setTitle("pip-dismiss-overlay");
+ lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ lp.setFitInsetsTypes(0 /* types */);
+
+ return lp;
+ }
+
+ /** Makes the dismiss target visible and animates it in, if it isn't already visible. */
+ public void showDismissTargetMaybe() {
+ if (!mEnableDismissDragToEdge) {
+ return;
+ }
+
+ createOrUpdateDismissTarget();
+
+ if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
+
+ mTargetView.setTranslationY(mTargetViewContainer.getHeight());
+ mTargetViewContainer.setVisibility(View.VISIBLE);
+
+ // Cancel in case we were in the middle of animating it out.
+ mMagneticTargetAnimator.cancel();
+ mMagneticTargetAnimator
+ .spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig)
+ .start();
+
+ ((TransitionDrawable) mTargetViewContainer.getBackground()).startTransition(
+ DISMISS_TRANSITION_DURATION_MS);
+ }
+ }
+
+ /** Animates the magnetic dismiss target out and then sets it to GONE. */
+ public void hideDismissTargetMaybe() {
+ if (!mEnableDismissDragToEdge) {
+ return;
+ }
+
+ mHandler.removeCallbacks(mShowTargetAction);
+ mMagneticTargetAnimator
+ .spring(DynamicAnimation.TRANSLATION_Y,
+ mTargetViewContainer.getHeight(),
+ mTargetSpringConfig)
+ .withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE))
+ .start();
+
+ ((TransitionDrawable) mTargetViewContainer.getBackground()).reverseTransition(
+ DISMISS_TRANSITION_DURATION_MS);
+ }
+
+ /**
+ * Removes the dismiss target and cancels any pending callbacks to show it.
+ */
+ public void cleanUpDismissTarget() {
+ mHandler.removeCallbacks(mShowTargetAction);
+
+ if (mTargetViewContainer.isAttachedToWindow()) {
+ mWindowManager.removeViewImmediate(mTargetViewContainer);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMediaController.java
new file mode 100644
index 000000000000..64e3758fd81a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMediaController.java
@@ -0,0 +1,261 @@
+/*
+ * 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 static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+
+import android.app.IActivityManager;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.UserHandle;
+
+import com.android.wm.shell.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only
+ * if there are no actions from the PiP activity itself). The active media controller is only set
+ * when there is a media session from the top PiP activity.
+ */
+public class PipMediaController {
+
+ private static final String ACTION_PLAY = "com.android.wm.shell.pip.phone.PLAY";
+ private static final String ACTION_PAUSE = "com.android.wm.shell.pip.phone.PAUSE";
+ private static final String ACTION_NEXT = "com.android.wm.shell.pip.phone.NEXT";
+ private static final String ACTION_PREV = "com.android.wm.shell.pip.phone.PREV";
+
+ /**
+ * A listener interface to receive notification on changes to the media actions.
+ */
+ public interface ActionListener {
+ /**
+ * Called when the media actions changes.
+ */
+ void onMediaActionsChanged(List<RemoteAction> actions);
+ }
+
+ private final Context mContext;
+ private final IActivityManager mActivityManager;
+
+ private final MediaSessionManager mMediaSessionManager;
+ private MediaController mMediaController;
+
+ private RemoteAction mPauseAction;
+ private RemoteAction mPlayAction;
+ private RemoteAction mNextAction;
+ private RemoteAction mPrevAction;
+
+ private BroadcastReceiver mPlayPauseActionReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(ACTION_PLAY)) {
+ mMediaController.getTransportControls().play();
+ } else if (action.equals(ACTION_PAUSE)) {
+ mMediaController.getTransportControls().pause();
+ } else if (action.equals(ACTION_NEXT)) {
+ mMediaController.getTransportControls().skipToNext();
+ } else if (action.equals(ACTION_PREV)) {
+ mMediaController.getTransportControls().skipToPrevious();
+ }
+ }
+ };
+
+ private final MediaController.Callback mPlaybackChangedListener =
+ new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ notifyActionsChanged();
+ }
+ };
+
+ private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener =
+ controllers -> resolveActiveMediaController(controllers);
+
+ private ArrayList<ActionListener> mListeners = new ArrayList<>();
+
+ public PipMediaController(Context context, IActivityManager activityManager) {
+ mContext = context;
+ mActivityManager = activityManager;
+ IntentFilter mediaControlFilter = new IntentFilter();
+ mediaControlFilter.addAction(ACTION_PLAY);
+ mediaControlFilter.addAction(ACTION_PAUSE);
+ mediaControlFilter.addAction(ACTION_NEXT);
+ mediaControlFilter.addAction(ACTION_PREV);
+ mContext.registerReceiver(mPlayPauseActionReceiver, mediaControlFilter,
+ UserHandle.USER_ALL);
+
+ createMediaActions();
+ mMediaSessionManager =
+ (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
+ }
+
+ /**
+ * Handles when an activity is pinned.
+ */
+ public void onActivityPinned() {
+ // Once we enter PiP, try to find the active media controller for the top most activity
+ resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null,
+ UserHandle.USER_CURRENT));
+ }
+
+ /**
+ * Adds a new media action listener.
+ */
+ public void addListener(ActionListener listener) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ listener.onMediaActionsChanged(getMediaActions());
+ }
+ }
+
+ /**
+ * Removes a media action listener.
+ */
+ public void removeListener(ActionListener listener) {
+ listener.onMediaActionsChanged(Collections.EMPTY_LIST);
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Gets the set of media actions currently available.
+ */
+ private List<RemoteAction> getMediaActions() {
+ if (mMediaController == null || mMediaController.getPlaybackState() == null) {
+ return Collections.EMPTY_LIST;
+ }
+
+ ArrayList<RemoteAction> mediaActions = new ArrayList<>();
+ boolean isPlaying = mMediaController.getPlaybackState().isActiveState();
+ long actions = mMediaController.getPlaybackState().getActions();
+
+ // Prev action
+ mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
+ mediaActions.add(mPrevAction);
+
+ // Play/pause action
+ if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
+ mediaActions.add(mPlayAction);
+ } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
+ mediaActions.add(mPauseAction);
+ }
+
+ // Next action
+ mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0);
+ mediaActions.add(mNextAction);
+ return mediaActions;
+ }
+
+ /**
+ * Creates the standard media buttons that we may show.
+ */
+ private void createMediaActions() {
+ String pauseDescription = mContext.getString(R.string.pip_pause);
+ mPauseAction = new RemoteAction(Icon.createWithResource(mContext,
+ R.drawable.pip_ic_pause_white), pauseDescription, pauseDescription,
+ PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PAUSE),
+ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
+
+ String playDescription = mContext.getString(R.string.pip_play);
+ mPlayAction = new RemoteAction(Icon.createWithResource(mContext,
+ R.drawable.pip_ic_play_arrow_white), playDescription, playDescription,
+ PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PLAY),
+ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
+
+ String nextDescription = mContext.getString(R.string.pip_skip_to_next);
+ mNextAction = new RemoteAction(Icon.createWithResource(mContext,
+ R.drawable.pip_ic_skip_next_white), nextDescription, nextDescription,
+ PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NEXT),
+ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
+
+ String prevDescription = mContext.getString(R.string.pip_skip_to_prev);
+ mPrevAction = new RemoteAction(Icon.createWithResource(mContext,
+ R.drawable.pip_ic_skip_previous_white), prevDescription, prevDescription,
+ PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PREV),
+ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
+ }
+
+ /**
+ * Re-registers the session listener for the current user.
+ */
+ public void registerSessionListenerForCurrentUser() {
+ mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener);
+ mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsChangedListener, null,
+ UserHandle.USER_CURRENT, null);
+ }
+
+ /**
+ * Tries to find and set the active media controller for the top PiP activity.
+ */
+ private void resolveActiveMediaController(List<MediaController> controllers) {
+ if (controllers != null) {
+ final ComponentName topActivity = PipUtils.getTopPipActivity(mContext,
+ mActivityManager).first;
+ if (topActivity != null) {
+ for (int i = 0; i < controllers.size(); i++) {
+ final MediaController controller = controllers.get(i);
+ if (controller.getPackageName().equals(topActivity.getPackageName())) {
+ setActiveMediaController(controller);
+ return;
+ }
+ }
+ }
+ }
+ setActiveMediaController(null);
+ }
+
+ /**
+ * Sets the active media controller for the top PiP activity.
+ */
+ private void setActiveMediaController(MediaController controller) {
+ if (controller != mMediaController) {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mPlaybackChangedListener);
+ }
+ mMediaController = controller;
+ if (controller != null) {
+ controller.registerCallback(mPlaybackChangedListener);
+ }
+ notifyActionsChanged();
+
+ // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
+ }
+ }
+
+ /**
+ * Notifies all listeners that the actions have changed.
+ */
+ private void notifyActionsChanged() {
+ if (!mListeners.isEmpty()) {
+ List<RemoteAction> actions = getMediaActions();
+ mListeners.forEach(l -> l.onMediaActionsChanged(actions));
+ }
+ }
+}
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/PipMenuActivityController.java
new file mode 100644
index 000000000000..cd47d55da7f0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActivityController.java
@@ -0,0 +1,395 @@
+/*
+ * 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 static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
+import android.app.ActivityTaskManager;
+import android.app.ActivityTaskManager.RootTaskInfo;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.Debug;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import com.android.wm.shell.pip.PipTaskOrganizer;
+import com.android.wm.shell.pip.phone.PipMediaController.ActionListener;
+
+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.
+ *
+ * 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 {
+
+ private static final String TAG = "PipMenuActController";
+ private static final boolean DEBUG = false;
+
+ public static final int MENU_STATE_NONE = 0;
+ public static final int MENU_STATE_CLOSE = 1;
+ public static final int MENU_STATE_FULL = 2;
+
+ /**
+ * A listener interface to receive notification on changes in PIP.
+ */
+ public interface Listener {
+ /**
+ * Called when the PIP menu visibility changes.
+ *
+ * @param menuState the current state of the menu
+ * @param resize whether or not to resize the PiP with the state change
+ */
+ void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback);
+
+ /**
+ * Called when the PIP requested to be expanded.
+ */
+ void onPipExpand();
+
+ /**
+ * Called when the PIP requested to be dismissed.
+ */
+ void onPipDismiss();
+
+ /**
+ * Called when the PIP requested to show the menu.
+ */
+ void onPipShowMenu();
+ }
+
+ private Context mContext;
+ private PipTaskOrganizer mPipTaskOrganizer;
+ private PipMediaController mMediaController;
+
+ private ArrayList<Listener> mListeners = new ArrayList<>();
+ private ParceledListSlice<RemoteAction> mAppActions;
+ private ParceledListSlice<RemoteAction> mMediaActions;
+ private int mMenuState;
+
+ private PipMenuView mPipMenuView;
+ private IBinder mPipMenuInputToken;
+
+ private ActionListener mMediaActionListener = new ActionListener() {
+ @Override
+ public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
+ mMediaActions = new ParceledListSlice<>(mediaActions);
+ updateMenuActions();
+ }
+ };
+
+ public PipMenuActivityController(Context context,
+ PipMediaController mediaController, PipTaskOrganizer pipTaskOrganizer) {
+ mContext = context;
+ mMediaController = mediaController;
+ mPipTaskOrganizer = pipTaskOrganizer;
+ }
+
+ public boolean isMenuVisible() {
+ return mPipMenuView != null && mMenuState != MENU_STATE_NONE;
+ }
+
+ public void onActivityPinned() {
+ attachPipMenuView();
+ }
+
+ public void onActivityUnpinned() {
+ hideMenu();
+ mPipTaskOrganizer.detachPipMenuViewHost();
+ mPipMenuView = null;
+ mPipMenuInputToken = null;
+ }
+
+ public void onPinnedStackAnimationEnded() {
+ if (isMenuVisible()) {
+ mPipMenuView.onPipAnimationEnded();
+ }
+ }
+
+ private void attachPipMenuView() {
+ if (mPipMenuView == null) {
+ mPipMenuView = new PipMenuView(mContext, this);
+
+ }
+
+ // If we haven't gotten the input toekn, that means we haven't had a success attempt
+ // yet at attaching the PipMenuView
+ if (mPipMenuInputToken == null) {
+ mPipMenuInputToken = mPipTaskOrganizer.attachPipMenuViewHost(mPipMenuView,
+ getPipMenuLayoutParams(0, 0));
+ }
+ }
+
+ /**
+ * Adds a new menu activity listener.
+ */
+ public void addListener(Listener listener) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+
+ /**
+ * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu
+ * upon PiP window transition is finished.
+ */
+ public void showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean willResizeMenu, boolean showResizeHandle) {
+ // hide all visible controls including close button and etc. first, this is to ensure
+ // menu is totally invisible during the transition to eliminate unpleasant artifacts
+ fadeOutMenu();
+ showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+ true /* withDelay */, showResizeHandle);
+ }
+
+ /**
+ * Shows the menu activity immediately.
+ */
+ public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean willResizeMenu, boolean showResizeHandle) {
+ showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+ false /* withDelay */, showResizeHandle);
+ }
+
+ private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
+ if (DEBUG) {
+ Log.d(TAG, "showMenu() state=" + menuState
+ + " isMenuVisible=" + isMenuVisible()
+ + " allowMenuTimeout=" + allowMenuTimeout
+ + " willResizeMenu=" + willResizeMenu
+ + " withDelay=" + withDelay
+ + " showResizeHandle=" + showResizeHandle
+ + " callers=\n" + Debug.getCallers(5, " "));
+ }
+
+ if (!mPipTaskOrganizer.isPipMenuViewHostAttached()) {
+ Log.d(TAG, "PipMenu has not been attached yet. Attaching now at showMenuInternal().");
+ attachPipMenuView();
+ }
+
+ mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
+ showResizeHandle);
+ }
+
+ /**
+ * Pokes the menu, indicating that the user is interacting with it.
+ */
+ public void pokeMenu() {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ Log.d(TAG, "pokeMenu() isMenuVisible=" + isMenuVisible);
+ }
+ if (isMenuVisible) {
+ mPipMenuView.pokeMenu();
+ }
+ }
+
+ private void fadeOutMenu() {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ Log.d(TAG, "fadeOutMenu() isMenuVisible=" + isMenuVisible);
+ }
+ if (isMenuVisible) {
+ mPipMenuView.fadeOutMenu();
+ }
+ }
+
+ /**
+ * Hides the menu activity.
+ */
+ public void hideMenu() {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ Log.d(TAG, "hideMenu() state=" + mMenuState
+ + " isMenuVisible=" + isMenuVisible
+ + " callers=\n" + Debug.getCallers(5, " "));
+ }
+ if (isMenuVisible) {
+ mPipMenuView.hideMenu();
+ }
+ }
+
+ /**
+ * Hides the menu activity.
+ */
+ public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) {
+ if (isMenuVisible()) {
+ // If the menu is visible in either the closed or full state, then hide the menu and
+ // trigger the animation trigger afterwards
+ if (onStartCallback != null) {
+ onStartCallback.run();
+ }
+ mPipMenuView.hideMenu(onEndCallback);
+ }
+ }
+
+ /**
+ * Preemptively mark the menu as invisible, used when we are directly manipulating the pinned
+ * stack and don't want to trigger a resize which can animate the stack in a conflicting way
+ * (ie. when manually expanding or dismissing).
+ */
+ public void hideMenuWithoutResize() {
+ onMenuStateChanged(MENU_STATE_NONE, false /* resize */, null /* callback */);
+ }
+
+ /**
+ * Sets the menu actions to the actions provided by the current PiP activity.
+ */
+ public void setAppActions(ParceledListSlice<RemoteAction> appActions) {
+ mAppActions = appActions;
+ updateMenuActions();
+ }
+
+ void onPipExpand() {
+ mListeners.forEach(Listener::onPipExpand);
+ }
+
+ void onPipDismiss() {
+ mListeners.forEach(Listener::onPipDismiss);
+ }
+
+ void onPipShowMenu() {
+ mListeners.forEach(Listener::onPipShowMenu);
+ }
+
+ /**
+ * @return the best set of actions to show in the PiP menu.
+ */
+ private ParceledListSlice<RemoteAction> resolveMenuActions() {
+ if (isValidActions(mAppActions)) {
+ return mAppActions;
+ }
+ return mMediaActions;
+ }
+
+ /**
+ * 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) {
+ return new WindowManager.LayoutParams(width, height,
+ WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSLUCENT);
+ }
+
+ /**
+ * Updates the PiP menu with the best set of actions provided.
+ */
+ private void updateMenuActions() {
+ if (isMenuVisible()) {
+ // Fetch the pinned stack bounds
+ Rect stackBounds = null;
+ try {
+ RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
+ if (pinnedTaskInfo != null) {
+ stackBounds = pinnedTaskInfo.bounds;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error showing PIP menu", e);
+ }
+
+ mPipMenuView.setActions(stackBounds, resolveMenuActions().getList());
+ }
+ }
+
+ /**
+ * Returns whether the set of actions are valid.
+ */
+ private static boolean isValidActions(ParceledListSlice<?> actions) {
+ return actions != null && actions.getList().size() > 0;
+ }
+
+ /**
+ * Handles changes in menu visibility.
+ */
+ void onMenuStateChanged(int menuState, boolean resize, Runnable callback) {
+ if (DEBUG) {
+ Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState
+ + " menuState=" + menuState + " resize=" + resize
+ + " callers=\n" + Debug.getCallers(5, " "));
+ }
+
+ if (menuState != mMenuState) {
+ mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize, callback));
+ if (menuState == MENU_STATE_FULL) {
+ // Once visible, start listening for media action changes. This call will trigger
+ // the menu actions to be updated again.
+ mMediaController.addListener(mMediaActionListener);
+ } else {
+ // Once hidden, stop listening for media action changes. This call will trigger
+ // the menu actions to be updated again.
+ mMediaController.removeListener(mMediaActionListener);
+ }
+
+ try {
+ WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
+ mPipMenuInputToken, menuState != MENU_STATE_NONE /* grantFocus */);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to update focus as menu appears/disappears", e);
+ }
+ }
+ mMenuState = menuState;
+ }
+
+ /**
+ * Handles a pointer event sent from pip input consumer.
+ */
+ void handlePointerEvent(MotionEvent ev) {
+ if (ev.isTouchEvent()) {
+ mPipMenuView.dispatchTouchEvent(ev);
+ } else {
+ mPipMenuView.dispatchGenericMotionEvent(ev);
+ }
+ }
+
+ /**
+ * Tell the PIP Menu to recalculate its layout given its current position on the display.
+ */
+ public void updateMenuLayout(Rect bounds) {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ Log.d(TAG, "updateMenuLayout() state=" + mMenuState
+ + " isMenuVisible=" + isMenuVisible
+ + " callers=\n" + Debug.getCallers(5, " "));
+ }
+ if (isMenuVisible) {
+ mPipMenuView.updateMenuLayout(bounds);
+ }
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mMenuState=" + mMenuState);
+ pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView);
+ pw.println(innerPrefix + "mListeners=" + mListeners.size());
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java
new file mode 100644
index 000000000000..985cd0f1fa19
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java
@@ -0,0 +1,86 @@
+/*
+ * 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.content.Context;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * Helper class to calculate and place the menu icons on the PIP Menu.
+ */
+public class PipMenuIconsAlgorithm {
+
+ private static final String TAG = "PipMenuIconsAlgorithm";
+
+ private boolean mFinishedLayout = false;
+ protected ViewGroup mViewRoot;
+ protected ViewGroup mTopEndContainer;
+ protected View mDragHandle;
+ protected View mSettingsButton;
+ protected View mDismissButton;
+
+ protected PipMenuIconsAlgorithm(Context context) {
+ }
+
+ /**
+ * Bind the necessary views.
+ */
+ public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle,
+ View settingsButton, View dismissButton) {
+ mViewRoot = viewRoot;
+ mTopEndContainer = topEndContainer;
+ mDragHandle = dragHandle;
+ mSettingsButton = settingsButton;
+ mDismissButton = dismissButton;
+ }
+
+ /**
+ * Updates the position of the drag handle based on where the PIP window is on the screen.
+ */
+ public void onBoundsChanged(Rect bounds) {
+ if (mViewRoot == null || mTopEndContainer == null || mDragHandle == null
+ || mSettingsButton == null || mDismissButton == null) {
+ Log.e(TAG, "One if the required views is null.");
+ }
+
+ //We only need to calculate the layout once since it does not change.
+ if (!mFinishedLayout) {
+ mTopEndContainer.removeView(mSettingsButton);
+ mViewRoot.addView(mSettingsButton);
+
+ setLayoutGravity(mDragHandle, Gravity.START | Gravity.TOP);
+ setLayoutGravity(mSettingsButton, Gravity.START | Gravity.TOP);
+ mFinishedLayout = true;
+ }
+ }
+
+ /**
+ * Set the gravity on the given view.
+ */
+ protected static void setLayoutGravity(View v, int gravity) {
+ if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) {
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams();
+ params.gravity = gravity;
+ v.setLayoutParams(params);
+ }
+ }
+}
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
new file mode 100644
index 000000000000..51951409f76c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -0,0 +1,474 @@
+/*
+ * 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 static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
+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 android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.app.PendingIntent.CanceledException;
+import android.app.RemoteAction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.Interpolators;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Translucent window that gets started on top of a task in PIP to allow the user to control it.
+ */
+public class PipMenuView extends FrameLayout {
+
+ private static final String TAG = "PipMenuView";
+
+ private static final int MESSAGE_INVALID_TYPE = -1;
+ public static final int MESSAGE_MENU_EXPANDED = 8;
+
+ private static final int INITIAL_DISMISS_DELAY = 3500;
+ private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
+ private static final long MENU_FADE_DURATION = 125;
+ private static final long MENU_SLOW_FADE_DURATION = 175;
+ private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30;
+
+ private static final float MENU_BACKGROUND_ALPHA = 0.3f;
+ private static final float DISMISS_BACKGROUND_ALPHA = 0.6f;
+
+ private static final float DISABLED_ACTION_ALPHA = 0.54f;
+
+ private static final boolean ENABLE_RESIZE_HANDLE = false;
+
+ private int mMenuState;
+ private boolean mResize = true;
+ private boolean mAllowMenuTimeout = true;
+ private boolean mAllowTouches = true;
+
+ private final List<RemoteAction> mActions = new ArrayList<>();
+
+ private AccessibilityManager mAccessibilityManager;
+ private Drawable mBackgroundDrawable;
+ private View mMenuContainer;
+ private LinearLayout mActionsGroup;
+ private int mBetweenActionPaddingLand;
+
+ private AnimatorSet mMenuContainerAnimator;
+ private PipMenuActivityController mController;
+
+ private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float alpha = (float) animation.getAnimatedValue();
+ mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255));
+ }
+ };
+
+ private Handler mHandler = new Handler();
+
+ private final Runnable mHideMenuRunnable = this::hideMenu;
+
+ protected View mViewRoot;
+ protected View mSettingsButton;
+ protected View mDismissButton;
+ protected View mResizeHandle;
+ protected View mTopEndContainer;
+ protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm;
+
+ public PipMenuView(Context context, PipMenuActivityController controller) {
+ super(context, null, 0);
+ mContext = context;
+ mController = controller;
+
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ inflate(context, R.layout.pip_menu, this);
+
+ mBackgroundDrawable = new ColorDrawable(Color.BLACK);
+ mBackgroundDrawable.setAlpha(0);
+ mViewRoot = findViewById(R.id.background);
+ mViewRoot.setBackground(mBackgroundDrawable);
+ mMenuContainer = findViewById(R.id.menu_container);
+ mMenuContainer.setAlpha(0);
+ mTopEndContainer = findViewById(R.id.top_end_container);
+ mSettingsButton = findViewById(R.id.settings);
+ mSettingsButton.setAlpha(0);
+ mSettingsButton.setOnClickListener((v) -> {
+ if (v.getAlpha() != 0) {
+ showSettings();
+ }
+ });
+ mDismissButton = findViewById(R.id.dismiss);
+ mDismissButton.setAlpha(0);
+ mDismissButton.setOnClickListener(v -> dismissPip());
+ findViewById(R.id.expand_button).setOnClickListener(v -> {
+ if (mMenuContainer.getAlpha() != 0) {
+ expandPip();
+ }
+ });
+
+ mResizeHandle = findViewById(R.id.resize_handle);
+ mResizeHandle.setAlpha(0);
+ mActionsGroup = findViewById(R.id.actions_group);
+ mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
+ R.dimen.pip_between_action_padding_land);
+ mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext);
+ mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer,
+ mResizeHandle, mSettingsButton, mDismissButton);
+
+ initAccessibility();
+ }
+
+ private void initAccessibility() {
+ this.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ String label = getResources().getString(R.string.pip_menu_title);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label));
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) {
+ mController.onPipShowMenu();
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ });
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ hideMenu();
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (!mAllowTouches) {
+ return false;
+ }
+
+ if (mAllowMenuTimeout) {
+ repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+ }
+
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
+ if (mAllowMenuTimeout) {
+ repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+ }
+
+ return super.dispatchGenericMotionEvent(event);
+ }
+
+ void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) {
+ mAllowMenuTimeout = allowMenuTimeout;
+ if (mMenuState != menuState) {
+ // Disallow touches if the menu needs to resize while showing, and we are transitioning
+ // to/from a full menu state.
+ boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow
+ && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL);
+ mAllowTouches = !disallowTouchesUntilAnimationEnd;
+ cancelDelayedHide();
+ updateActionViews(stackBounds);
+ if (mMenuContainerAnimator != null) {
+ mMenuContainerAnimator.cancel();
+ }
+ mMenuContainerAnimator = new AnimatorSet();
+ ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+ mMenuContainer.getAlpha(), 1f);
+ menuAnim.addUpdateListener(mMenuBgUpdateListener);
+ ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+ mSettingsButton.getAlpha(), 1f);
+ ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+ mDismissButton.getAlpha(), 1f);
+ ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA,
+ mResizeHandle.getAlpha(),
+ ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle
+ ? 1f : 0f);
+ if (menuState == MENU_STATE_FULL) {
+ mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim,
+ resizeAnim);
+ } else {
+ mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim);
+ }
+ mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
+ mMenuContainerAnimator.setDuration(menuState == MENU_STATE_CLOSE
+ ? MENU_FADE_DURATION
+ : MENU_SLOW_FADE_DURATION);
+ if (allowMenuTimeout) {
+ mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ repostDelayedHide(INITIAL_DISMISS_DELAY);
+ }
+ });
+ }
+ if (withDelay) {
+ // starts the menu container animation after window expansion is completed
+ notifyMenuStateChange(menuState, resizeMenuOnShow, () -> {
+ if (mMenuContainerAnimator == null) {
+ return;
+ }
+ mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY);
+ mMenuContainerAnimator.start();
+ });
+ } else {
+ notifyMenuStateChange(menuState, resizeMenuOnShow, null);
+ mMenuContainerAnimator.start();
+ }
+ } else {
+ // If we are already visible, then just start the delayed dismiss and unregister any
+ // existing input consumers from the previous drag
+ if (allowMenuTimeout) {
+ repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+ }
+ }
+ }
+
+ /**
+ * Different from {@link #hideMenu()}, this function does not try to finish this menu activity
+ * and instead, it fades out the controls by setting the alpha to 0 directly without menu
+ * visibility callbacks invoked.
+ */
+ void fadeOutMenu() {
+ mMenuContainer.setAlpha(0f);
+ mSettingsButton.setAlpha(0f);
+ mDismissButton.setAlpha(0f);
+ mResizeHandle.setAlpha(0f);
+ }
+
+ void pokeMenu() {
+ cancelDelayedHide();
+ }
+
+ void onPipAnimationEnded() {
+ mAllowTouches = true;
+ }
+
+ void updateMenuLayout(Rect bounds) {
+ mPipMenuIconsAlgorithm.onBoundsChanged(bounds);
+ }
+
+ void hideMenu() {
+ hideMenu(null);
+ }
+
+ void hideMenu(Runnable animationEndCallback) {
+ hideMenu(animationEndCallback, true /* notifyMenuVisibility */, true /* animate */);
+ }
+
+ private void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility,
+ boolean animate) {
+ if (mMenuState != MENU_STATE_NONE) {
+ cancelDelayedHide();
+ if (notifyMenuVisibility) {
+ notifyMenuStateChange(MENU_STATE_NONE, mResize, null);
+ }
+ mMenuContainerAnimator = new AnimatorSet();
+ ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+ mMenuContainer.getAlpha(), 0f);
+ menuAnim.addUpdateListener(mMenuBgUpdateListener);
+ ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+ mSettingsButton.getAlpha(), 0f);
+ ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+ mDismissButton.getAlpha(), 0f);
+ ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA,
+ mResizeHandle.getAlpha(), 0f);
+ mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, resizeAnim);
+ mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
+ mMenuContainerAnimator.setDuration(animate ? MENU_FADE_DURATION : 0);
+ mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (animationFinishedRunnable != null) {
+ animationFinishedRunnable.run();
+ }
+ }
+ });
+ mMenuContainerAnimator.start();
+ }
+ }
+
+ void setActions(Rect stackBounds, List<RemoteAction> actions) {
+ mActions.clear();
+ mActions.addAll(actions);
+ updateActionViews(stackBounds);
+ }
+
+ private void updateActionViews(Rect stackBounds) {
+ ViewGroup expandContainer = findViewById(R.id.expand_container);
+ ViewGroup actionsContainer = findViewById(R.id.actions_container);
+ actionsContainer.setOnTouchListener((v, ev) -> {
+ // Do nothing, prevent click through to parent
+ return true;
+ });
+
+ if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE) {
+ actionsContainer.setVisibility(View.INVISIBLE);
+ } else {
+ actionsContainer.setVisibility(View.VISIBLE);
+ if (mActionsGroup != null) {
+ // Ensure we have as many buttons as actions
+ final LayoutInflater inflater = LayoutInflater.from(mContext);
+ while (mActionsGroup.getChildCount() < mActions.size()) {
+ final ImageButton actionView = (ImageButton) inflater.inflate(
+ R.layout.pip_menu_action, mActionsGroup, false);
+ mActionsGroup.addView(actionView);
+ }
+
+ // Update the visibility of all views
+ for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
+ mActionsGroup.getChildAt(i).setVisibility(i < mActions.size()
+ ? View.VISIBLE
+ : View.GONE);
+ }
+
+ // Recreate the layout
+ final boolean isLandscapePip = stackBounds != null
+ && (stackBounds.width() > stackBounds.height());
+ for (int i = 0; i < mActions.size(); i++) {
+ final RemoteAction action = mActions.get(i);
+ final ImageButton actionView = (ImageButton) mActionsGroup.getChildAt(i);
+
+ // TODO: Check if the action drawable has changed before we reload it
+ action.getIcon().loadDrawableAsync(mContext, d -> {
+ if (d != null) {
+ d.setTint(Color.WHITE);
+ actionView.setImageDrawable(d);
+ }
+ }, mHandler);
+ actionView.setContentDescription(action.getContentDescription());
+ if (action.isEnabled()) {
+ actionView.setOnClickListener(v -> {
+ mHandler.post(() -> {
+ try {
+ action.getActionIntent().send();
+ } catch (CanceledException e) {
+ Log.w(TAG, "Failed to send action", e);
+ }
+ });
+ });
+ }
+ actionView.setEnabled(action.isEnabled());
+ actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
+
+ // Update the margin between actions
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+ actionView.getLayoutParams();
+ lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0;
+ }
+ }
+
+ // Update the expand container margin to adjust the center of the expand button to
+ // account for the existence of the action container
+ FrameLayout.LayoutParams expandedLp =
+ (FrameLayout.LayoutParams) expandContainer.getLayoutParams();
+ expandedLp.topMargin = getResources().getDimensionPixelSize(
+ R.dimen.pip_action_padding);
+ expandedLp.bottomMargin = getResources().getDimensionPixelSize(
+ R.dimen.pip_expand_container_edge_margin);
+ expandContainer.requestLayout();
+ }
+ }
+
+ private void notifyMenuStateChange(int menuState, boolean resize, Runnable callback) {
+ mMenuState = menuState;
+ mController.onMenuStateChanged(menuState, resize, callback);
+ }
+
+ private void expandPip() {
+ // Do not notify menu visibility when hiding the menu, the controller will do this when it
+ // handles the message
+ hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* animate */);
+ }
+
+ private void dismissPip() {
+ // Since tapping on the close-button invokes a double-tap wait callback in PipTouchHandler,
+ // we want to disable animating the fadeout animation of the buttons in order to call on
+ // PipTouchHandler#onPipDismiss fast enough.
+ final boolean animate = mMenuState != MENU_STATE_CLOSE;
+ // Do not notify menu visibility when hiding the menu, the controller will do this when it
+ // handles the message
+ hideMenu(mController::onPipDismiss, false /* notifyMenuVisibility */, animate);
+ }
+
+ private void showSettings() {
+ final Pair<ComponentName, Integer> topPipActivityInfo =
+ PipUtils.getTopPipActivity(mContext, ActivityManager.getService());
+ if (topPipActivityInfo.first != null) {
+ 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);
+ }
+ }
+
+ private void cancelDelayedHide() {
+ mHandler.removeCallbacks(mHideMenuRunnable);
+ }
+
+ private void repostDelayedHide(int delay) {
+ int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
+ FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS);
+ mHandler.removeCallbacks(mHideMenuRunnable);
+ mHandler.postDelayed(mHideMenuRunnable, recommendedTimeout);
+ }
+}
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
new file mode 100644
index 000000000000..b5fa03082401
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -0,0 +1,662 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Choreographer;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.dynamicanimation.animation.AnimationHandler;
+import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.wm.shell.animation.FloatProperties;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipSnapAlgorithm;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
+
+/**
+ * A helper to animate and manipulate the PiP.
+ */
+public class PipMotionHelper implements PipAppOpsListener.Callback,
+ FloatingContentCoordinator.FloatingContent {
+
+ private static final String TAG = "PipMotionHelper";
+ private static final boolean DEBUG = false;
+
+ private static final int SHRINK_STACK_FROM_MENU_DURATION = 250;
+ private static final int EXPAND_STACK_TO_MENU_DURATION = 250;
+ private static final int LEAVE_PIP_DURATION = 300;
+ private static final int SHIFT_DURATION = 300;
+ private static final float STASH_RATIO = 0.25f;
+
+ /** Friction to use for PIP when it moves via physics fling animations. */
+ private static final float DEFAULT_FRICTION = 2f;
+
+ private final Context mContext;
+ private final PipTaskOrganizer mPipTaskOrganizer;
+ private final @NonNull PipBoundsState mPipBoundsState;
+
+ private PipMenuActivityController mMenuController;
+ private PipSnapAlgorithm mSnapAlgorithm;
+
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+ /** PIP's current bounds on the screen. */
+ private final Rect mBounds = new Rect();
+
+ /** The bounds within which PIP's top-left coordinate is allowed to move. */
+ private final Rect mMovementBounds = new Rect();
+
+ /** The region that all of PIP must stay within. */
+ private final Rect mFloatingAllowedArea = new Rect();
+
+ /**
+ * Temporary bounds used when PIP is being dragged or animated. These bounds are applied to PIP
+ * using {@link PipTaskOrganizer#scheduleUserResizePip}, so that we can animate shrinking into
+ * and expanding out of the magnetic dismiss target.
+ *
+ * Once PIP is done being dragged or animated, we set {@link #mBounds} equal to these temporary
+ * bounds, and call {@link PipTaskOrganizer#scheduleFinishResizePip} to 'officially' move PIP to
+ * its new bounds.
+ */
+ private final Rect mTemporaryBounds = new Rect();
+
+ /** The destination bounds to which PIP is animating. */
+ private final Rect mAnimatingToBounds = new Rect();
+
+ /** Coordinator instance for resolving conflicts with other floating content. */
+ private FloatingContentCoordinator mFloatingContentCoordinator;
+
+ private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
+ ThreadLocal.withInitial(() -> {
+ FrameCallbackScheduler scheduler = runnable ->
+ Choreographer.getSfInstance().postFrameCallback(t -> runnable.run());
+ AnimationHandler handler = new AnimationHandler(scheduler);
+ return handler;
+ });
+
+ /**
+ * PhysicsAnimator instance for animating {@link #mTemporaryBounds} using physics animations.
+ */
+ private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
+ mTemporaryBounds);
+
+ private MagnetizedObject<Rect> mMagnetizedPip;
+
+ /**
+ * Update listener that resizes the PIP to {@link #mTemporaryBounds}.
+ */
+ private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener;
+
+ /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */
+ private PhysicsAnimator.FlingConfig mFlingConfigX;
+ private PhysicsAnimator.FlingConfig mFlingConfigY;
+ /** FlingConfig instances proviced to PhysicsAnimator for stashing. */
+ private PhysicsAnimator.FlingConfig mStashConfigX;
+
+ /** SpringConfig to use for fling-then-spring animations. */
+ private final PhysicsAnimator.SpringConfig mSpringConfig =
+ new PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+
+ /** SpringConfig to use for springing PIP away from conflicting floating content. */
+ private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig =
+ new PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+
+ private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> {
+ mMainHandler.post(() -> {
+ mMenuController.updateMenuLayout(newBounds);
+ mBounds.set(newBounds);
+ });
+ };
+
+ /**
+ * Whether we're springing to the touch event location (vs. moving it to that position
+ * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was
+ * 'stuck' in the target and needs to catch up to the touch location.
+ */
+ private boolean mSpringingToTouch = false;
+
+ /**
+ * Whether PIP was released in the dismiss target, and will be animated out and dismissed
+ * shortly.
+ */
+ private boolean mDismissalPending = false;
+
+ /**
+ * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is
+ * used to show menu activity when the expand animation is completed.
+ */
+ private Runnable mPostPipTransitionCallback;
+
+ private final PipTaskOrganizer.PipTransitionCallback mPipTransitionCallback =
+ new PipTaskOrganizer.PipTransitionCallback() {
+ @Override
+ public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {}
+
+ @Override
+ public void onPipTransitionFinished(ComponentName activity, int direction) {
+ if (mPostPipTransitionCallback != null) {
+ mPostPipTransitionCallback.run();
+ mPostPipTransitionCallback = null;
+ }
+ }
+
+ @Override
+ public void onPipTransitionCanceled(ComponentName activity, int direction) {}
+ };
+
+ public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState,
+ PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController menuController,
+ PipSnapAlgorithm snapAlgorithm, FloatingContentCoordinator floatingContentCoordinator) {
+ mContext = context;
+ mPipTaskOrganizer = pipTaskOrganizer;
+ mPipBoundsState = pipBoundsState;
+ mMenuController = menuController;
+ mSnapAlgorithm = snapAlgorithm;
+ mFloatingContentCoordinator = floatingContentCoordinator;
+ mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback);
+ mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler(
+ mSfAnimationHandlerThreadLocal.get());
+
+ mResizePipUpdateListener = (target, values) -> {
+ if (!mTemporaryBounds.isEmpty()) {
+ mPipTaskOrganizer.scheduleUserResizePip(
+ mBounds, mTemporaryBounds, null);
+ }
+ };
+ }
+
+ @NonNull
+ @Override
+ public Rect getFloatingBoundsOnScreen() {
+ return !mAnimatingToBounds.isEmpty() ? mAnimatingToBounds : mBounds;
+ }
+
+ @NonNull
+ @Override
+ public Rect getAllowedFloatingBoundsRegion() {
+ return mFloatingAllowedArea;
+ }
+
+ @Override
+ public void moveToBounds(@NonNull Rect bounds) {
+ animateToBounds(bounds, mConflictResolutionSpringConfig);
+ }
+
+ /**
+ * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations.
+ */
+ void synchronizePinnedStackBounds() {
+ cancelAnimations();
+ mBounds.set(mPipBoundsState.getBounds());
+ mTemporaryBounds.setEmpty();
+
+ if (mPipTaskOrganizer.isInPip()) {
+ mFloatingContentCoordinator.onContentMoved(this);
+ }
+ }
+
+ boolean isAnimating() {
+ return mTemporaryBoundsPhysicsAnimator.isRunning();
+ }
+
+ /**
+ * Tries to move the pinned stack to the given {@param bounds}.
+ */
+ void movePip(Rect toBounds) {
+ movePip(toBounds, false /* isDragging */);
+ }
+
+ /**
+ * Tries to move the pinned stack to the given {@param bounds}.
+ *
+ * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we
+ * won't notify the floating content coordinator of this move, since that will
+ * happen when the gesture ends.
+ */
+ void movePip(Rect toBounds, boolean isDragging) {
+ if (!isDragging) {
+ mFloatingContentCoordinator.onContentMoved(this);
+ }
+
+ if (!mSpringingToTouch) {
+ // If we are moving PIP directly to the touch event locations, cancel any animations and
+ // move PIP to the given bounds.
+ cancelAnimations();
+
+ if (!isDragging) {
+ resizePipUnchecked(toBounds);
+ mBounds.set(toBounds);
+ } else {
+ mTemporaryBounds.set(toBounds);
+ mPipTaskOrganizer.scheduleUserResizePip(mBounds, mTemporaryBounds,
+ (Rect newBounds) -> {
+ mMainHandler.post(() -> {
+ mMenuController.updateMenuLayout(newBounds);
+ });
+ });
+ }
+ } else {
+ // If PIP is 'catching up' after being stuck in the dismiss target, update the animation
+ // to spring towards the new touch location.
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig)
+ .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig)
+ .spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig)
+ .spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig);
+
+ startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */,
+ false /* dismiss */);
+ }
+ }
+
+ /** Animates the PIP into the dismiss target, scaling it down. */
+ void animateIntoDismissTarget(
+ MagnetizedObject.MagneticTarget target,
+ float velX, float velY,
+ boolean flung, Function0<Unit> after) {
+ final PointF targetCenter = target.getCenterOnScreen();
+
+ final float desiredWidth = mBounds.width() / 2;
+ final float desiredHeight = mBounds.height() / 2;
+
+ final float destinationX = targetCenter.x - (desiredWidth / 2f);
+ final float destinationY = targetCenter.y - (desiredHeight / 2f);
+
+ // If we're already in the dismiss target area, then there won't be a move to set the
+ // temporary bounds, so just initialize it to the current bounds
+ if (mTemporaryBounds.isEmpty()) {
+ mTemporaryBounds.set(mBounds);
+ }
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_X, destinationX, velX, mSpringConfig)
+ .spring(FloatProperties.RECT_Y, destinationY, velY, mSpringConfig)
+ .spring(FloatProperties.RECT_WIDTH, desiredWidth, mSpringConfig)
+ .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mSpringConfig)
+ .withEndActions(after);
+
+ startBoundsAnimator(destinationX, destinationY, false);
+ }
+
+ /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */
+ void setSpringingToTouch(boolean springingToTouch) {
+ mSpringingToTouch = springingToTouch;
+ }
+
+ /**
+ * Resizes the pinned stack back to unknown windowing mode, which could be freeform or
+ * * fullscreen depending on the display area's windowing mode.
+ */
+ void expandLeavePip() {
+ expandLeavePip(false /* skipAnimation */);
+ }
+
+ /**
+ * Resizes the pinned stack back to unknown windowing mode, which could be freeform or
+ * fullscreen depending on the display area's windowing mode.
+ */
+ void expandLeavePip(boolean skipAnimation) {
+ if (DEBUG) {
+ Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation
+ + " callers=\n" + Debug.getCallers(5, " "));
+ }
+ cancelAnimations();
+ mMenuController.hideMenuWithoutResize();
+ mPipTaskOrganizer.getUpdateHandler().post(() -> {
+ mPipTaskOrganizer.exitPip(skipAnimation
+ ? 0
+ : LEAVE_PIP_DURATION);
+ });
+ }
+
+ /**
+ * Dismisses the pinned stack.
+ */
+ @Override
+ public void dismissPip() {
+ if (DEBUG) {
+ Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, " "));
+ }
+ cancelAnimations();
+ mMenuController.hideMenuWithoutResize();
+ mPipTaskOrganizer.removePip();
+ }
+
+ /** Sets the movement bounds to use to constrain PIP position animations. */
+ void setCurrentMovementBounds(Rect movementBounds) {
+ mMovementBounds.set(movementBounds);
+ rebuildFlingConfigs();
+
+ // The movement bounds represent the area within which we can move PIP's top-left position.
+ // The allowed area for all of PIP is those bounds plus PIP's width and height.
+ mFloatingAllowedArea.set(mMovementBounds);
+ mFloatingAllowedArea.right += mBounds.width();
+ mFloatingAllowedArea.bottom += mBounds.height();
+ }
+
+ /**
+ * @return the PiP bounds.
+ */
+ Rect getBounds() {
+ return mBounds;
+ }
+
+ /**
+ * Returns the PIP bounds if we're not animating, or the current, temporary animating bounds
+ * otherwise.
+ */
+ Rect getPossiblyAnimatingBounds() {
+ return mTemporaryBounds.isEmpty() ? mBounds : mTemporaryBounds;
+ }
+
+ /**
+ * Flings the PiP to the closest snap target.
+ */
+ void flingToSnapTarget(
+ float velocityX, float velocityY, @Nullable Runnable endAction) {
+ movetoTarget(velocityX, velocityY, endAction, false /* isStash */);
+ }
+
+ /**
+ * Stash PiP to the closest edge.
+ */
+ void stashToEdge(
+ float velocityX, float velocityY, @Nullable Runnable endAction) {
+ movetoTarget(velocityX, velocityY, endAction, true /* isStash */);
+ }
+
+ private void movetoTarget(
+ float velocityX, float velocityY, @Nullable Runnable endAction, boolean isStash) {
+ // If we're flinging to a snap target now, we're not springing to catch up to the touch
+ // location now.
+ mSpringingToTouch = false;
+
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig)
+ .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig)
+ .flingThenSpring(
+ FloatProperties.RECT_X, velocityX, isStash ? mStashConfigX : mFlingConfigX,
+ mSpringConfig, true /* flingMustReachMinOrMax */)
+ .flingThenSpring(
+ FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig)
+ .withEndActions(endAction);
+
+ final float offset = ((float) mBounds.width()) * (1.0f - STASH_RATIO);
+ final float leftEdge = isStash ? mMovementBounds.left - offset : mMovementBounds.left;
+ final float rightEdge = isStash ? mMovementBounds.right + offset : mMovementBounds.right;
+
+ final float xEndValue = velocityX < 0 ? leftEdge : rightEdge;
+ final float estimatedFlingYEndValue =
+ PhysicsAnimator.estimateFlingEndValue(
+ mTemporaryBounds.top, velocityY, mFlingConfigY);
+
+ startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */,
+ false /* dismiss */);
+ }
+
+ /**
+ * Animates PIP to the provided bounds, using physics animations and the given spring
+ * configuration
+ */
+ void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) {
+ if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
+ // Animate from the current bounds if we're not already animating.
+ mTemporaryBounds.set(mBounds);
+ }
+
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_X, bounds.left, springConfig)
+ .spring(FloatProperties.RECT_Y, bounds.top, springConfig);
+ startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */,
+ false /* dismiss */);
+ }
+
+ /**
+ * Animates the dismissal of the PiP off the edge of the screen.
+ */
+ void animateDismiss() {
+ // Animate off the bottom of the screen, then dismiss PIP.
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_Y,
+ mMovementBounds.bottom + mBounds.height() * 2,
+ 0,
+ mSpringConfig)
+ .withEndActions(this::dismissPip);
+
+ startBoundsAnimator(
+ mBounds.left /* toX */, mBounds.bottom + mBounds.height() /* toY */,
+ true /* dismiss */);
+
+ mDismissalPending = false;
+ }
+
+ /**
+ * Animates the PiP to the expanded state to show the menu.
+ */
+ float animateToExpandedState(Rect expandedBounds, Rect movementBounds,
+ Rect expandedMovementBounds, Runnable callback) {
+ float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), movementBounds);
+ mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction);
+ mPostPipTransitionCallback = callback;
+ resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION);
+ return savedSnapFraction;
+ }
+
+ /**
+ * Animates the PiP from the expanded state to the normal state after the menu is hidden.
+ */
+ void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction,
+ Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) {
+ if (savedSnapFraction < 0f) {
+ // If there are no saved snap fractions, then just use the current bounds
+ savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds),
+ currentMovementBounds);
+ }
+ mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction);
+
+ if (immediate) {
+ movePip(normalBounds);
+ } else {
+ resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION);
+ }
+ }
+
+ /**
+ * Animates the PiP to offset it from the IME or shelf.
+ */
+ @VisibleForTesting
+ public void animateToOffset(Rect originalBounds, int offset) {
+ if (DEBUG) {
+ Log.d(TAG, "animateToOffset: originalBounds=" + originalBounds + " offset=" + offset
+ + " callers=\n" + Debug.getCallers(5, " "));
+ }
+ cancelAnimations();
+ mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION,
+ mUpdateBoundsCallback);
+ }
+
+ /**
+ * Cancels all existing animations.
+ */
+ private void cancelAnimations() {
+ mTemporaryBoundsPhysicsAnimator.cancel();
+ mAnimatingToBounds.setEmpty();
+ mSpringingToTouch = false;
+ }
+
+ /** Set new fling configs whose min/max values respect the given movement bounds. */
+ private void rebuildFlingConfigs() {
+ mFlingConfigX = new PhysicsAnimator.FlingConfig(
+ DEFAULT_FRICTION, mMovementBounds.left, mMovementBounds.right);
+ mFlingConfigY = new PhysicsAnimator.FlingConfig(
+ DEFAULT_FRICTION, mMovementBounds.top, mMovementBounds.bottom);
+ final float offset = ((float) mBounds.width()) * (1.0f - STASH_RATIO);
+ mStashConfigX = new PhysicsAnimator.FlingConfig(
+ DEFAULT_FRICTION, mMovementBounds.left - offset, mMovementBounds.right + offset);
+ }
+
+ /**
+ * Starts the physics animator which will update the animated PIP bounds using physics
+ * animations, as well as the TimeAnimator which will apply those bounds to PIP.
+ *
+ * This will also add end actions to the bounds animator that cancel the TimeAnimator and update
+ * the 'real' bounds to equal the final animated bounds.
+ */
+ private void startBoundsAnimator(float toX, float toY, boolean dismiss) {
+ if (!mSpringingToTouch) {
+ cancelAnimations();
+ }
+
+ // Set animatingToBounds directly to avoid allocating a new Rect, but then call
+ // setAnimatingToBounds to run the normal logic for changing animatingToBounds.
+ mAnimatingToBounds.set(
+ (int) toX,
+ (int) toY,
+ (int) toX + mBounds.width(),
+ (int) toY + mBounds.height());
+ setAnimatingToBounds(mAnimatingToBounds);
+
+ if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
+ mTemporaryBoundsPhysicsAnimator
+ .addUpdateListener(mResizePipUpdateListener)
+ .withEndActions(this::onBoundsAnimationEnd);
+ }
+
+ mTemporaryBoundsPhysicsAnimator.start();
+ }
+
+ /**
+ * Notify that PIP was released in the dismiss target and will be animated out and dismissed
+ * shortly.
+ */
+ void notifyDismissalPending() {
+ mDismissalPending = true;
+ }
+
+ private void onBoundsAnimationEnd() {
+ if (!mDismissalPending
+ && !mSpringingToTouch
+ && !mMagnetizedPip.getObjectStuckToTarget()) {
+ mBounds.set(mTemporaryBounds);
+ if (!mDismissalPending) {
+ // do not schedule resize if PiP is dismissing, which may cause app re-open to
+ // mBounds instead of it's normal bounds.
+ mPipTaskOrganizer.scheduleFinishResizePip(mBounds);
+ }
+ mTemporaryBounds.setEmpty();
+ }
+
+ mAnimatingToBounds.setEmpty();
+ mSpringingToTouch = false;
+ mDismissalPending = false;
+ }
+
+ /**
+ * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so
+ * we return these bounds from
+ * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
+ */
+ private void setAnimatingToBounds(Rect bounds) {
+ mAnimatingToBounds.set(bounds);
+ mFloatingContentCoordinator.onContentMoved(this);
+ }
+
+ /**
+ * Directly resizes the PiP to the given {@param bounds}.
+ */
+ private void resizePipUnchecked(Rect toBounds) {
+ if (DEBUG) {
+ Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds
+ + " callers=\n" + Debug.getCallers(5, " "));
+ }
+ if (!toBounds.equals(mBounds)) {
+ mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback);
+ }
+ }
+
+ /**
+ * Directly resizes the PiP to the given {@param bounds}.
+ */
+ private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) {
+ if (DEBUG) {
+ Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds
+ + " duration=" + duration + " callers=\n" + Debug.getCallers(5, " "));
+ }
+
+ // Intentionally resize here even if the current bounds match the destination bounds.
+ // This is so all the proper callbacks are performed.
+ mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, mUpdateBoundsCallback);
+ setAnimatingToBounds(toBounds);
+ }
+
+ /**
+ * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the
+ * magnetic dismiss target so it can calculate PIP's size and position.
+ */
+ MagnetizedObject<Rect> getMagnetizedPip() {
+ if (mMagnetizedPip == null) {
+ mMagnetizedPip = new MagnetizedObject<Rect>(
+ mContext, mTemporaryBounds, FloatProperties.RECT_X, FloatProperties.RECT_Y) {
+ @Override
+ public float getWidth(@NonNull Rect animatedPipBounds) {
+ return animatedPipBounds.width();
+ }
+
+ @Override
+ public float getHeight(@NonNull Rect animatedPipBounds) {
+ return animatedPipBounds.height();
+ }
+
+ @Override
+ public void getLocationOnScreen(
+ @NonNull Rect animatedPipBounds, @NonNull int[] loc) {
+ loc[0] = animatedPipBounds.left;
+ loc[1] = animatedPipBounds.top;
+ }
+ };
+ }
+
+ return mMagnetizedPip;
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mBounds=" + mBounds);
+ }
+}
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
new file mode 100644
index 000000000000..ef3875597aa2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -0,0 +1,523 @@
+/*
+ * 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 static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_PINCH_RESIZE;
+import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM;
+import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT;
+import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE;
+import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT;
+import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.DeviceConfig;
+import android.view.BatchedInputEventReceiver;
+import android.view.Choreographer;
+import android.view.InputChannel;
+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;
+
+import com.android.internal.policy.TaskResizingAlgorithm;
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip.PipBoundsHandler;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+import com.android.wm.shell.pip.PipUiEventLogger;
+
+import java.io.PrintWriter;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+/**
+ * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to
+ * trigger dynamic resize.
+ */
+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 final Context mContext;
+ private final PipBoundsHandler mPipBoundsHandler;
+ private final PipMotionHelper mMotionHelper;
+ 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 Point mMaxSize = new Point();
+ private final Point mMinSize = new Point();
+ private final Rect mLastResizeBounds = new Rect();
+ private final Rect mUserResizeBounds = new Rect();
+ private final Rect mLastDownBounds = new Rect();
+ private final Rect mDragCornerSize = new Rect();
+ private final Rect mTmpTopLeftCorner = new Rect();
+ private final Rect mTmpTopRightCorner = new Rect();
+ private final Rect mTmpBottomLeftCorner = new Rect();
+ private final Rect mTmpBottomRightCorner = new Rect();
+ private final Rect mDisplayBounds = new Rect();
+ private final Function<Rect, Rect> mMovementBoundsSupplier;
+ private final Runnable mUpdateMovementBoundsRunnable;
+
+ private int mDelta;
+ private float mTouchSlop;
+ private boolean mAllowGesture;
+ private boolean mIsAttached;
+ private boolean mIsEnabled;
+ private boolean mEnablePinchResize;
+ private boolean mIsSysUiStateValid;
+ private boolean mThresholdCrossed;
+ private boolean mUsingPinchToZoom = false;
+ private float mScaleFactor = STARTING_SCALE_FACTOR;
+
+ private InputMonitor mInputMonitor;
+ private InputEventReceiver mInputEventReceiver;
+ private PipTaskOrganizer mPipTaskOrganizer;
+ private PipMenuActivityController mPipMenuActivityController;
+ private PipUiEventLogger mPipUiEventLogger;
+
+ private int mCtrlType;
+
+ public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler,
+ PipMotionHelper motionHelper, PipTaskOrganizer pipTaskOrganizer,
+ Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable,
+ PipUiEventLogger pipUiEventLogger, PipMenuActivityController menuActivityController) {
+ mContext = context;
+ mDisplayId = context.getDisplayId();
+ mMainExecutor = context.getMainExecutor();
+ mPipBoundsHandler = pipBoundsHandler;
+ mMotionHelper = motionHelper;
+ mPipTaskOrganizer = pipTaskOrganizer;
+ mMovementBoundsSupplier = movementBoundsSupplier;
+ mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
+ mPipMenuActivityController = menuActivityController;
+ mPipUiEventLogger = pipUiEventLogger;
+
+ 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,
+ /* defaultValue = */ false);
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, mMainExecutor,
+ new DeviceConfig.OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(DeviceConfig.Properties properties) {
+ if (properties.getKeyset().contains(PIP_PINCH_RESIZE)) {
+ mEnablePinchResize = properties.getBoolean(
+ PIP_PINCH_RESIZE, /* defaultValue = */ false);
+ }
+ }
+ });
+ }
+
+ public void onConfigurationChanged() {
+ reloadResources();
+ }
+
+ /**
+ * Called when SysUI state changed.
+ *
+ * @param isSysUiStateValid Is SysUI valid or not.
+ */
+ public void onSystemUiStateChanged(boolean isSysUiStateValid) {
+ mIsSysUiStateValid = isSysUiStateValid;
+ }
+
+ private void reloadResources() {
+ final Resources res = mContext.getResources();
+ mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size);
+ mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
+ }
+
+ private void resetDragCorners() {
+ mDragCornerSize.set(0, 0, mDelta, mDelta);
+ mTmpTopLeftCorner.set(mDragCornerSize);
+ mTmpTopRightCorner.set(mDragCornerSize);
+ mTmpBottomLeftCorner.set(mDragCornerSize);
+ mTmpBottomRightCorner.set(mDragCornerSize);
+ }
+
+ private void disposeInputChannel() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ }
+
+ void onActivityPinned() {
+ mIsAttached = true;
+ updateIsEnabled();
+ }
+
+ void onActivityUnpinned() {
+ mIsAttached = false;
+ mUserResizeBounds.setEmpty();
+ updateIsEnabled();
+ }
+
+ private void updateIsEnabled() {
+ boolean isEnabled = mIsAttached;
+ if (isEnabled == mIsEnabled) {
+ return;
+ }
+ mIsEnabled = isEnabled;
+ disposeInputChannel();
+
+ if (mIsEnabled) {
+ // Register input event receiver
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ "pip-resize", mDisplayId);
+ mInputEventReceiver = new SysUiInputEventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper());
+ }
+ }
+
+ private void onInputEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ if (mUsingPinchToZoom) {
+ mScaleGestureDetector.onTouchEvent((MotionEvent) ev);
+ } else {
+ onDragCornerResize((MotionEvent) ev);
+ }
+ }
+ }
+
+ /**
+ * 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
+ * overlaps with the PIP window while the rest goes outside of the PIP window.
+ * _ _ _ _
+ * |_|_|_________|_|_|
+ * |_|_| |_|_|
+ * | PIP |
+ * | WINDOW |
+ * _|_ _|_
+ * |_|_|_________|_|_|
+ * |_|_| |_|_|
+ */
+ public boolean isWithinTouchRegion(int x, int y) {
+ final Rect currentPipBounds = mMotionHelper.getBounds();
+ if (currentPipBounds == null) {
+ return false;
+ }
+ resetDragCorners();
+ mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2,
+ currentPipBounds.top - mDelta / 2);
+ mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2,
+ currentPipBounds.top - mDelta / 2);
+ mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2,
+ currentPipBounds.bottom - mDelta / 2);
+ mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2,
+ currentPipBounds.bottom - mDelta / 2);
+
+ mTmpRegion.setEmpty();
+ mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION);
+ mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION);
+ mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION);
+ mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION);
+
+ return mTmpRegion.contains(x, y);
+ }
+
+ public boolean isUsingPinchToZoom() {
+ return mEnablePinchResize;
+ }
+
+ public boolean willStartResizeGesture(MotionEvent ev) {
+ 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())) {
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mEnablePinchResize && ev.getPointerCount() == 2) {
+ mUsingPinchToZoom = true;
+ return true;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ return false;
+ }
+
+ private void setCtrlTypeForPinchToZoom() {
+ final Rect currentPipBounds = mMotionHelper.getBounds();
+ mLastDownBounds.set(mMotionHelper.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 = mMotionHelper.getBounds();
+
+ Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds);
+ mDisplayBounds.set(movementBounds.left,
+ movementBounds.top,
+ movementBounds.right + currentPipBounds.width(),
+ movementBounds.bottom + currentPipBounds.height());
+
+ if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
+ && currentPipBounds.left != mDisplayBounds.left) {
+ mCtrlType |= CTRL_LEFT;
+ mCtrlType |= CTRL_TOP;
+ }
+ if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
+ && currentPipBounds.right != mDisplayBounds.right) {
+ mCtrlType |= CTRL_RIGHT;
+ mCtrlType |= CTRL_TOP;
+ }
+ if (mTmpBottomRightCorner.contains(x, y)
+ && currentPipBounds.bottom != mDisplayBounds.bottom
+ && currentPipBounds.right != mDisplayBounds.right) {
+ mCtrlType |= CTRL_RIGHT;
+ mCtrlType |= CTRL_BOTTOM;
+ }
+ if (mTmpBottomLeftCorner.contains(x, y)
+ && currentPipBounds.bottom != mDisplayBounds.bottom
+ && currentPipBounds.left != mDisplayBounds.left) {
+ mCtrlType |= CTRL_LEFT;
+ mCtrlType |= CTRL_BOTTOM;
+ }
+ }
+
+ private boolean isInValidSysUiState() {
+ return mIsSysUiStateValid;
+ }
+
+ private void onDragCornerResize(MotionEvent ev) {
+ int action = ev.getActionMasked();
+ float x = ev.getX();
+ float y = ev.getY();
+ if (action == MotionEvent.ACTION_DOWN) {
+ final Rect currentPipBounds = mMotionHelper.getBounds();
+ mLastResizeBounds.setEmpty();
+ mAllowGesture = isInValidSysUiState() && isWithinTouchRegion((int) x, (int) y);
+ if (mAllowGesture) {
+ setCtrlType((int) x, (int) y);
+ mDownPoint.set(x, y);
+ mLastDownBounds.set(mMotionHelper.getBounds());
+ }
+ if (!currentPipBounds.contains((int) ev.getX(), (int) ev.getY())
+ && mPipMenuActivityController.isMenuVisible()) {
+ mPipMenuActivityController.hideMenu();
+ }
+
+ } else if (mAllowGesture) {
+ switch (action) {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // We do not support multi touch for resizing via drag
+ mAllowGesture = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // Capture inputs
+ if (!mThresholdCrossed
+ && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) {
+ mThresholdCrossed = true;
+ // Reset the down to begin resizing from this point
+ mDownPoint.set(x, y);
+ mInputMonitor.pilferPointers();
+ }
+ if (mThresholdCrossed) {
+ if (mPipMenuActivityController.isMenuVisible()) {
+ mPipMenuActivityController.hideMenuWithoutResize();
+ mPipMenuActivityController.hideMenu();
+ }
+ final Rect currentPipBounds = mMotionHelper.getBounds();
+ mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y,
+ mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x,
+ mMinSize.y, mMaxSize, true,
+ mLastDownBounds.width() > mLastDownBounds.height()));
+ mPipBoundsHandler.transformBoundsToAspectRatio(mLastResizeBounds);
+ mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds,
+ null);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ finishResize();
+ break;
+ }
+ }
+ }
+
+ private void finishResize() {
+ if (!mLastResizeBounds.isEmpty()) {
+ mUserResizeBounds.set(mLastResizeBounds);
+ mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
+ (Rect bounds) -> {
+ new Handler(Looper.getMainLooper()).post(() -> {
+ mMotionHelper.synchronizePinnedStackBounds();
+ mUpdateMovementBoundsRunnable.run();
+ resetState();
+ });
+ });
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE);
+ } else {
+ resetState();
+ }
+ }
+
+ private void resetState() {
+ mCtrlType = CTRL_NONE;
+ mUsingPinchToZoom = false;
+ mAllowGesture = false;
+ mThresholdCrossed = false;
+ }
+
+ void setUserResizeBounds(Rect bounds) {
+ mUserResizeBounds.set(bounds);
+ }
+
+ void invalidateUserResizeBounds() {
+ mUserResizeBounds.setEmpty();
+ }
+
+ Rect getUserResizeBounds() {
+ return mUserResizeBounds;
+ }
+
+ @VisibleForTesting public void updateMaxSize(int maxX, int maxY) {
+ mMaxSize.set(maxX, maxY);
+ }
+
+ @VisibleForTesting public void updateMinSize(int minX, int minY) {
+ mMinSize.set(minX, minY);
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture);
+ pw.println(innerPrefix + "mIsAttached=" + mIsAttached);
+ pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled);
+ pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize);
+ pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed);
+ }
+
+ class SysUiInputEventReceiver extends BatchedInputEventReceiver {
+ SysUiInputEventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper, Choreographer.getSfInstance());
+ }
+
+ public void onInputEvent(InputEvent event) {
+ PipResizeGestureHandler.this.onInputEvent(event);
+ finishInputEvent(event, true);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java
new file mode 100644
index 000000000000..1a3cc8b1c1d2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * A generic interface for a touch gesture.
+ */
+public abstract class PipTouchGesture {
+
+ /**
+ * Handle the touch down.
+ */
+ public void onDown(PipTouchState touchState) {}
+
+ /**
+ * Handle the touch move, and return whether the event was consumed.
+ */
+ public boolean onMove(PipTouchState touchState) {
+ return false;
+ }
+
+ /**
+ * Handle the touch up, and return whether the gesture was consumed.
+ */
+ public boolean onUp(PipTouchState touchState) {
+ return false;
+ }
+}
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
new file mode 100644
index 000000000000..a2233e5c5874
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -0,0 +1,893 @@
+/*
+ * 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 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 android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+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;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+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.pip.PipAnimationController;
+import com.android.wm.shell.pip.PipBoundsHandler;
+import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+import com.android.wm.shell.pip.PipUiEventLogger;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding
+ * the PIP.
+ */
+public class PipTouchHandler {
+ private static final String TAG = "PipTouchHandler";
+
+ /** Duration of the dismiss scrim fading in/out. */
+ private static final int DISMISS_TRANSITION_DURATION_MS = 200;
+
+ /* The multiplier to apply scale the target size by when applying the magnetic field radius */
+ private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f;
+
+ // Allow PIP to resize to a slightly bigger state upon touch
+ private final boolean mEnableResize;
+ private final Context mContext;
+ private final PipBoundsHandler mPipBoundsHandler;
+ private final @NonNull PipBoundsState mPipBoundsState;
+ private final PipUiEventLogger mPipUiEventLogger;
+ private final PipDismissTargetHandler mPipDismissTargetHandler;
+
+ private PipResizeGestureHandler mPipResizeGestureHandler;
+ private IPinnedStackController mPinnedStackController;
+
+ private final PipMenuActivityController mMenuController;
+ private final AccessibilityManager mAccessibilityManager;
+ private boolean mShowPipMenuOnAnimationEnd = false;
+
+ /**
+ * Whether PIP stash is enabled or not. When enabled, if at the time of fling-release the
+ * PIP bounds is outside the left/right edge of the screen, it will be shown in "stashed" mode,
+ * where PIP will only show partially.
+ */
+ private boolean mEnableStash = false;
+
+ // The current movement bounds
+ private Rect mMovementBounds = new Rect();
+
+ // The reference inset bounds, used to determine the dismiss fraction
+ private Rect mInsetBounds = new Rect();
+ // The reference bounds used to calculate the normal/expanded target bounds
+ private Rect mNormalBounds = new Rect();
+ @VisibleForTesting public Rect mNormalMovementBounds = new Rect();
+ private Rect mExpandedBounds = new Rect();
+ @VisibleForTesting public Rect mExpandedMovementBounds = new Rect();
+ private int mExpandedShortestEdgeSize;
+
+ // Used to workaround an issue where the WM rotation happens before we are notified, allowing
+ // us to send stale bounds
+ private int mDeferResizeToNormalBoundsUntilRotation = -1;
+ private int mDisplayRotation;
+
+ private Handler mHandler = new Handler();
+
+ // Behaviour states
+ private int mMenuState = MENU_STATE_NONE;
+ private boolean mIsImeShowing;
+ private int mImeHeight;
+ private int mImeOffset;
+ private boolean mIsShelfShowing;
+ private int mShelfHeight;
+ private int mMovementBoundsExtraOffsets;
+ private int mBottomOffsetBufferPx;
+ private float mSavedSnapFraction = -1f;
+ private boolean mSendingHoverAccessibilityEvents;
+ private boolean mMovementWithinDismiss;
+ private PipAccessibilityInteractionConnection mConnection;
+
+ // Touch state
+ private final PipTouchState mTouchState;
+ private final FloatingContentCoordinator mFloatingContentCoordinator;
+ private PipMotionHelper mMotionHelper;
+ private PipTouchGesture mGesture;
+
+ // Temp vars
+ private final Rect mTmpBounds = new Rect();
+
+ /**
+ * A listener for the PIP menu activity.
+ */
+ private class PipMenuListener implements PipMenuActivityController.Listener {
+ @Override
+ public void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback) {
+ setMenuState(menuState, resize, callback);
+ }
+
+ @Override
+ public void onPipExpand() {
+ mMotionHelper.expandLeavePip();
+ }
+
+ @Override
+ public void onPipDismiss() {
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE);
+ mTouchState.removeDoubleTapTimeoutCallback();
+ mMotionHelper.dismissPip();
+ }
+
+ @Override
+ public void onPipShowMenu() {
+ mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle());
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ public PipTouchHandler(Context context,
+ PipMenuActivityController menuController,
+ PipBoundsHandler pipBoundsHandler,
+ @NonNull PipBoundsState pipBoundsState,
+ PipTaskOrganizer pipTaskOrganizer,
+ FloatingContentCoordinator floatingContentCoordinator,
+ PipUiEventLogger pipUiEventLogger) {
+ // Initialize the Pip input consumer
+ mContext = context;
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ mPipBoundsHandler = pipBoundsHandler;
+ mPipBoundsState = pipBoundsState;
+ mMenuController = menuController;
+ mMenuController.addListener(new PipMenuListener());
+ mGesture = new DefaultPipTouchGesture();
+ mMotionHelper = new PipMotionHelper(mContext, pipBoundsState, pipTaskOrganizer,
+ mMenuController, mPipBoundsHandler.getSnapAlgorithm(), floatingContentCoordinator);
+ mPipResizeGestureHandler =
+ new PipResizeGestureHandler(context, pipBoundsHandler, mMotionHelper,
+ pipTaskOrganizer, this::getMovementBounds,
+ this::updateMovementBounds, pipUiEventLogger, menuController);
+ mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger,
+ mMotionHelper, mHandler);
+ mTouchState = new PipTouchState(ViewConfiguration.get(context), mHandler,
+ () -> mMenuController.showMenuWithDelay(MENU_STATE_FULL, mMotionHelper.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()),
+ menuController::hideMenu);
+
+ Resources res = context.getResources();
+ mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu);
+ reloadResources();
+
+ mFloatingContentCoordinator = floatingContentCoordinator;
+ mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState,
+ mMotionHelper, pipTaskOrganizer, mPipBoundsHandler.getSnapAlgorithm(),
+ this::onAccessibilityShowMenu, this::updateMovementBounds, mHandler);
+
+ mPipUiEventLogger = pipUiEventLogger;
+
+ mEnableStash = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ PIP_STASHING,
+ /* defaultValue = */ false);
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
+ context.getMainExecutor(),
+ properties -> {
+ if (properties.getKeyset().contains(PIP_STASHING)) {
+ mEnableStash = properties.getBoolean(
+ PIP_STASHING, /* defaultValue = */ false);
+ }
+ });
+ }
+
+ private void reloadResources() {
+ final Resources res = mContext.getResources();
+ mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer);
+ mExpandedShortestEdgeSize = res.getDimensionPixelSize(
+ R.dimen.pip_expanded_shortest_edge_size);
+ mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
+ mPipDismissTargetHandler.updateMagneticTargetSize();
+ }
+
+ private boolean shouldShowResizeHandle() {
+ return false;
+ }
+
+ public void setTouchGesture(PipTouchGesture gesture) {
+ mGesture = gesture;
+ }
+
+ public void setTouchEnabled(boolean enabled) {
+ mTouchState.setAllowTouches(enabled);
+ }
+
+ public void showPictureInPictureMenu() {
+ // Only show the menu if the user isn't currently interacting with the PiP
+ if (!mTouchState.isUserInteracting()) {
+ mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
+ false /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ }
+ }
+
+ public void onActivityPinned() {
+ mPipDismissTargetHandler.createOrUpdateDismissTarget();
+
+ mShowPipMenuOnAnimationEnd = true;
+ mPipResizeGestureHandler.onActivityPinned();
+ mFloatingContentCoordinator.onContentAdded(mMotionHelper);
+ }
+
+ public void onActivityUnpinned(ComponentName topPipActivity) {
+ if (topPipActivity == null) {
+ // Clean up state after the last PiP activity is removed
+ mPipDismissTargetHandler.cleanUpDismissTarget();
+
+ mFloatingContentCoordinator.onContentRemoved(mMotionHelper);
+ }
+ mPipResizeGestureHandler.onActivityUnpinned();
+ }
+
+ public void onPinnedStackAnimationEnded(
+ @PipAnimationController.TransitionDirection int direction) {
+ // Always synchronize the motion helper bounds once PiP animations finish
+ mMotionHelper.synchronizePinnedStackBounds();
+ updateMovementBounds();
+ if (direction == TRANSITION_DIRECTION_TO_PIP) {
+ // Set the initial bounds as the user resize bounds.
+ mPipResizeGestureHandler.setUserResizeBounds(mMotionHelper.getBounds());
+ }
+
+ if (mShowPipMenuOnAnimationEnd) {
+ mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(),
+ true /* allowMenuTimeout */, false /* willResizeMenu */,
+ shouldShowResizeHandle());
+ mShowPipMenuOnAnimationEnd = false;
+ }
+ }
+
+ public void onConfigurationChanged() {
+ mPipResizeGestureHandler.onConfigurationChanged();
+ mMotionHelper.synchronizePinnedStackBounds();
+ reloadResources();
+
+ // Recreate the dismiss target for the new orientation.
+ mPipDismissTargetHandler.createOrUpdateDismissTarget();
+ }
+
+ public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+ mIsImeShowing = imeVisible;
+ mImeHeight = imeHeight;
+ }
+
+ public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
+ mIsShelfShowing = shelfVisible;
+ mShelfHeight = shelfHeight;
+ }
+
+ /**
+ * Called when SysUI state changed.
+ *
+ * @param isSysUiStateValid Is SysUI valid or not.
+ */
+ public void onSystemUiStateChanged(boolean isSysUiStateValid) {
+ mPipResizeGestureHandler.onSystemUiStateChanged(isSysUiStateValid);
+ }
+
+ public void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) {
+ final Rect toMovementBounds = new Rect();
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(outBounds, insetBounds,
+ toMovementBounds, 0);
+ final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets;
+ if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) {
+ outBounds.offsetTo(outBounds.left, toMovementBounds.bottom);
+ }
+ }
+
+ /**
+ * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window.
+ */
+ public void onAspectRatioChanged() {
+ mPipResizeGestureHandler.invalidateUserResizeBounds();
+ }
+
+ public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds,
+ boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) {
+ // Set the user resized bounds equal to the new normal bounds in case they were
+ // invalidated (e.g. by an aspect ratio change).
+ if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) {
+ mPipResizeGestureHandler.setUserResizeBounds(normalBounds);
+ }
+
+ final int bottomOffset = mIsImeShowing ? mImeHeight : 0;
+ final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation);
+ if (fromDisplayRotationChanged) {
+ mTouchState.reset();
+ }
+
+ // Re-calculate the expanded bounds
+ mNormalBounds.set(normalBounds);
+ Rect normalMovementBounds = new Rect();
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mNormalBounds, insetBounds,
+ normalMovementBounds, bottomOffset);
+
+ if (mMovementBounds.isEmpty()) {
+ // mMovementBounds is not initialized yet and a clean movement bounds without
+ // bottom offset shall be used later in this function.
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, insetBounds,
+ mMovementBounds, 0 /* bottomOffset */);
+ }
+
+ // Calculate the expanded size
+ float aspectRatio = (float) normalBounds.width() / normalBounds.height();
+ Point displaySize = new Point();
+ mContext.getDisplay().getRealSize(displaySize);
+ Size expandedSize = mPipBoundsHandler.getSnapAlgorithm().getSizeForAspectRatio(aspectRatio,
+ mExpandedShortestEdgeSize, displaySize.x, displaySize.y);
+ mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight());
+ Rect expandedMovementBounds = new Rect();
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mExpandedBounds, insetBounds,
+ expandedMovementBounds, bottomOffset);
+
+ mPipResizeGestureHandler.updateMinSize(mNormalBounds.width(), mNormalBounds.height());
+ mPipResizeGestureHandler.updateMaxSize(mExpandedBounds.width(), mExpandedBounds.height());
+
+ // The extra offset does not really affect the movement bounds, but are applied based on the
+ // current state (ime showing, or shelf offset) when we need to actually shift
+ int extraOffset = Math.max(
+ mIsImeShowing ? mImeOffset : 0,
+ !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0);
+
+ // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not
+ // occluded by the IME or shelf.
+ if (fromImeAdjustment || fromShelfAdjustment) {
+ if (mTouchState.isUserInteracting()) {
+ // Defer the update of the current movement bounds until after the user finishes
+ // touching the screen
+ } else {
+ final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu();
+ final Rect toMovementBounds = new Rect();
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, insetBounds,
+ toMovementBounds, mIsImeShowing ? mImeHeight : 0);
+ final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets;
+ // This is to handle landscape fullscreen IMEs, don't apply the extra offset in this
+ // case
+ final int toBottom = toMovementBounds.bottom < toMovementBounds.top
+ ? toMovementBounds.bottom
+ : toMovementBounds.bottom - extraOffset;
+
+ if (isExpanded) {
+ curBounds.set(mExpandedBounds);
+ mPipBoundsHandler.getSnapAlgorithm().applySnapFraction(curBounds,
+ toMovementBounds, mSavedSnapFraction);
+ }
+
+ if (prevBottom < toBottom) {
+ // The movement bounds are expanding
+ if (curBounds.top > prevBottom - mBottomOffsetBufferPx) {
+ mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top);
+ }
+ } else if (prevBottom > toBottom) {
+ // The movement bounds are shrinking
+ if (curBounds.top > toBottom - mBottomOffsetBufferPx) {
+ mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top);
+ }
+ }
+ }
+ }
+
+ // Update the movement bounds after doing the calculations based on the old movement bounds
+ // above
+ mNormalMovementBounds.set(normalMovementBounds);
+ mExpandedMovementBounds.set(expandedMovementBounds);
+ mDisplayRotation = displayRotation;
+ mInsetBounds.set(insetBounds);
+ updateMovementBounds();
+ mMovementBoundsExtraOffsets = extraOffset;
+ mConnection.onMovementBoundsChanged(mNormalBounds, mExpandedBounds, mNormalMovementBounds,
+ mExpandedMovementBounds);
+
+ // If we have a deferred resize, apply it now
+ if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) {
+ mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
+ mNormalMovementBounds, mMovementBounds, true /* immediate */);
+ mSavedSnapFraction = -1f;
+ mDeferResizeToNormalBoundsUntilRotation = -1;
+ }
+ }
+
+ /**
+ * TODO Add appropriate description
+ */
+ public void onRegistrationChanged(boolean isRegistered) {
+ mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered
+ ? mConnection : 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
+ mPipDismissTargetHandler.cleanUpDismissTarget();
+ }
+ }
+
+ private void onAccessibilityShowMenu() {
+ mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ }
+
+ /**
+ * TODO Add appropriate description
+ */
+ public boolean handleTouchEvent(InputEvent inputEvent) {
+ // Skip any non motion events
+ 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 (mPipResizeGestureHandler.willStartResizeGesture(ev)) {
+ // Initialize the touch state for the gesture, but immediately reset to invalidate the
+ // gesture
+ mTouchState.onTouchEvent(ev);
+ mTouchState.reset();
+ 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
+ // to the touch state. Touch state needs a DOWN event in order to later process MOVE
+ // events it'll receive if the object is dragged out of the magnetic field.
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mTouchState.onTouchEvent(ev);
+ }
+
+ // Continue tracking velocity when the object is in the magnetic field, since we want to
+ // respect touch input velocity if the object is dragged out and then flung.
+ mTouchState.addMovementToVelocityTracker(ev);
+
+ return true;
+ }
+
+ // Update the touch state
+ mTouchState.onTouchEvent(ev);
+
+ boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE;
+
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ mGesture.onDown(mTouchState);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mGesture.onMove(mTouchState)) {
+ break;
+ }
+
+ shouldDeliverToMenu = !mTouchState.isDragging();
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ // Update the movement bounds again if the state has changed since the user started
+ // dragging (ie. when the IME shows)
+ updateMovementBounds();
+
+ if (mGesture.onUp(mTouchState)) {
+ break;
+ }
+
+ // Fall through to clean up
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging();
+ mTouchState.reset();
+ break;
+ }
+ case MotionEvent.ACTION_HOVER_ENTER:
+ // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably
+ // on and changing MotionEvents into HoverEvents.
+ // Let's not enable menu show/hide for a11y services.
+ if (!mAccessibilityManager.isTouchExplorationEnabled()) {
+ mTouchState.removeHoverExitTimeoutCallback();
+ mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
+ false /* allowMenuTimeout */, false /* willResizeMenu */,
+ shouldShowResizeHandle());
+ }
+ case MotionEvent.ACTION_HOVER_MOVE: {
+ if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) {
+ sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ mSendingHoverAccessibilityEvents = true;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably
+ // on and changing MotionEvents into HoverEvents.
+ // Let's not enable menu show/hide for a11y services.
+ if (!mAccessibilityManager.isTouchExplorationEnabled()) {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+ }
+ if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) {
+ sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ mSendingHoverAccessibilityEvents = false;
+ }
+ break;
+ }
+ }
+
+ // Deliver the event to PipMenuActivity to handle button click if the menu has shown.
+ if (shouldDeliverToMenu) {
+ final MotionEvent cloneEvent = MotionEvent.obtain(ev);
+ // Send the cancel event and cancel menu timeout if it starts to drag.
+ if (mTouchState.startedDragging()) {
+ cloneEvent.setAction(MotionEvent.ACTION_CANCEL);
+ mMenuController.pokeMenu();
+ }
+
+ mMenuController.handlePointerEvent(cloneEvent);
+ }
+
+ return true;
+ }
+
+ private void sendAccessibilityHoverEvent(int type) {
+ if (!mAccessibilityManager.isEnabled()) {
+ return;
+ }
+
+ AccessibilityEvent event = AccessibilityEvent.obtain(type);
+ event.setImportantForAccessibility(true);
+ event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
+ event.setWindowId(
+ AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+
+ /**
+ * 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) {
+ if (mMenuState == menuState && !resize) {
+ return;
+ }
+
+ if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) {
+ // Save the current snap fraction and if we do not drag or move the PiP, then
+ // we store back to this snap fraction. Otherwise, we'll reset the snap
+ // fraction and snap to the closest edge.
+ if (resize) {
+ animateToExpandedState(callback);
+ }
+ } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) {
+ // Try and restore the PiP to the closest edge, using the saved snap fraction
+ // if possible
+ if (resize) {
+ if (mDeferResizeToNormalBoundsUntilRotation == -1) {
+ // This is a very special case: when the menu is expanded and visible,
+ // navigating to another activity can trigger auto-enter PiP, and if the
+ // revealed activity has a forced rotation set, then the controller will get
+ // updated with the new rotation of the display. However, at the same time,
+ // SystemUI will try to hide the menu by creating an animation to the normal
+ // 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");
+ }
+ }
+
+ if (mDeferResizeToNormalBoundsUntilRotation == -1) {
+ animateToUnexpandedState(getUserResizeBounds());
+ }
+ } else {
+ mSavedSnapFraction = -1f;
+ }
+ }
+ mMenuState = menuState;
+ updateMovementBounds();
+ // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip
+ // as well, or it can't handle a11y focus and pip menu can't perform any action.
+ onRegistrationChanged(menuState == MENU_STATE_NONE);
+ if (menuState == MENU_STATE_NONE) {
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU);
+ } else if (menuState == MENU_STATE_FULL) {
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU);
+ }
+ }
+
+ private void animateToExpandedState(Runnable callback) {
+ Rect expandedBounds = new Rect(mExpandedBounds);
+ mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds,
+ mMovementBounds, mExpandedMovementBounds, callback);
+ }
+
+ private void animateToUnexpandedState(Rect restoreBounds) {
+ Rect restoredMovementBounds = new Rect();
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(restoreBounds,
+ mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0);
+ mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction,
+ restoredMovementBounds, mMovementBounds, false /* immediate */);
+ mSavedSnapFraction = -1f;
+ }
+
+ /**
+ * @return the motion helper.
+ */
+ public PipMotionHelper getMotionHelper() {
+ return mMotionHelper;
+ }
+
+ @VisibleForTesting
+ public PipResizeGestureHandler getPipResizeGestureHandler() {
+ return mPipResizeGestureHandler;
+ }
+
+ @VisibleForTesting
+ public void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) {
+ mPipResizeGestureHandler = pipResizeGestureHandler;
+ }
+
+ @VisibleForTesting
+ public void setPipMotionHelper(PipMotionHelper pipMotionHelper) {
+ mMotionHelper = pipMotionHelper;
+ }
+
+ /**
+ * @return the unexpanded bounds.
+ */
+ public Rect getNormalBounds() {
+ return mNormalBounds;
+ }
+
+ Rect getUserResizeBounds() {
+ return mPipResizeGestureHandler.getUserResizeBounds();
+ }
+
+ /**
+ * Gesture controlling normal movement of the PIP.
+ */
+ private class DefaultPipTouchGesture extends PipTouchGesture {
+ private final Point mStartPosition = new Point();
+ private final PointF mDelta = new PointF();
+ private boolean mShouldHideMenuAfterFling;
+
+ @Override
+ public void onDown(PipTouchState touchState) {
+ if (!touchState.isUserInteracting()) {
+ return;
+ }
+
+ Rect bounds = mMotionHelper.getPossiblyAnimatingBounds();
+ mDelta.set(0f, 0f);
+ mStartPosition.set(bounds.left, bounds.top);
+ mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom;
+ mMotionHelper.setSpringingToTouch(false);
+
+ // If the menu is still visible then just poke the menu
+ // so that it will timeout after the user stops touching it
+ if (mMenuState != MENU_STATE_NONE) {
+ mMenuController.pokeMenu();
+ }
+ }
+
+ @Override
+ public boolean onMove(PipTouchState touchState) {
+ if (!touchState.isUserInteracting()) {
+ return false;
+ }
+
+ if (touchState.startedDragging()) {
+ mSavedSnapFraction = -1f;
+ mPipDismissTargetHandler.showDismissTargetMaybe();
+ }
+
+ if (touchState.isDragging()) {
+ // Move the pinned stack freely
+ final PointF lastDelta = touchState.getLastTouchDelta();
+ float lastX = mStartPosition.x + mDelta.x;
+ float lastY = mStartPosition.y + mDelta.y;
+ float left = lastX + lastDelta.x;
+ float top = lastY + lastDelta.y;
+
+ // Add to the cumulative delta after bounding the position
+ mDelta.x += left - lastX;
+ mDelta.y += top - lastY;
+
+ mTmpBounds.set(mMotionHelper.getPossiblyAnimatingBounds());
+ mTmpBounds.offsetTo((int) left, (int) top);
+ mMotionHelper.movePip(mTmpBounds, true /* isDragging */);
+
+ final PointF curPos = touchState.getLastTouchPosition();
+ if (mMovementWithinDismiss) {
+ // Track if movement remains near the bottom edge to identify swipe to dismiss
+ mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onUp(PipTouchState touchState) {
+ mPipDismissTargetHandler.hideDismissTargetMaybe();
+
+ if (!touchState.isUserInteracting()) {
+ return false;
+ }
+
+ final PointF vel = touchState.getVelocity();
+
+ if (touchState.isDragging()) {
+ if (mMenuState != MENU_STATE_NONE) {
+ // If the menu is still visible, then just poke the menu so that
+ // it will timeout after the user stops touching it
+ mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ }
+ mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE;
+
+ // Reset the touch state on up before the fling settles
+ mTouchState.reset();
+ final Rect animatingBounds = mMotionHelper.getPossiblyAnimatingBounds();
+ // If User releases the PIP window while it's out of the display bounds, put
+ // PIP into stashed mode.
+ if (mEnableStash
+ && (animatingBounds.right > mPipBoundsHandler.getDisplayBounds().right
+ || animatingBounds.left < mPipBoundsHandler.getDisplayBounds().left)) {
+ mMotionHelper.stashToEdge(vel.x, vel.y, this::flingEndAction /* endAction */);
+ } else {
+ mMotionHelper.flingToSnapTarget(vel.x, vel.y,
+ this::flingEndAction /* endAction */);
+ }
+ } else if (mTouchState.isDoubleTap()) {
+ // If using pinch to zoom, double-tap functions as resizing between max/min size
+ if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
+ final boolean toExpand =
+ mMotionHelper.getBounds().width() < mExpandedBounds.width()
+ && mMotionHelper.getBounds().height() < mExpandedBounds.height();
+ mPipResizeGestureHandler.setUserResizeBounds(toExpand ? mExpandedBounds
+ : mNormalBounds);
+ if (toExpand) {
+ animateToExpandedState(null);
+ } else {
+ animateToUnexpandedState(mNormalBounds);
+ }
+ } else {
+ // Expand to fullscreen if this is a double tap
+ // the PiP should be frozen until the transition ends
+ setTouchEnabled(false);
+ mMotionHelper.expandLeavePip();
+ }
+ } else if (mMenuState != MENU_STATE_FULL) {
+ if (!mTouchState.isWaitingForDoubleTap()) {
+ // User has stalled long enough for this not to be a drag or a double tap, just
+ // expand the menu
+ mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ } else {
+ // Next touch event _may_ be the second tap for the double-tap, schedule a
+ // fallback runnable to trigger the menu if no touch event occurs before the
+ // next tap
+ mTouchState.scheduleDoubleTapTimeoutCallback();
+ }
+ }
+ return true;
+ }
+
+ private void flingEndAction() {
+ if (mShouldHideMenuAfterFling) {
+ // If the menu is not visible, then we can still be showing the activity for the
+ // dismiss overlay, so just finish it after the animation completes
+ mMenuController.hideMenu();
+ }
+ }
+ }
+
+ /**
+ * Updates the current movement bounds based on whether the menu is currently visible and
+ * resized.
+ */
+ private void updateMovementBounds() {
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mMotionHelper.getBounds(),
+ mInsetBounds, mMovementBounds, mIsImeShowing ? mImeHeight : 0);
+ mMotionHelper.setCurrentMovementBounds(mMovementBounds);
+
+ boolean isMenuExpanded = mMenuState == MENU_STATE_FULL;
+ mPipBoundsHandler.setMinEdgeSize(
+ isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize : 0);
+ }
+
+ private Rect getMovementBounds(Rect curBounds) {
+ Rect movementBounds = new Rect();
+ mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, mInsetBounds,
+ movementBounds, mIsImeShowing ? mImeHeight : 0);
+ return movementBounds;
+ }
+
+ /**
+ * @return whether the menu will resize as a part of showing the full menu.
+ */
+ private boolean willResizeMenu() {
+ if (!mEnableResize) {
+ return false;
+ }
+ return mExpandedBounds.width() != mNormalBounds.width()
+ || mExpandedBounds.height() != mNormalBounds.height();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds);
+ pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds);
+ pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds);
+ pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds);
+ pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds);
+ pw.println(innerPrefix + "mMenuState=" + mMenuState);
+ pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
+ pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
+ pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
+ pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight);
+ pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction);
+ pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets);
+ mPipBoundsHandler.dump(pw, innerPrefix);
+ mTouchState.dump(pw, innerPrefix);
+ mMotionHelper.dump(pw, innerPrefix);
+ if (mPipResizeGestureHandler != null) {
+ mPipResizeGestureHandler.dump(pw, innerPrefix);
+ }
+ }
+
+}
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
new file mode 100644
index 000000000000..217150770084
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java
@@ -0,0 +1,391 @@
+/*
+ * 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.PointF;
+import android.os.Handler;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * This keeps track of the touch state throughout the current touch gesture.
+ */
+public class PipTouchState {
+ private static final String TAG = "PipTouchState";
+ private static final boolean DEBUG = false;
+
+ @VisibleForTesting
+ public static final long DOUBLE_TAP_TIMEOUT = 200;
+ static final long HOVER_EXIT_TIMEOUT = 50;
+
+ private final Handler mHandler;
+ private final ViewConfiguration mViewConfig;
+ private final Runnable mDoubleTapTimeoutCallback;
+ private final Runnable mHoverExitTimeoutCallback;
+
+ private VelocityTracker mVelocityTracker;
+ private long mDownTouchTime = 0;
+ private long mLastDownTouchTime = 0;
+ private long mUpTouchTime = 0;
+ private final PointF mDownTouch = new PointF();
+ private final PointF mDownDelta = new PointF();
+ private final PointF mLastTouch = new PointF();
+ private final PointF mLastDelta = new PointF();
+ private final PointF mVelocity = new PointF();
+ private boolean mAllowTouches = true;
+ private boolean mIsUserInteracting = false;
+ // Set to true only if the multiple taps occur within the double tap timeout
+ private boolean mIsDoubleTap = false;
+ // Set to true only if a gesture
+ private boolean mIsWaitingForDoubleTap = false;
+ private boolean mIsDragging = false;
+ // The previous gesture was a drag
+ private boolean mPreviouslyDragging = false;
+ private boolean mStartedDragging = false;
+ private boolean mAllowDraggingOffscreen = false;
+ private int mActivePointerId;
+
+ public PipTouchState(ViewConfiguration viewConfig, Handler handler,
+ Runnable doubleTapTimeoutCallback, Runnable hoverExitTimeoutCallback) {
+ mViewConfig = viewConfig;
+ mHandler = handler;
+ mDoubleTapTimeoutCallback = doubleTapTimeoutCallback;
+ mHoverExitTimeoutCallback = hoverExitTimeoutCallback;
+ }
+
+ /**
+ * Resets this state.
+ */
+ public void reset() {
+ mAllowDraggingOffscreen = false;
+ mIsDragging = false;
+ mStartedDragging = false;
+ mIsUserInteracting = false;
+ }
+
+ /**
+ * Processes a given touch event and updates the state.
+ */
+ public void onTouchEvent(MotionEvent ev) {
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (!mAllowTouches) {
+ return;
+ }
+
+ // Initialize the velocity tracker
+ initOrResetVelocityTracker();
+ addMovementToVelocityTracker(ev);
+
+ mActivePointerId = ev.getPointerId(0);
+ if (DEBUG) {
+ Log.e(TAG, "Setting active pointer id on DOWN: " + mActivePointerId);
+ }
+ mLastTouch.set(ev.getRawX(), ev.getRawY());
+ mDownTouch.set(mLastTouch);
+ mAllowDraggingOffscreen = true;
+ mIsUserInteracting = true;
+ mDownTouchTime = ev.getEventTime();
+ mIsDoubleTap = !mPreviouslyDragging
+ && (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
+ mIsWaitingForDoubleTap = false;
+ mIsDragging = false;
+ mLastDownTouchTime = mDownTouchTime;
+ if (mDoubleTapTimeoutCallback != null) {
+ mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ // Skip event if we did not start processing this touch gesture
+ if (!mIsUserInteracting) {
+ break;
+ }
+
+ // Update the velocity tracker
+ addMovementToVelocityTracker(ev);
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId);
+ break;
+ }
+
+ float x = ev.getRawX(pointerIndex);
+ float y = ev.getRawY(pointerIndex);
+ mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y);
+ mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y);
+
+ boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop();
+ if (!mIsDragging) {
+ if (hasMovedBeyondTap) {
+ mIsDragging = true;
+ mStartedDragging = true;
+ }
+ } else {
+ mStartedDragging = false;
+ }
+ mLastTouch.set(x, y);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ // Skip event if we did not start processing this touch gesture
+ if (!mIsUserInteracting) {
+ break;
+ }
+
+ // Update the velocity tracker
+ addMovementToVelocityTracker(ev);
+
+ int pointerIndex = ev.getActionIndex();
+ int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // Select a new active pointer id and reset the movement state
+ final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (DEBUG) {
+ Log.e(TAG,
+ "Relinquish active pointer id on POINTER_UP: " + mActivePointerId);
+ }
+ mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex));
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ // Skip event if we did not start processing this touch gesture
+ if (!mIsUserInteracting) {
+ break;
+ }
+
+ // Update the velocity tracker
+ addMovementToVelocityTracker(ev);
+ mVelocityTracker.computeCurrentVelocity(1000,
+ mViewConfig.getScaledMaximumFlingVelocity());
+ mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ Log.e(TAG, "Invalid active pointer id on UP: " + mActivePointerId);
+ break;
+ }
+
+ mUpTouchTime = ev.getEventTime();
+ mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex));
+ mPreviouslyDragging = mIsDragging;
+ mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging
+ && (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT;
+
+ // Fall through to clean up
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ recycleVelocityTracker();
+ break;
+ }
+ case MotionEvent.ACTION_BUTTON_PRESS: {
+ removeHoverExitTimeoutCallback();
+ break;
+ }
+ }
+ }
+
+ /**
+ * @return the velocity of the active touch pointer at the point it is lifted off the screen.
+ */
+ public PointF getVelocity() {
+ return mVelocity;
+ }
+
+ /**
+ * @return the last touch position of the active pointer.
+ */
+ public PointF getLastTouchPosition() {
+ return mLastTouch;
+ }
+
+ /**
+ * @return the movement delta between the last handled touch event and the previous touch
+ * position.
+ */
+ public PointF getLastTouchDelta() {
+ return mLastDelta;
+ }
+
+ /**
+ * @return the down touch position.
+ */
+ public PointF getDownTouchPosition() {
+ return mDownTouch;
+ }
+
+ /**
+ * @return the movement delta between the last handled touch event and the down touch
+ * position.
+ */
+ public PointF getDownTouchDelta() {
+ return mDownDelta;
+ }
+
+ /**
+ * @return whether the user has started dragging.
+ */
+ public boolean isDragging() {
+ return mIsDragging;
+ }
+
+ /**
+ * @return whether the user is currently interacting with the PiP.
+ */
+ public boolean isUserInteracting() {
+ return mIsUserInteracting;
+ }
+
+ /**
+ * @return whether the user has started dragging just in the last handled touch event.
+ */
+ public boolean startedDragging() {
+ return mStartedDragging;
+ }
+
+ /**
+ * Sets whether touching is currently allowed.
+ */
+ public void setAllowTouches(boolean allowTouches) {
+ mAllowTouches = allowTouches;
+
+ // If the user happens to touch down before this is sent from the system during a transition
+ // then block any additional handling by resetting the state now
+ if (mIsUserInteracting) {
+ reset();
+ }
+ }
+
+ /**
+ * Disallows dragging offscreen for the duration of the current gesture.
+ */
+ public void setDisallowDraggingOffscreen() {
+ mAllowDraggingOffscreen = false;
+ }
+
+ /**
+ * @return whether dragging offscreen is allowed during this gesture.
+ */
+ public boolean allowDraggingOffscreen() {
+ return mAllowDraggingOffscreen;
+ }
+
+ /**
+ * @return whether this gesture is a double-tap.
+ */
+ public boolean isDoubleTap() {
+ return mIsDoubleTap;
+ }
+
+ /**
+ * @return whether this gesture will potentially lead to a following double-tap.
+ */
+ public boolean isWaitingForDoubleTap() {
+ return mIsWaitingForDoubleTap;
+ }
+
+ /**
+ * Schedules the callback to run if the next double tap does not occur. Only runs if
+ * isWaitingForDoubleTap() is true.
+ */
+ public void scheduleDoubleTapTimeoutCallback() {
+ if (mIsWaitingForDoubleTap) {
+ long delay = getDoubleTapTimeoutCallbackDelay();
+ mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
+ mHandler.postDelayed(mDoubleTapTimeoutCallback, delay);
+ }
+ }
+
+ @VisibleForTesting
+ public long getDoubleTapTimeoutCallbackDelay() {
+ if (mIsWaitingForDoubleTap) {
+ return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime));
+ }
+ return -1;
+ }
+
+ /**
+ * Removes the timeout callback if it's in queue.
+ */
+ public void removeDoubleTapTimeoutCallback() {
+ mIsWaitingForDoubleTap = false;
+ mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
+ }
+
+ @VisibleForTesting
+ public void scheduleHoverExitTimeoutCallback() {
+ mHandler.removeCallbacks(mHoverExitTimeoutCallback);
+ mHandler.postDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT);
+ }
+
+ void removeHoverExitTimeoutCallback() {
+ mHandler.removeCallbacks(mHoverExitTimeoutCallback);
+ }
+
+ void addMovementToVelocityTracker(MotionEvent event) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ // Add movement to velocity tracker using raw screen X and Y coordinates instead
+ // of window coordinates because the window frame may be moving at the same time.
+ float deltaX = event.getRawX() - event.getX();
+ float deltaY = event.getRawY() - event.getY();
+ event.offsetLocation(deltaX, deltaY);
+ mVelocityTracker.addMovement(event);
+ event.offsetLocation(-deltaX, -deltaY);
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches);
+ pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId);
+ pw.println(innerPrefix + "mDownTouch=" + mDownTouch);
+ pw.println(innerPrefix + "mDownDelta=" + mDownDelta);
+ pw.println(innerPrefix + "mLastTouch=" + mLastTouch);
+ pw.println(innerPrefix + "mLastDelta=" + mLastDelta);
+ pw.println(innerPrefix + "mVelocity=" + mVelocity);
+ pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting);
+ pw.println(innerPrefix + "mIsDragging=" + mIsDragging);
+ pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging);
+ pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUpdateThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUpdateThread.java
new file mode 100644
index 000000000000..d686cac3457b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUpdateThread.java
@@ -0,0 +1,60 @@
+/*
+ * 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.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * Similar to {@link com.android.internal.os.BackgroundThread}, this is a shared singleton
+ * foreground thread for each process for updating PIP.
+ */
+public final class PipUpdateThread extends HandlerThread {
+ private static PipUpdateThread sInstance;
+ private static Handler sHandler;
+
+ private PipUpdateThread() {
+ super("pip");
+ }
+
+ private static void ensureThreadLocked() {
+ if (sInstance == null) {
+ sInstance = new PipUpdateThread();
+ sInstance.start();
+ sHandler = new Handler(sInstance.getLooper());
+ }
+ }
+
+ /**
+ * @return the static update thread instance
+ */
+ public static PipUpdateThread get() {
+ synchronized (PipUpdateThread.class) {
+ ensureThreadLocked();
+ return sInstance;
+ }
+ }
+ /**
+ * @return the static update thread handler instance
+ */
+ public static Handler getHandler() {
+ synchronized (PipUpdateThread.class) {
+ ensureThreadLocked();
+ return sHandler;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUtils.java
new file mode 100644
index 000000000000..bd2ba32912bc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUtils.java
@@ -0,0 +1,59 @@
+/*
+ * 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 static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
+import android.app.ActivityTaskManager;
+import android.app.ActivityTaskManager.RootTaskInfo;
+import android.app.IActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+public class PipUtils {
+ private static final String TAG = "PipUtils";
+
+ /**
+ * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack.
+ * The component name may be null if no such activity exists.
+ */
+ public static Pair<ComponentName, Integer> getTopPipActivity(Context context,
+ IActivityManager activityManager) {
+ try {
+ final String sysUiPackageName = context.getPackageName();
+ final RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
+ if (pinnedTaskInfo != null && pinnedTaskInfo.childTaskIds != null
+ && pinnedTaskInfo.childTaskIds.length > 0) {
+ for (int i = pinnedTaskInfo.childTaskNames.length - 1; i >= 0; i--) {
+ ComponentName cn = ComponentName.unflattenFromString(
+ pinnedTaskInfo.childTaskNames[i]);
+ if (cn != null && !cn.getPackageName().equals(sysUiPackageName)) {
+ return new Pair<>(cn, pinnedTaskInfo.childTaskUserIds[i]);
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to get pinned stack.");
+ }
+ return new Pair<>(null, 0);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java
new file mode 100644
index 000000000000..4e82bb557fb9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+
+/**
+ * A view containing PIP controls including fullscreen, close, and media controls.
+ */
+public class PipControlButtonView extends RelativeLayout {
+
+ private OnFocusChangeListener mFocusChangeListener;
+ private ImageView mIconImageView;
+ ImageView mButtonImageView;
+ private TextView mDescriptionTextView;
+ private Animator mTextFocusGainAnimator;
+ private Animator mButtonFocusGainAnimator;
+ private Animator mTextFocusLossAnimator;
+ private Animator mButtonFocusLossAnimator;
+
+ private final OnFocusChangeListener mInternalFocusChangeListener =
+ new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ startFocusGainAnimation();
+ } else {
+ startFocusLossAnimation();
+ }
+
+ if (mFocusChangeListener != null) {
+ mFocusChangeListener.onFocusChange(PipControlButtonView.this, hasFocus);
+ }
+ }
+ };
+
+ public PipControlButtonView(Context context) {
+ this(context, null, 0, 0);
+ }
+
+ public PipControlButtonView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0, 0);
+ }
+
+ public PipControlButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public PipControlButtonView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.tv_pip_control_button, this);
+
+ mIconImageView = findViewById(R.id.icon);
+ mButtonImageView = findViewById(R.id.button);
+ mDescriptionTextView = findViewById(R.id.desc);
+
+ int[] values = new int[]{android.R.attr.src, android.R.attr.text};
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr,
+ defStyleRes);
+
+ setImageResource(typedArray.getResourceId(0, 0));
+ setText(typedArray.getResourceId(1, 0));
+
+ typedArray.recycle();
+ }
+
+ @Override
+ public void onFinishInflate() {
+ super.onFinishInflate();
+ mButtonImageView.setOnFocusChangeListener(mInternalFocusChangeListener);
+
+ mTextFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(),
+ R.anim.tv_pip_controls_focus_gain_animation);
+ mTextFocusGainAnimator.setTarget(mDescriptionTextView);
+ mButtonFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(),
+ R.anim.tv_pip_controls_focus_gain_animation);
+ mButtonFocusGainAnimator.setTarget(mButtonImageView);
+
+ mTextFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(),
+ R.anim.tv_pip_controls_focus_loss_animation);
+ mTextFocusLossAnimator.setTarget(mDescriptionTextView);
+ mButtonFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(),
+ R.anim.tv_pip_controls_focus_loss_animation);
+ mButtonFocusLossAnimator.setTarget(mButtonImageView);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener listener) {
+ mButtonImageView.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setOnFocusChangeListener(OnFocusChangeListener listener) {
+ mFocusChangeListener = listener;
+ }
+
+ /**
+ * Sets the drawable for the button with the given drawable.
+ */
+ public void setImageDrawable(Drawable d) {
+ mIconImageView.setImageDrawable(d);
+ }
+
+ /**
+ * Sets the drawable for the button with the given resource id.
+ */
+ public void setImageResource(int resId) {
+ if (resId != 0) {
+ mIconImageView.setImageResource(resId);
+ }
+ }
+
+ /**
+ * Sets the text for description the with the given string.
+ */
+ public void setText(CharSequence text) {
+ mButtonImageView.setContentDescription(text);
+ mDescriptionTextView.setText(text);
+ }
+
+ /**
+ * Sets the text for description the with the given resource id.
+ */
+ public void setText(int resId) {
+ if (resId != 0) {
+ mButtonImageView.setContentDescription(getContext().getString(resId));
+ mDescriptionTextView.setText(resId);
+ }
+ }
+
+ private static void cancelAnimator(Animator animator) {
+ if (animator.isStarted()) {
+ animator.cancel();
+ }
+ }
+
+ /**
+ * Starts the focus gain animation.
+ */
+ public void startFocusGainAnimation() {
+ cancelAnimator(mButtonFocusLossAnimator);
+ cancelAnimator(mTextFocusLossAnimator);
+ mTextFocusGainAnimator.start();
+ if (mButtonImageView.getAlpha() < 1f) {
+ // If we had faded out the ripple drawable, run our manual focus change animation.
+ // See the comment at {@link #startFocusLossAnimation()} for the reason of manual
+ // animator.
+ mButtonFocusGainAnimator.start();
+ }
+ }
+
+ /**
+ * Starts the focus loss animation.
+ */
+ public void startFocusLossAnimation() {
+ cancelAnimator(mButtonFocusGainAnimator);
+ cancelAnimator(mTextFocusGainAnimator);
+ mTextFocusLossAnimator.start();
+ if (mButtonImageView.hasFocus()) {
+ // Button uses ripple that has the default animation for the focus changes.
+ // Howevever, it doesn't expose the API to fade out while it is focused,
+ // so we should manually run the fade out animation when PIP controls row loses focus.
+ mButtonFocusLossAnimator.start();
+ }
+ }
+
+ /**
+ * Resets to initial state.
+ */
+ public void reset() {
+ cancelAnimator(mButtonFocusGainAnimator);
+ cancelAnimator(mTextFocusGainAnimator);
+ cancelAnimator(mButtonFocusLossAnimator);
+ cancelAnimator(mTextFocusLossAnimator);
+ mButtonImageView.setAlpha(1f);
+ mDescriptionTextView.setAlpha(mButtonImageView.hasFocus() ? 1f : 0f);
+ }
+}
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
new file mode 100644
index 000000000000..4f2d4e50f76d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java
@@ -0,0 +1,750 @@
+/*
+ * 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.app.ActivityTaskManager.INVALID_STACK_ID;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.content.Intent.ACTION_MEDIA_RESOURCE_GRANTED;
+
+import static com.android.wm.shell.pip.tv.PipNotification.ACTION_CLOSE;
+import static com.android.wm.shell.pip.tv.PipNotification.ACTION_MENU;
+
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.ActivityTaskManager.RootTaskInfo;
+import android.app.IActivityTaskManager;
+import android.app.RemoteAction;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+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.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.DisplayInfo;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.WindowManagerShellWrapper;
+import com.android.wm.shell.pip.PinnedStackListenerForwarder;
+import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.pip.PipBoundsHandler;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 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);
+
+ /**
+ * Unknown or invalid state
+ */
+ public static final int STATE_UNKNOWN = -1;
+ /**
+ * State when there's no PIP.
+ */
+ public static final int STATE_NO_PIP = 0;
+ /**
+ * State when PIP is shown. This is used as default PIP state.
+ */
+ public static final int STATE_PIP = 1;
+ /**
+ * State when PIP menu dialog is shown.
+ */
+ public static final int STATE_PIP_MENU = 2;
+
+ private static final int TASK_ID_NO_PIP = -1;
+ private static final int INVALID_RESOURCE_TYPE = -1;
+
+ public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1;
+
+ /**
+ * PIPed activity is playing a media and it can be paused.
+ */
+ static final int PLAYBACK_STATE_PLAYING = 0;
+ /**
+ * PIPed activity has a paused media and it can be played.
+ */
+ static final int PLAYBACK_STATE_PAUSED = 1;
+ /**
+ * Users are unable to control PIPed activity's media playback.
+ */
+ static final int PLAYBACK_STATE_UNAVAILABLE = 2;
+
+ private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000;
+
+ private int mSuspendPipResizingReason;
+
+ private Context mContext;
+ private PipBoundsHandler mPipBoundsHandler;
+ private PipTaskOrganizer mPipTaskOrganizer;
+ private IActivityTaskManager mActivityTaskManager;
+ private MediaSessionManager mMediaSessionManager;
+ 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 List<MediaListener> mMediaListeners = new ArrayList<>();
+ private Rect mPipBounds;
+ private Rect mDefaultPipBounds = new Rect();
+ private Rect mMenuModePipBounds;
+ private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
+ private boolean mInitialized;
+ private int mPipTaskId = TASK_ID_NO_PIP;
+ private int mPinnedStackId = INVALID_STACK_ID;
+ private ComponentName mPipComponentName;
+ private MediaController mPipMediaController;
+ private String[] mLastPackagesResourceGranted;
+ private PipNotification mPipNotification;
+ private ParceledListSlice<RemoteAction> mCustomActions;
+ private WindowManagerShellWrapper mWindowManagerShellWrapper;
+ private int mResizeAnimationDuration;
+
+ // Used to calculate the movement bounds
+ private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
+ private final Rect mTmpInsetBounds = new Rect();
+
+ // Keeps track of the IME visibility to adjust the PiP when the IME is visible
+ private boolean mImeVisible;
+ private int mImeHeightAdjustment;
+
+ private final Runnable mResizePinnedStackRunnable =
+ () -> resizePinnedStack(mResumeResizePinnedStackRunnableState);
+ private final Runnable mClosePipRunnable = () -> closePip();
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Log.d(TAG, "mBroadcastReceiver, action: " + intent.getAction());
+ }
+ switch (intent.getAction()) {
+ case ACTION_MENU:
+ showPictureInPictureMenu();
+ break;
+ case ACTION_CLOSE:
+ closePip();
+ break;
+ case ACTION_MEDIA_RESOURCE_GRANTED:
+ String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
+ int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE,
+ INVALID_RESOURCE_TYPE);
+ if (packageNames != null && packageNames.length > 0
+ && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) {
+ handleMediaResourceGranted(packageNames);
+ }
+ break;
+ }
+ }
+ };
+ private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener =
+ controllers -> updateMediaController(controllers);
+ private final PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener =
+ new PipControllerPinnedStackListener();
+
+ @Override
+ public void registerSessionListenerForCurrentUser() {
+ // TODO Need confirm if TV have to re-registers when switch user
+ mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener);
+ mMediaSessionManager.addOnActiveSessionsChangedListener(mActiveMediaSessionListener, null,
+ UserHandle.USER_CURRENT, null);
+ }
+
+ /**
+ * Handler for messages from the PIP controller.
+ */
+ private class PipControllerPinnedStackListener extends
+ PinnedStackListenerForwarder.PinnedStackListener {
+ @Override
+ public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+ mHandler.post(() -> {
+ mPipBoundsHandler.onImeVisibilityChanged(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);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onMovementBoundsChanged(boolean fromImeAdjustment) {
+ mHandler.post(() -> {
+ // Populate the inset / normal bounds and DisplayInfo from mPipBoundsHandler first.
+ mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mPipBounds,
+ mDefaultPipBounds, mTmpDisplayInfo);
+ });
+ }
+
+ @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);
+ }
+ });
+ }
+ }
+
+ public PipController(Context context,
+ PipBoundsHandler pipBoundsHandler,
+ PipTaskOrganizer pipTaskOrganizer,
+ WindowManagerShellWrapper windowManagerShellWrapper
+ ) {
+ if (!mInitialized) {
+ mInitialized = true;
+ mContext = context;
+ mPipNotification = new PipNotification(context, this);
+ mPipBoundsHandler = pipBoundsHandler;
+ // 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();
+ context.getDisplay().getDisplayInfo(displayInfo);
+ mPipBoundsHandler.onDisplayInfoChanged(displayInfo);
+
+ mResizeAnimationDuration = context.getResources()
+ .getInteger(R.integer.config_pipResizeAnimationDuration);
+ mPipTaskOrganizer = pipTaskOrganizer;
+ mPipTaskOrganizer.registerPipTransitionCallback(this);
+ mActivityTaskManager = ActivityTaskManager.getService();
+
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_CLOSE);
+ intentFilter.addAction(ACTION_MENU);
+ intentFilter.addAction(ACTION_MEDIA_RESOURCE_GRANTED);
+ mContext.registerReceiver(mBroadcastReceiver, intentFilter, UserHandle.USER_ALL);
+
+ // Initialize the last orientation and apply the current configuration
+ Configuration initialConfig = mContext.getResources().getConfiguration();
+ mLastOrientation = initialConfig.orientation;
+ loadConfigurationsAndApply(initialConfig);
+
+ mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
+ mWindowManagerShellWrapper = windowManagerShellWrapper;
+ try {
+ mWindowManagerShellWrapper.addPinnedStackListener(mPinnedStackListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register pinned stack listener", e);
+ }
+
+ // TODO(b/169395392) Refactor PipMenuActivity to PipMenuView
+ PipMenuActivity.setPipController(this);
+ }
+ }
+
+ private void loadConfigurationsAndApply(Configuration newConfig) {
+ if (mLastOrientation != newConfig.orientation) {
+ // Don't resize the pinned stack on orientation change. TV does not care about this case
+ // and this could clobber the existing animation to the new bounds calculated by WM.
+ mLastOrientation = newConfig.orientation;
+ return;
+ }
+
+ Resources res = mContext.getResources();
+ mMenuModePipBounds = Rect.unflattenFromString(res.getString(
+ R.string.pip_menu_bounds));
+
+ // Reset the PIP bounds and apply. PIP bounds can be changed by two reasons.
+ // 1. Configuration changed due to the language change (RTL <-> RTL)
+ // 2. SystemUI restarts after the crash
+ mPipBounds = mDefaultPipBounds;
+ resizePinnedStack(getPinnedTaskInfo() == null ? STATE_NO_PIP : STATE_PIP);
+ }
+
+ /**
+ * Updates the PIP per configuration changed.
+ */
+ public void onConfigurationChanged(Configuration newConfig) {
+ loadConfigurationsAndApply(newConfig);
+ mPipNotification.onConfigurationChanged(mContext);
+ }
+
+ /**
+ * Shows the picture-in-picture menu if an activity is in picture-in-picture mode.
+ */
+ public void showPictureInPictureMenu() {
+ if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), current state=" + getStateDescription());
+
+ if (getState() == STATE_PIP) {
+ resizePinnedStack(STATE_PIP_MENU);
+ }
+ }
+
+ /**
+ * Closes PIP (PIPed activity and PIP system UI).
+ */
+ public void closePip() {
+ if (DEBUG) Log.d(TAG, "closePip(), current state=" + getStateDescription());
+
+ closePipInternal(true);
+ }
+
+ private void closePipInternal(boolean removePipStack) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "closePipInternal() removePipStack=" + removePipStack + ", current state="
+ + getStateDescription());
+ }
+
+ mState = STATE_NO_PIP;
+ mPipTaskId = TASK_ID_NO_PIP;
+ mPipMediaController = null;
+ mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener);
+ if (removePipStack) {
+ try {
+ mActivityTaskManager.removeTask(mPinnedStackId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "removeTask failed", e);
+ } finally {
+ mPinnedStackId = INVALID_STACK_ID;
+ }
+ }
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onPipActivityClosed();
+ }
+ mHandler.removeCallbacks(mClosePipRunnable);
+ }
+
+ /**
+ * Moves the PIPed activity to the fullscreen and closes PIP system UI.
+ */
+ public void movePipToFullscreen() {
+ if (DEBUG) Log.d(TAG, "movePipToFullscreen(), current state=" + getStateDescription());
+
+ mPipTaskId = TASK_ID_NO_PIP;
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onMoveToFullscreen();
+ }
+ resizePinnedStack(STATE_NO_PIP);
+ }
+
+ @Override
+ public void onActivityPinned(String packageName) {
+ if (DEBUG) Log.d(TAG, "onActivityPinned()");
+
+ RootTaskInfo taskInfo = getPinnedTaskInfo();
+ if (taskInfo == null) {
+ Log.w(TAG, "Cannot find pinned stack");
+ return;
+ }
+ if (DEBUG) Log.d(TAG, "PINNED_STACK:" + taskInfo);
+ mPinnedStackId = taskInfo.taskId;
+ mPipTaskId = taskInfo.childTaskIds[taskInfo.childTaskIds.length - 1];
+ mPipComponentName = ComponentName.unflattenFromString(
+ taskInfo.childTaskNames[taskInfo.childTaskNames.length - 1]);
+ // Set state to STATE_PIP so we show it when the pinned stack animation ends.
+ mState = STATE_PIP;
+ mMediaSessionManager.addOnActiveSessionsChangedListener(
+ mActiveMediaSessionListener, null);
+ updateMediaController(mMediaSessionManager.getActiveSessions(null));
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ mListeners.get(i).onPipEntered(packageName);
+ }
+ }
+
+ @Override
+ public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+ boolean clearedTask) {
+ if (task.configuration.windowConfiguration.getWindowingMode()
+ != WINDOWING_MODE_PINNED) {
+ return;
+ }
+ if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()");
+
+ // If PIPed activity is launched again by Launcher or intent, make it fullscreen.
+ movePipToFullscreen();
+ }
+
+ @Override
+ public void onTaskStackChanged() {
+ if (DEBUG) Log.d(TAG, "onTaskStackChanged()");
+
+ if (getState() != STATE_NO_PIP) {
+ boolean hasPip = false;
+
+ RootTaskInfo taskInfo = getPinnedTaskInfo();
+ if (taskInfo == null || taskInfo.childTaskIds == null) {
+ Log.w(TAG, "There is nothing in pinned stack");
+ closePipInternal(false);
+ return;
+ }
+ for (int i = taskInfo.childTaskIds.length - 1; i >= 0; --i) {
+ if (taskInfo.childTaskIds[i] == mPipTaskId) {
+ // PIP task is still alive.
+ hasPip = true;
+ break;
+ }
+ }
+ if (!hasPip) {
+ // PIP task doesn't exist anymore in PINNED_STACK.
+ closePipInternal(true);
+ return;
+ }
+ }
+ if (getState() == STATE_PIP) {
+ if (mPipBounds != mDefaultPipBounds) {
+ mPipBounds = mDefaultPipBounds;
+ resizePinnedStack(STATE_PIP);
+ }
+ }
+ }
+
+ /**
+ * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called
+ *
+ * @param reason The reason for suspending resizing operations on the Pip.
+ */
+ public void suspendPipResizing(int reason) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
+ }
+
+ mSuspendPipResizingReason |= reason;
+ }
+
+ /**
+ * Resumes resizing operation on the Pip that was previously suspended.
+ *
+ * @param reason The reason resizing operations on the Pip was suspended.
+ */
+ public void resumePipResizing(int reason) {
+ if ((mSuspendPipResizingReason & reason) == 0) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG,
+ "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
+ }
+ mSuspendPipResizingReason &= ~reason;
+ mHandler.post(mResizePinnedStackRunnable);
+ }
+
+ /**
+ * Resize the Pip to the appropriate size for the input state.
+ *
+ * @param state In Pip state also used to determine the new size for the Pip.
+ */
+ public void resizePinnedStack(int state) {
+ if (DEBUG) {
+ Log.d(TAG, "resizePinnedStack() state=" + stateToName(state) + ", current state="
+ + getStateDescription(), new Exception());
+ }
+
+ boolean wasStateNoPip = (mState == STATE_NO_PIP);
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onPipResizeAboutToStart();
+ }
+ if (mSuspendPipResizingReason != 0) {
+ mResumeResizePinnedStackRunnableState = state;
+ if (DEBUG) {
+ Log.d(TAG, "resizePinnedStack() deferring"
+ + " mSuspendPipResizingReason=" + mSuspendPipResizingReason
+ + " mResumeResizePinnedStackRunnableState="
+ + stateToName(mResumeResizePinnedStackRunnableState));
+ }
+ return;
+ }
+ mState = state;
+ final Rect newBounds;
+ switch (mState) {
+ case STATE_NO_PIP:
+ newBounds = null;
+ // If the state was already STATE_NO_PIP, then do not resize the stack below as it
+ // will not exist
+ if (wasStateNoPip) {
+ return;
+ }
+ break;
+ case STATE_PIP_MENU:
+ newBounds = mMenuModePipBounds;
+ break;
+ case STATE_PIP: // fallthrough
+ default:
+ newBounds = mPipBounds;
+ break;
+ }
+ if (newBounds != null) {
+ mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null);
+ } else {
+ mPipTaskOrganizer.exitPip(mResizeAnimationDuration);
+ }
+ }
+
+ /**
+ * @return the current state, or the pending state if the state change was previously suspended.
+ */
+ private int getState() {
+ if (mSuspendPipResizingReason != 0) {
+ return mResumeResizePinnedStackRunnableState;
+ }
+ return mState;
+ }
+
+ /**
+ * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned
+ * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}.
+ */
+ private void showPipMenu() {
+ if (DEBUG) Log.d(TAG, "showPipMenu(), current state=" + getStateDescription());
+
+ mState = STATE_PIP_MENU;
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onShowPipMenu();
+ }
+ Intent intent = new Intent(mContext, PipMenuActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(PipMenuActivity.EXTRA_CUSTOM_ACTIONS, mCustomActions);
+ mContext.startActivity(intent);
+ }
+
+ /**
+ * Adds a {@link Listener} to PipController.
+ */
+ public void addListener(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link Listener} from PipController.
+ */
+ public void removeListener(Listener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Adds a {@link MediaListener} to PipController.
+ */
+ public void addMediaListener(MediaListener listener) {
+ mMediaListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link MediaListener} from PipController.
+ */
+ public void removeMediaListener(MediaListener listener) {
+ mMediaListeners.remove(listener);
+ }
+
+ /**
+ * Returns {@code true} if PIP is shown.
+ */
+ public boolean isPipShown() {
+ return mState != STATE_NO_PIP;
+ }
+
+ private RootTaskInfo getPinnedTaskInfo() {
+ RootTaskInfo taskInfo = null;
+ try {
+ taskInfo = ActivityTaskManager.getService().getRootTaskInfo(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
+ } catch (RemoteException e) {
+ Log.e(TAG, "getRootTaskInfo failed", e);
+ }
+ if (DEBUG) Log.d(TAG, "getPinnedTaskInfo(), taskInfo=" + taskInfo);
+ return taskInfo;
+ }
+
+ private void handleMediaResourceGranted(String[] packageNames) {
+ if (getState() == STATE_NO_PIP) {
+ mLastPackagesResourceGranted = packageNames;
+ } else {
+ boolean requestedFromLastPackages = false;
+ if (mLastPackagesResourceGranted != null) {
+ for (String packageName : mLastPackagesResourceGranted) {
+ for (String newPackageName : packageNames) {
+ if (TextUtils.equals(newPackageName, packageName)) {
+ requestedFromLastPackages = true;
+ break;
+ }
+ }
+ }
+ }
+ mLastPackagesResourceGranted = packageNames;
+ if (!requestedFromLastPackages) {
+ closePip();
+ }
+ }
+ }
+
+ private void updateMediaController(List<MediaController> controllers) {
+ MediaController mediaController = null;
+ if (controllers != null && getState() != STATE_NO_PIP && mPipComponentName != null) {
+ for (int i = controllers.size() - 1; i >= 0; i--) {
+ MediaController controller = controllers.get(i);
+ // We assumes that an app with PIPable activity
+ // keeps the single instance of media controller especially when PIP is on.
+ if (controller.getPackageName().equals(mPipComponentName.getPackageName())) {
+ mediaController = controller;
+ break;
+ }
+ }
+ }
+ if (mPipMediaController != mediaController) {
+ mPipMediaController = mediaController;
+ for (int i = mMediaListeners.size() - 1; i >= 0; i--) {
+ mMediaListeners.get(i).onMediaControllerChanged();
+ }
+ if (mPipMediaController == null) {
+ mHandler.postDelayed(mClosePipRunnable,
+ CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS);
+ } else {
+ mHandler.removeCallbacks(mClosePipRunnable);
+ }
+ }
+ }
+
+ /**
+ * Gets the {@link android.media.session.MediaController} for the PIPed activity.
+ */
+ public MediaController getMediaController() {
+ return mPipMediaController;
+ }
+
+ @Override
+ public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {
+
+ }
+
+ /**
+ * Returns the PIPed activity's playback state.
+ * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED},
+ * or {@link #PLAYBACK_STATE_UNAVAILABLE}.
+ */
+ public int getPlaybackState() {
+ if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) {
+ return PLAYBACK_STATE_UNAVAILABLE;
+ }
+ int state = mPipMediaController.getPlaybackState().getState();
+ boolean isPlaying = (state == PlaybackState.STATE_BUFFERING
+ || state == PlaybackState.STATE_CONNECTING
+ || state == PlaybackState.STATE_PLAYING
+ || state == PlaybackState.STATE_FAST_FORWARDING
+ || state == PlaybackState.STATE_REWINDING
+ || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
+ || state == PlaybackState.STATE_SKIPPING_TO_NEXT);
+ long actions = mPipMediaController.getPlaybackState().getActions();
+ if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
+ return PLAYBACK_STATE_PAUSED;
+ } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
+ return PLAYBACK_STATE_PLAYING;
+ }
+ return PLAYBACK_STATE_UNAVAILABLE;
+ }
+
+ @Override
+ public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {
+ }
+
+ @Override
+ public void onPipTransitionFinished(ComponentName activity, int direction) {
+ onPipTransitionFinishedOrCanceled();
+ }
+
+ @Override
+ public void onPipTransitionCanceled(ComponentName activity, int direction) {
+ onPipTransitionFinishedOrCanceled();
+ }
+
+ private void onPipTransitionFinishedOrCanceled() {
+ if (DEBUG) Log.d(TAG, "onPipTransitionFinishedOrCanceled()");
+
+ if (getState() == STATE_PIP_MENU) {
+ showPipMenu();
+ }
+ }
+
+ /**
+ * A listener interface to receive notification on changes in PIP.
+ */
+ public interface Listener {
+ /**
+ * Invoked when an activity is pinned and PIP manager is set corresponding information.
+ * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned}
+ * because there's no guarantee for the PIP manager be return relavent information
+ * correctly. (e.g. {@link Pip.isPipShown}).
+ */
+ void onPipEntered(String packageName);
+ /** Invoked when a PIPed activity is closed. */
+ void onPipActivityClosed();
+ /** Invoked when the PIP menu gets shown. */
+ void onShowPipMenu();
+ /** Invoked when the PIP menu actions change. */
+ void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions);
+ /** Invoked when the PIPed activity is about to return back to the fullscreen. */
+ void onMoveToFullscreen();
+ /** Invoked when we are above to start resizing the Pip. */
+ void onPipResizeAboutToStart();
+ }
+
+ /**
+ * A listener interface to receive change in PIP's media controller
+ */
+ public interface MediaListener {
+ /** Invoked when the MediaController on PIPed activity is changed. */
+ void onMediaControllerChanged();
+ }
+
+ private String getStateDescription() {
+ if (mSuspendPipResizingReason == 0) {
+ return stateToName(mState);
+ }
+ return stateToName(mResumeResizePinnedStackRunnableState) + " (while " + stateToName(mState)
+ + " is suspended)";
+ }
+
+ private static String stateToName(int state) {
+ switch (state) {
+ case STATE_NO_PIP:
+ return "NO_PIP";
+
+ case STATE_PIP:
+ return "PIP";
+
+ case STATE_PIP_MENU:
+ return "PIP_MENU";
+
+ default:
+ return "UNKNOWN(" + state + ")";
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java
new file mode 100644
index 000000000000..14960c38fd43
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+
+import com.android.wm.shell.R;
+
+
+/**
+ * A view containing PIP controls including fullscreen, close, and media controls.
+ */
+public class PipControlsView extends LinearLayout {
+
+ public PipControlsView(Context context) {
+ this(context, null);
+ }
+
+ public PipControlsView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ layoutInflater.inflate(R.layout.tv_pip_controls, this);
+ setOrientation(LinearLayout.HORIZONTAL);
+ setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+ }
+
+ PipControlButtonView getFullButtonView() {
+ return findViewById(R.id.full_button);
+ }
+
+ PipControlButtonView getCloseButtonView() {
+ return findViewById(R.id.close_button);
+ }
+
+ PipControlButtonView getPlayPauseButtonView() {
+ return findViewById(R.id.play_pause_button);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java
new file mode 100644
index 000000000000..f66e9025a9ed
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.graphics.Color;
+import android.media.session.MediaController;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.wm.shell.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+
+/**
+ * Controller for {@link PipControlsView}.
+ */
+public class PipControlsViewController {
+ private static final String TAG = PipControlsViewController.class.getSimpleName();
+
+ private static final float DISABLED_ACTION_ALPHA = 0.54f;
+
+ private final PipControlsView mView;
+ private final LayoutInflater mLayoutInflater;
+ private final Handler mHandler;
+ private final PipController mPipController;
+ private final PipControlButtonView mPlayPauseButtonView;
+ private MediaController mMediaController;
+ private PipControlButtonView mFocusedChild;
+ private Listener mListener;
+ private ArrayList<PipControlButtonView> mCustomButtonViews = new ArrayList<>();
+ private List<RemoteAction> mCustomActions = new ArrayList<>();
+
+ public PipControlsView getView() {
+ return mView;
+ }
+
+ /**
+ * An interface to listen user action.
+ */
+ public interface Listener {
+ /**
+ * Called when a user clicks close PIP button.
+ */
+ void onClosed();
+ }
+
+ private View.OnAttachStateChangeListener
+ mOnAttachStateChangeListener =
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ updateMediaController();
+ mPipController.addMediaListener(mPipMediaListener);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ mPipController.removeMediaListener(mPipMediaListener);
+ }
+ };
+
+ private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ updateUserActions();
+ }
+ };
+
+ private final PipController.MediaListener mPipMediaListener = this::updateMediaController;
+
+ private final View.OnFocusChangeListener
+ mFocusChangeListener =
+ new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (hasFocus) {
+ mFocusedChild = (PipControlButtonView) view;
+ } else if (mFocusedChild == view) {
+ mFocusedChild = null;
+ }
+ }
+ };
+
+ public PipControlsViewController(PipControlsView view, PipController pipController,
+ LayoutInflater layoutInflater, Handler handler) {
+ super();
+ mView = view;
+ mPipController = pipController;
+ mLayoutInflater = layoutInflater;
+ mHandler = handler;
+
+ mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener);
+ if (mView.isAttachedToWindow()) {
+ mOnAttachStateChangeListener.onViewAttachedToWindow(mView);
+ }
+
+ View fullButtonView = mView.getFullButtonView();
+ fullButtonView.setOnFocusChangeListener(mFocusChangeListener);
+ fullButtonView.setOnClickListener(mView -> mPipController.movePipToFullscreen());
+
+ View closeButtonView = mView.getCloseButtonView();
+ closeButtonView.setOnFocusChangeListener(mFocusChangeListener);
+ closeButtonView.setOnClickListener(v -> {
+ mPipController.closePip();
+ if (mListener != null) {
+ mListener.onClosed();
+ }
+ });
+
+ mPlayPauseButtonView = mView.getPlayPauseButtonView();
+ mPlayPauseButtonView.setOnFocusChangeListener(mFocusChangeListener);
+ mPlayPauseButtonView.setOnClickListener(v -> {
+ if (mMediaController == null || mMediaController.getPlaybackState() == null) {
+ return;
+ }
+ final int playbackState = mPipController.getPlaybackState();
+ if (playbackState == PipController.PLAYBACK_STATE_PAUSED) {
+ mMediaController.getTransportControls().play();
+ } else if (playbackState == PipController.PLAYBACK_STATE_PLAYING) {
+ mMediaController.getTransportControls().pause();
+ }
+
+ // View will be updated later in {@link mMediaControllerCallback}
+ });
+ }
+
+ private void updateMediaController() {
+ AtomicReference<MediaController> newController = new AtomicReference<>();
+ newController.set(mPipController.getMediaController());
+
+ if (newController.get() == null || mMediaController == newController.get()) {
+ return;
+ }
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mMediaControllerCallback);
+ }
+ mMediaController = newController.get();
+ if (mMediaController != null) {
+ mMediaController.registerCallback(mMediaControllerCallback);
+ }
+ updateUserActions();
+ }
+
+ /**
+ * Updates the actions for the PIP. If there are no custom actions, then the media session
+ * actions are shown.
+ */
+ private void updateUserActions() {
+ if (!mCustomActions.isEmpty()) {
+ // Ensure we have as many buttons as actions
+ while (mCustomButtonViews.size() < mCustomActions.size()) {
+ PipControlButtonView buttonView = (PipControlButtonView) mLayoutInflater.inflate(
+ R.layout.tv_pip_custom_control, mView, false);
+ mView.addView(buttonView);
+ mCustomButtonViews.add(buttonView);
+ }
+
+ // Update the visibility of all views
+ for (int i = 0; i < mCustomButtonViews.size(); i++) {
+ mCustomButtonViews.get(i).setVisibility(
+ i < mCustomActions.size() ? View.VISIBLE : View.GONE);
+ }
+
+ // Update the state and visibility of the action buttons, and hide the rest
+ for (int i = 0; i < mCustomActions.size(); i++) {
+ final RemoteAction action = mCustomActions.get(i);
+ PipControlButtonView actionView = mCustomButtonViews.get(i);
+
+ // TODO: Check if the action drawable has changed before we reload it
+ action.getIcon().loadDrawableAsync(mView.getContext(), d -> {
+ d.setTint(Color.WHITE);
+ actionView.setImageDrawable(d);
+ }, mHandler);
+ actionView.setText(action.getContentDescription());
+ if (action.isEnabled()) {
+ actionView.setOnClickListener(v -> {
+ try {
+ action.getActionIntent().send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Failed to send action", e);
+ }
+ });
+ }
+ actionView.setEnabled(action.isEnabled());
+ actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
+ }
+
+ // Hide the media session buttons
+ mPlayPauseButtonView.setVisibility(View.GONE);
+ } else {
+ AtomicInteger state = new AtomicInteger(PipController.STATE_UNKNOWN);
+ state.set(mPipController.getPlaybackState());
+ if (state.get() == PipController.STATE_UNKNOWN
+ || state.get() == PipController.PLAYBACK_STATE_UNAVAILABLE) {
+ mPlayPauseButtonView.setVisibility(View.GONE);
+ } else {
+ mPlayPauseButtonView.setVisibility(View.VISIBLE);
+ if (state.get() == PipController.PLAYBACK_STATE_PLAYING) {
+ mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_pause_white);
+ mPlayPauseButtonView.setText(R.string.pip_pause);
+ } else {
+ mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_play_arrow_white);
+ mPlayPauseButtonView.setText(R.string.pip_play);
+ }
+ }
+
+ // Hide all the custom action buttons
+ for (int i = 0; i < mCustomButtonViews.size(); i++) {
+ mCustomButtonViews.get(i).setVisibility(View.GONE);
+ }
+ }
+ }
+
+
+ /**
+ * Sets the {@link Listener} to listen user actions.
+ */
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+
+ /**
+ * Updates the set of activity-defined actions.
+ */
+ public void setActions(List<? extends RemoteAction> actions) {
+ mCustomActions.clear();
+ mCustomActions.addAll(actions);
+ updateUserActions();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java
new file mode 100644
index 000000000000..e185a9604449
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.app.Activity;
+import android.app.RemoteAction;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.wm.shell.R;
+
+import java.util.Collections;
+
+/**
+ * Activity to show the PIP menu to control PIP.
+ * TODO(b/169395392) Refactor PipMenuActivity to PipMenuView
+ */
+public class PipMenuActivity extends Activity implements PipController.Listener {
+ private static final String TAG = "PipMenuActivity";
+ private static final boolean DEBUG = PipController.DEBUG;
+
+ static final String EXTRA_CUSTOM_ACTIONS = "custom_actions";
+
+ private static PipController sPipController;
+
+ private Animator mFadeInAnimation;
+ private Animator mFadeOutAnimation;
+ private boolean mRestorePipSizeWhenClose;
+ private PipControlsViewController mPipControlsViewController;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ if (DEBUG) Log.d(TAG, "onCreate()");
+
+ super.onCreate(bundle);
+ if (sPipController == null) {
+ finish();
+ }
+ setContentView(R.layout.tv_pip_menu);
+ mPipControlsViewController = new PipControlsViewController(
+ findViewById(R.id.pip_controls), sPipController,
+ getLayoutInflater(), getApplicationContext().getMainThreadHandler());
+ sPipController.addListener(this);
+ mRestorePipSizeWhenClose = true;
+ mFadeInAnimation = AnimatorInflater.loadAnimator(
+ this, R.anim.tv_pip_menu_fade_in_animation);
+ mFadeInAnimation.setTarget(mPipControlsViewController.getView());
+ mFadeOutAnimation = AnimatorInflater.loadAnimator(
+ this, R.anim.tv_pip_menu_fade_out_animation);
+ mFadeOutAnimation.setTarget(mPipControlsViewController.getView());
+
+ onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS));
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (DEBUG) Log.d(TAG, "onNewIntent(), intent=" + intent);
+ super.onNewIntent(intent);
+
+ onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS));
+ }
+
+ private void restorePipAndFinish() {
+ if (DEBUG) Log.d(TAG, "restorePipAndFinish()");
+
+ if (mRestorePipSizeWhenClose) {
+ if (DEBUG) Log.d(TAG, " > restoring to the default position");
+
+ // When PIP menu activity is closed, restore to the default position.
+ sPipController.resizePinnedStack(PipController.STATE_PIP);
+ }
+ finish();
+ }
+
+ @Override
+ public void onResume() {
+ if (DEBUG) Log.d(TAG, "onResume()");
+
+ super.onResume();
+ mFadeInAnimation.start();
+ }
+
+ @Override
+ public void onPause() {
+ if (DEBUG) Log.d(TAG, "onPause()");
+
+ super.onPause();
+ mFadeOutAnimation.start();
+ restorePipAndFinish();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy()");
+
+ super.onDestroy();
+ sPipController.removeListener(this);
+ sPipController.resumePipResizing(
+ PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (DEBUG) Log.d(TAG, "onBackPressed()");
+
+ restorePipAndFinish();
+ }
+
+ @Override
+ public void onPipEntered(String packageName) {
+ if (DEBUG) Log.d(TAG, "onPipEntered(), packageName=" + packageName);
+ }
+
+ @Override
+ public void onPipActivityClosed() {
+ if (DEBUG) Log.d(TAG, "onPipActivityClosed()");
+
+ finish();
+ }
+
+ @Override
+ public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) {
+ if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()");
+
+ boolean hasCustomActions = actions != null && !actions.getList().isEmpty();
+ mPipControlsViewController.setActions(
+ hasCustomActions ? actions.getList() : Collections.emptyList());
+ }
+
+ @Override
+ public void onShowPipMenu() {
+ if (DEBUG) Log.d(TAG, "onShowPipMenu()");
+ }
+
+ @Override
+ public void onMoveToFullscreen() {
+ if (DEBUG) Log.d(TAG, "onMoveToFullscreen()");
+
+ // Moving PIP to fullscreen is implemented by resizing PINNED_STACK with null bounds.
+ // This conflicts with restoring PIP position, so disable it.
+ mRestorePipSizeWhenClose = false;
+ finish();
+ }
+
+ @Override
+ public void onPipResizeAboutToStart() {
+ if (DEBUG) Log.d(TAG, "onPipResizeAboutToStart()");
+
+ finish();
+ sPipController.suspendPipResizing(
+ PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH);
+ }
+
+ @Override
+ public void finish() {
+ if (DEBUG) Log.d(TAG, "finish()", new RuntimeException());
+
+ super.finish();
+ }
+
+ /**
+ * TODO(b/169395392) Refactor PipMenuActivity to PipMenuView
+ *
+ * @param pipController The singleton pipController instance for TV
+ */
+ public static void setPipController(PipController pipController) {
+ if (sPipController != null) {
+ return;
+ }
+ sPipController = pipController;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java
new file mode 100644
index 000000000000..f5bbd23fa1d6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import android.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;
+import android.media.session.MediaController;
+import android.media.session.PlaybackState;
+import android.text.TextUtils;
+
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.wm.shell.R;
+
+/**
+ * A notification that informs users that PIP is running and also provides PIP controls.
+ * <p>Once it's created, it will manage the PIP notification UI by itself except for handling
+ * configuration changes.
+ */
+public class PipNotification {
+ private static final String TAG = "PipNotification";
+ private static final String NOTIFICATION_TAG = PipNotification.class.getSimpleName();
+ private static final boolean DEBUG = PipController.DEBUG;
+
+ static final String ACTION_MENU = "PipNotification.menu";
+ static final String ACTION_CLOSE = "PipNotification.close";
+
+ public static final String NOTIFICATION_CHANNEL_TVPIP = "TPP";
+
+ private final PackageManager mPackageManager;
+
+ private final PipController mPipController;
+
+ private final NotificationManager mNotificationManager;
+ private final Notification.Builder mNotificationBuilder;
+
+ private MediaController mMediaController;
+ private String mDefaultTitle;
+ private int mDefaultIconResId;
+
+ /** Package name for the application that owns PiP window. */
+ private String mPackageName;
+ private boolean mNotified;
+ private String mMediaTitle;
+ private Bitmap mArt;
+
+ private PipController.Listener mPipListener = new PipController.Listener() {
+ @Override
+ public void onPipEntered(String packageName) {
+ mPackageName = packageName;
+ updateMediaControllerMetadata();
+ notifyPipNotification();
+ }
+
+ @Override
+ public void onPipActivityClosed() {
+ dismissPipNotification();
+ mPackageName = null;
+ }
+
+ @Override
+ public void onShowPipMenu() {
+ // no-op.
+ }
+
+ @Override
+ public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) {
+ // no-op.
+ }
+
+ @Override
+ public void onMoveToFullscreen() {
+ dismissPipNotification();
+ mPackageName = null;
+ }
+
+ @Override
+ public void onPipResizeAboutToStart() {
+ // no-op.
+ }
+ };
+
+ private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ if (updateMediaControllerMetadata() && mNotified) {
+ // update notification
+ notifyPipNotification();
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ if (updateMediaControllerMetadata() && mNotified) {
+ // update notification
+ notifyPipNotification();
+ }
+ }
+ };
+
+ private final PipController.MediaListener mPipMediaListener =
+ new PipController.MediaListener() {
+ @Override
+ public void onMediaControllerChanged() {
+ MediaController newController = mPipController.getMediaController();
+ if (newController == null || mMediaController == newController) {
+ return;
+ }
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mMediaControllerCallback);
+ }
+ mMediaController = newController;
+ if (mMediaController != null) {
+ mMediaController.registerCallback(mMediaControllerCallback);
+ }
+ if (updateMediaControllerMetadata() && mNotified) {
+ // update notification
+ notifyPipNotification();
+ }
+ }
+ };
+
+ public PipNotification(Context context, PipController pipController) {
+ mPackageManager = context.getPackageManager();
+
+ mNotificationManager = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+
+ mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL_TVPIP)
+ .setLocalOnly(true)
+ .setOngoing(false)
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .extend(new Notification.TvExtender()
+ .setContentIntent(createPendingIntent(context, ACTION_MENU))
+ .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE)));
+
+ mPipController = pipController;
+ pipController.addListener(mPipListener);
+ pipController.addMediaListener(mPipMediaListener);
+
+ onConfigurationChanged(context);
+ }
+
+ /**
+ * Called by {@link PipController} when the configuration is changed.
+ */
+ void onConfigurationChanged(Context context) {
+ Resources res = context.getResources();
+ mDefaultTitle = res.getString(R.string.pip_notification_unknown_title);
+ mDefaultIconResId = R.drawable.pip_icon;
+ if (mNotified) {
+ // update notification
+ notifyPipNotification();
+ }
+ }
+
+ private void notifyPipNotification() {
+ mNotified = true;
+ mNotificationBuilder
+ .setShowWhen(true)
+ .setWhen(System.currentTimeMillis())
+ .setSmallIcon(mDefaultIconResId)
+ .setContentTitle(getNotificationTitle());
+ if (mArt != null) {
+ mNotificationBuilder.setStyle(new Notification.BigPictureStyle()
+ .bigPicture(mArt));
+ } else {
+ mNotificationBuilder.setStyle(null);
+ }
+ mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP,
+ mNotificationBuilder.build());
+ }
+
+ private void dismissPipNotification() {
+ mNotified = false;
+ mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
+ }
+
+ private boolean updateMediaControllerMetadata() {
+ String title = null;
+ Bitmap art = null;
+ if (mPipController.getMediaController() != null) {
+ MediaMetadata metadata = mPipController.getMediaController().getMetadata();
+ if (metadata != null) {
+ title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE);
+ if (TextUtils.isEmpty(title)) {
+ title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
+ }
+ art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+ if (art == null) {
+ art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
+ }
+ }
+ }
+ if (!TextUtils.equals(title, mMediaTitle) || art != mArt) {
+ mMediaTitle = title;
+ mArt = art;
+ return true;
+ }
+ return false;
+ }
+
+
+ private String getNotificationTitle() {
+ if (!TextUtils.isEmpty(mMediaTitle)) {
+ return mMediaTitle;
+ }
+
+ final String applicationTitle = getApplicationLabel(mPackageName);
+ if (!TextUtils.isEmpty(applicationTitle)) {
+ return applicationTitle;
+ }
+
+ return mDefaultTitle;
+ }
+
+ private String getApplicationLabel(String packageName) {
+ try {
+ final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
+ return mPackageManager.getApplicationLabel(appInfo).toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private static PendingIntent createPendingIntent(Context context, String action) {
+ return PendingIntent.getBroadcast(context, 0,
+ new Intent(action), PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
new file mode 100644
index 000000000000..f3dadfcb933a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -0,0 +1,96 @@
+/*
+ * 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.protolog;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+/**
+ * Defines logging groups for ProtoLog.
+ *
+ * This file is used by the ProtoLogTool to generate optimized logging code.
+ */
+public enum ShellProtoLogGroup implements IProtoLogGroup {
+ // NOTE: Since we enable these from the same WM ShellCommand, these names should not conflict
+ // with those in the framework ProtoLogGroup
+ WM_SHELL_TASK_ORG(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+ Consts.TAG_WM_SHELL),
+ WM_SHELL_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+ Consts.TAG_WM_SHELL),
+ TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
+
+ private final boolean mEnabled;
+ private volatile boolean mLogToProto;
+ private volatile boolean mLogToLogcat;
+ private final String mTag;
+
+ /**
+ * @param enabled set to false to exclude all log statements for this group from
+ * compilation,
+ * they will not be available in runtime.
+ * @param logToProto enable binary logging for the group
+ * @param logToLogcat enable text logging for the group
+ * @param tag name of the source of the logged message
+ */
+ ShellProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) {
+ this.mEnabled = enabled;
+ this.mLogToProto = logToProto;
+ this.mLogToLogcat = logToLogcat;
+ this.mTag = tag;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public boolean isLogToProto() {
+ return mLogToProto;
+ }
+
+ @Override
+ public boolean isLogToLogcat() {
+ return mLogToLogcat;
+ }
+
+ @Override
+ public boolean isLogToAny() {
+ return mLogToLogcat || mLogToProto;
+ }
+
+ @Override
+ public String getTag() {
+ return mTag;
+ }
+
+ @Override
+ public void setLogToProto(boolean logToProto) {
+ this.mLogToProto = logToProto;
+ }
+
+ @Override
+ public void setLogToLogcat(boolean logToLogcat) {
+ this.mLogToLogcat = logToLogcat;
+ }
+
+ private static class Consts {
+ private static final String TAG_WM_SHELL = "WindowManagerShell";
+
+ private static final boolean ENABLE_DEBUG = true;
+ private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
new file mode 100644
index 000000000000..66ecf453c362
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.protolog;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.BaseProtoLogImpl;
+import com.android.internal.protolog.ProtoLogViewerConfigReader;
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.wm.shell.R;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import org.json.JSONException;
+
+
+/**
+ * A service for the ProtoLog logging system.
+ */
+public class ShellProtoLogImpl extends BaseProtoLogImpl {
+ private static final String TAG = "ProtoLogImpl";
+ private static final int BUFFER_CAPACITY = 1024 * 1024;
+ // TODO: Get the right path for the proto log file when we initialize the shell components
+ private static final String LOG_FILENAME = new File("wm_shell_log.pb").getAbsolutePath();
+
+ private static ShellProtoLogImpl sServiceInstance = null;
+
+ static {
+ addLogGroupEnum(ShellProtoLogGroup.values());
+ }
+
+ /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
+ public static void d(IProtoLogGroup group, int messageHash, int paramsMask,
+ @Nullable String messageString,
+ Object... args) {
+ getSingleInstance()
+ .log(LogLevel.DEBUG, group, messageHash, paramsMask, messageString, args);
+ }
+
+ /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
+ public static void v(IProtoLogGroup group, int messageHash, int paramsMask,
+ @Nullable String messageString,
+ Object... args) {
+ getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString,
+ args);
+ }
+
+ /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
+ public static void i(IProtoLogGroup group, int messageHash, int paramsMask,
+ @Nullable String messageString,
+ Object... args) {
+ getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);
+ }
+
+ /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
+ public static void w(IProtoLogGroup group, int messageHash, int paramsMask,
+ @Nullable String messageString,
+ Object... args) {
+ getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args);
+ }
+
+ /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
+ public static void e(IProtoLogGroup group, int messageHash, int paramsMask,
+ @Nullable String messageString,
+ Object... args) {
+ getSingleInstance()
+ .log(LogLevel.ERROR, group, messageHash, paramsMask, messageString, args);
+ }
+
+ /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
+ public static void wtf(IProtoLogGroup group, int messageHash, int paramsMask,
+ @Nullable String messageString,
+ Object... args) {
+ getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args);
+ }
+
+ /** Returns true iff logging is enabled for the given {@code IProtoLogGroup}. */
+ public static boolean isEnabled(IProtoLogGroup group) {
+ return group.isLogToLogcat()
+ || (group.isLogToProto() && getSingleInstance().isProtoEnabled());
+ }
+
+ /**
+ * Returns the single instance of the ProtoLogImpl singleton class.
+ */
+ public static synchronized ShellProtoLogImpl getSingleInstance() {
+ if (sServiceInstance == null) {
+ sServiceInstance = new ShellProtoLogImpl();
+ }
+ return sServiceInstance;
+ }
+
+ public int startTextLogging(Context context, String[] groups, PrintWriter pw) {
+ try {
+ mViewerConfig.loadViewerConfig(
+ context.getResources().openRawResource(R.raw.wm_shell_protolog));
+ return setLogging(true /* setTextLogging */, true, pw, groups);
+ } catch (IOException e) {
+ Log.i(TAG, "Unable to load log definitions: IOException while reading "
+ + "wm_shell_protolog. " + e);
+ } catch (JSONException e) {
+ Log.i(TAG, "Unable to load log definitions: JSON parsing exception while reading "
+ + "wm_shell_protolog. " + e);
+ }
+ return -1;
+ }
+
+ public int stopTextLogging(String[] groups, PrintWriter pw) {
+ return setLogging(true /* setTextLogging */, false, pw, groups);
+ }
+
+ private ShellProtoLogImpl() {
+ super(new File(LOG_FILENAME), null, BUFFER_CAPACITY, new ProtoLogViewerConfigReader());
+ }
+}
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java
new file mode 100644
index 000000000000..2cb1fff4cde6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.view.View;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.Interpolators;
+
+/**
+ * View for the handle in the docked stack divider.
+ */
+public class DividerHandleView extends View {
+
+ private static final Property<DividerHandleView, Integer> WIDTH_PROPERTY =
+ new Property<DividerHandleView, Integer>(Integer.class, "width") {
+ @Override
+ public Integer get(DividerHandleView object) {
+ return object.mCurrentWidth;
+ }
+
+ @Override
+ public void set(DividerHandleView object, Integer value) {
+ object.mCurrentWidth = value;
+ object.invalidate();
+ }
+ };
+
+ private static final Property<DividerHandleView, Integer> HEIGHT_PROPERTY =
+ new Property<DividerHandleView, Integer>(Integer.class, "height") {
+ @Override
+ public Integer get(DividerHandleView object) {
+ return object.mCurrentHeight;
+ }
+
+ @Override
+ public void set(DividerHandleView object, Integer value) {
+ object.mCurrentHeight = value;
+ object.invalidate();
+ }
+ };
+
+ private final Paint mPaint = new Paint();
+ private final int mWidth;
+ private final int mHeight;
+ private final int mCircleDiameter;
+ private int mCurrentWidth;
+ private int mCurrentHeight;
+ private AnimatorSet mAnimator;
+ private boolean mTouching;
+
+ public DividerHandleView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null));
+ mPaint.setAntiAlias(true);
+ mWidth = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_width);
+ mHeight = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_height);
+ mCurrentWidth = mWidth;
+ mCurrentHeight = mHeight;
+ mCircleDiameter = (mWidth + mHeight) / 3;
+ }
+
+ void setTouching(boolean touching, boolean animate) {
+ if (touching == mTouching) {
+ return;
+ }
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ mAnimator = null;
+ }
+ if (!animate) {
+ if (touching) {
+ mCurrentWidth = mCircleDiameter;
+ mCurrentHeight = mCircleDiameter;
+ } else {
+ mCurrentWidth = mWidth;
+ mCurrentHeight = mHeight;
+ }
+ invalidate();
+ } else {
+ animateToTarget(touching ? mCircleDiameter : mWidth,
+ touching ? mCircleDiameter : mHeight, touching);
+ }
+ mTouching = touching;
+ }
+
+ private void animateToTarget(int targetWidth, int targetHeight, boolean touching) {
+ ObjectAnimator widthAnimator = ObjectAnimator.ofInt(this, WIDTH_PROPERTY,
+ mCurrentWidth, targetWidth);
+ ObjectAnimator heightAnimator = ObjectAnimator.ofInt(this, HEIGHT_PROPERTY,
+ mCurrentHeight, targetHeight);
+ mAnimator = new AnimatorSet();
+ mAnimator.playTogether(widthAnimator, heightAnimator);
+ mAnimator.setDuration(touching
+ ? DividerView.TOUCH_ANIMATION_DURATION
+ : DividerView.TOUCH_RELEASE_ANIMATION_DURATION);
+ mAnimator.setInterpolator(touching
+ ? Interpolators.TOUCH_RESPONSE
+ : Interpolators.FAST_OUT_SLOW_IN);
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimator = null;
+ }
+ });
+ mAnimator.start();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int left = getWidth() / 2 - mCurrentWidth / 2;
+ int top = getHeight() / 2 - mCurrentHeight / 2;
+ int radius = Math.min(mCurrentWidth, mCurrentHeight) / 2;
+ canvas.drawRoundRect(left, top, left + mCurrentWidth, top + mCurrentHeight,
+ radius, radius, mPaint);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java
new file mode 100644
index 000000000000..ff617ed466d1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java
@@ -0,0 +1,415 @@
+/*
+ * 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.splitscreen;
+
+import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.window.TaskOrganizer;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.TransactionPool;
+
+class DividerImeController implements DisplayImeController.ImePositionProcessor {
+ private static final String TAG = "DividerImeController";
+ private static final boolean DEBUG = SplitScreenController.DEBUG;
+
+ private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
+
+ private final SplitScreenTaskOrganizer mSplits;
+ private final TransactionPool mTransactionPool;
+ private final Handler mHandler;
+ private final TaskOrganizer mTaskOrganizer;
+
+ /**
+ * These are the y positions of the top of the IME surface when it is hidden and when it is
+ * shown respectively. These are NOT necessarily the top of the visible IME itself.
+ */
+ private int mHiddenTop = 0;
+ private int mShownTop = 0;
+
+ // The following are target states (what we are curretly animating towards).
+ /**
+ * {@code true} if, at the end of the animation, the split task positions should be
+ * adjusted by height of the IME. This happens when the secondary split is the IME target.
+ */
+ private boolean mTargetAdjusted = false;
+ /**
+ * {@code true} if, at the end of the animation, the IME should be shown/visible
+ * regardless of what has focus.
+ */
+ private boolean mTargetShown = false;
+ private float mTargetPrimaryDim = 0.f;
+ private float mTargetSecondaryDim = 0.f;
+
+ // The following are the current (most recent) states set during animation
+ /** {@code true} if the secondary split has IME focus. */
+ private boolean mSecondaryHasFocus = false;
+ /** The dimming currently applied to the primary/secondary splits. */
+ private float mLastPrimaryDim = 0.f;
+ private float mLastSecondaryDim = 0.f;
+ /** The most recent y position of the top of the IME surface */
+ private int mLastAdjustTop = -1;
+
+ // The following are states reached last time an animation fully completed.
+ /** {@code true} if the IME was shown/visible by the last-completed animation. */
+ private boolean mImeWasShown = false;
+ /** {@code true} if the split positions were adjusted by the last-completed animation. */
+ private boolean mAdjusted = false;
+
+ /**
+ * When some aspect of split-screen needs to animate independent from the IME,
+ * this will be non-null and control split animation.
+ */
+ @Nullable
+ private ValueAnimator mAnimation = null;
+
+ private boolean mPaused = true;
+ private boolean mPausedTargetAdjusted = false;
+ private boolean mAdjustedWhileHidden = false;
+
+ DividerImeController(SplitScreenTaskOrganizer splits, TransactionPool pool, Handler handler,
+ TaskOrganizer taskOrganizer) {
+ mSplits = splits;
+ mTransactionPool = pool;
+ mHandler = handler;
+ mTaskOrganizer = taskOrganizer;
+ }
+
+ private DividerView getView() {
+ return mSplits.mSplitScreenController.getDividerView();
+ }
+
+ private SplitDisplayLayout getLayout() {
+ return mSplits.mSplitScreenController.getSplitLayout();
+ }
+
+ private boolean isDividerVisible() {
+ return mSplits.mSplitScreenController.isDividerVisible();
+ }
+
+ private boolean getSecondaryHasFocus(int displayId) {
+ WindowContainerToken imeSplit = mTaskOrganizer.getImeTarget(displayId);
+ return imeSplit != null
+ && (imeSplit.asBinder() == mSplits.mSecondary.token.asBinder());
+ }
+
+ void reset() {
+ mPaused = true;
+ mPausedTargetAdjusted = false;
+ mAdjustedWhileHidden = false;
+ mAnimation = null;
+ mAdjusted = mTargetAdjusted = false;
+ mImeWasShown = mTargetShown = false;
+ mTargetPrimaryDim = mTargetSecondaryDim = mLastPrimaryDim = mLastSecondaryDim = 0.f;
+ mSecondaryHasFocus = false;
+ mLastAdjustTop = -1;
+ }
+
+ private void updateDimTargets() {
+ final boolean splitIsVisible = !getView().isHidden();
+ mTargetPrimaryDim = (mSecondaryHasFocus && mTargetShown && splitIsVisible)
+ ? ADJUSTED_NONFOCUS_DIM : 0.f;
+ mTargetSecondaryDim = (!mSecondaryHasFocus && mTargetShown && splitIsVisible)
+ ? ADJUSTED_NONFOCUS_DIM : 0.f;
+ }
+
+ @Override
+ @ImeAnimationFlags
+ public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
+ boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t) {
+ mHiddenTop = hiddenTop;
+ mShownTop = shownTop;
+ mTargetShown = imeShouldShow;
+ if (!isDividerVisible()) {
+ return 0;
+ }
+ final boolean splitIsVisible = !getView().isHidden();
+ mSecondaryHasFocus = getSecondaryHasFocus(displayId);
+ final boolean targetAdjusted = splitIsVisible && imeShouldShow && mSecondaryHasFocus
+ && !imeIsFloating && !getLayout().mDisplayLayout.isLandscape()
+ && !mSplits.mSplitScreenController.isMinimized();
+ if (mLastAdjustTop < 0) {
+ mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop;
+ } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) {
+ if (mTargetAdjusted != targetAdjusted && targetAdjusted == mAdjusted) {
+ // Check for an "interruption" of an existing animation. In this case, we
+ // need to fake-flip the last-known state direction so that the animation
+ // completes in the other direction.
+ mAdjusted = mTargetAdjusted;
+ } else if (targetAdjusted && mTargetAdjusted && mAdjusted) {
+ // Already fully adjusted for IME, but IME height has changed; so, force-start
+ // an async animation to the new IME height.
+ mAdjusted = false;
+ }
+ }
+ if (mPaused) {
+ mPausedTargetAdjusted = targetAdjusted;
+ if (DEBUG) Slog.d(TAG, " ime starting but paused " + dumpState());
+ return (targetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
+ }
+ mTargetAdjusted = targetAdjusted;
+ updateDimTargets();
+ if (DEBUG) Slog.d(TAG, " ime starting. vis:" + splitIsVisible + " " + dumpState());
+ if (mAnimation != null || (mImeWasShown && imeShouldShow
+ && mTargetAdjusted != mAdjusted)) {
+ // We need to animate adjustment independently of the IME position, so
+ // start our own animation to drive adjustment. This happens when a
+ // different split's editor has gained focus while the IME is still visible.
+ startAsyncAnimation();
+ }
+ if (splitIsVisible) {
+ // If split is hidden, we don't want to trigger any relayouts that would cause the
+ // divider to show again.
+ updateImeAdjustState();
+ } else {
+ mAdjustedWhileHidden = true;
+ }
+ return (mTargetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
+ }
+
+ private void updateImeAdjustState() {
+ updateImeAdjustState(false /* force */);
+ }
+
+ private void updateImeAdjustState(boolean force) {
+ if (mAdjusted != mTargetAdjusted || force) {
+ // Reposition the server's secondary split position so that it evaluates
+ // insets properly.
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ final SplitDisplayLayout splitLayout = getLayout();
+ if (mTargetAdjusted) {
+ splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop);
+ wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary);
+ // "Freeze" the configuration size so that the app doesn't get a config
+ // or relaunch. This is required because normally nav-bar contributes
+ // to configuration bounds (via nondecorframe).
+ Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration
+ .windowConfiguration.getAppBounds());
+ adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top
+ - splitLayout.mSecondary.top);
+ wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds);
+ wct.setScreenSizeDp(mSplits.mSecondary.token,
+ mSplits.mSecondary.configuration.screenWidthDp,
+ mSplits.mSecondary.configuration.screenHeightDp);
+
+ wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary);
+ adjustAppBounds = new Rect(mSplits.mPrimary.configuration
+ .windowConfiguration.getAppBounds());
+ adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top
+ - splitLayout.mPrimary.top);
+ wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds);
+ wct.setScreenSizeDp(mSplits.mPrimary.token,
+ mSplits.mPrimary.configuration.screenWidthDp,
+ mSplits.mPrimary.configuration.screenHeightDp);
+ } else {
+ wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary);
+ wct.setAppBounds(mSplits.mSecondary.token, null);
+ wct.setScreenSizeDp(mSplits.mSecondary.token,
+ SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
+ wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary);
+ wct.setAppBounds(mSplits.mPrimary.token, null);
+ wct.setScreenSizeDp(mSplits.mPrimary.token,
+ SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
+ }
+
+ if (!mSplits.mSplitScreenController.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ }
+
+ // Update all the adjusted-for-ime states
+ if (!mPaused) {
+ final DividerView view = getView();
+ if (view != null) {
+ view.setAdjustedForIme(mTargetShown, mTargetShown
+ ? DisplayImeController.ANIMATION_DURATION_SHOW_MS
+ : DisplayImeController.ANIMATION_DURATION_HIDE_MS);
+ }
+ }
+ mSplits.mSplitScreenController.setAdjustedForIme(mTargetShown && !mPaused);
+ }
+
+ public void updateAdjustForIme() {
+ updateImeAdjustState(mAdjustedWhileHidden);
+ mAdjustedWhileHidden = false;
+ }
+
+ @Override
+ public void onImePositionChanged(int displayId, int imeTop,
+ SurfaceControl.Transaction t) {
+ if (mAnimation != null || !isDividerVisible() || mPaused) {
+ // Not synchronized with IME anymore, so return.
+ return;
+ }
+ final float fraction = ((float) imeTop - mHiddenTop) / (mShownTop - mHiddenTop);
+ final float progress = mTargetShown ? fraction : 1.f - fraction;
+ onProgress(progress, t);
+ }
+
+ @Override
+ public void onImeEndPositioning(int displayId, boolean cancelled,
+ SurfaceControl.Transaction t) {
+ if (mAnimation != null || !isDividerVisible() || mPaused) {
+ // Not synchronized with IME anymore, so return.
+ return;
+ }
+ onEnd(cancelled, t);
+ }
+
+ private void onProgress(float progress, SurfaceControl.Transaction t) {
+ final DividerView view = getView();
+ if (mTargetAdjusted != mAdjusted && !mPaused) {
+ final SplitDisplayLayout splitLayout = getLayout();
+ final float fraction = mTargetAdjusted ? progress : 1.f - progress;
+ mLastAdjustTop = (int) (fraction * mShownTop + (1.f - fraction) * mHiddenTop);
+ splitLayout.updateAdjustedBounds(mLastAdjustTop, mHiddenTop, mShownTop);
+ view.resizeSplitSurfaces(t, splitLayout.mAdjustedPrimary,
+ splitLayout.mAdjustedSecondary);
+ }
+ final float invProg = 1.f - progress;
+ view.setResizeDimLayer(t, true /* primary */,
+ mLastPrimaryDim * invProg + progress * mTargetPrimaryDim);
+ view.setResizeDimLayer(t, false /* primary */,
+ mLastSecondaryDim * invProg + progress * mTargetSecondaryDim);
+ }
+
+ void setDimsHidden(SurfaceControl.Transaction t, boolean hidden) {
+ final DividerView view = getView();
+ if (hidden) {
+ view.setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
+ view.setResizeDimLayer(t, false /* primary */, 0.f /* alpha */);
+ } else {
+ updateDimTargets();
+ view.setResizeDimLayer(t, true /* primary */, mTargetPrimaryDim);
+ view.setResizeDimLayer(t, false /* primary */, mTargetSecondaryDim);
+ }
+ }
+
+ private void onEnd(boolean cancelled, SurfaceControl.Transaction t) {
+ if (!cancelled) {
+ onProgress(1.f, t);
+ mAdjusted = mTargetAdjusted;
+ mImeWasShown = mTargetShown;
+ mLastAdjustTop = mAdjusted ? mShownTop : mHiddenTop;
+ mLastPrimaryDim = mTargetPrimaryDim;
+ mLastSecondaryDim = mTargetSecondaryDim;
+ }
+ }
+
+ private void startAsyncAnimation() {
+ if (mAnimation != null) {
+ mAnimation.cancel();
+ }
+ mAnimation = ValueAnimator.ofFloat(0.f, 1.f);
+ mAnimation.setDuration(DisplayImeController.ANIMATION_DURATION_SHOW_MS);
+ if (mTargetAdjusted != mAdjusted) {
+ final float fraction =
+ ((float) mLastAdjustTop - mHiddenTop) / (mShownTop - mHiddenTop);
+ final float progress = mTargetAdjusted ? fraction : 1.f - fraction;
+ mAnimation.setCurrentFraction(progress);
+ }
+
+ mAnimation.addUpdateListener(animation -> {
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ float value = (float) animation.getAnimatedValue();
+ onProgress(value, t);
+ t.apply();
+ mTransactionPool.release(t);
+ });
+ mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR);
+ mAnimation.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancel = false;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancel = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ onEnd(mCancel, t);
+ t.apply();
+ mTransactionPool.release(t);
+ mAnimation = null;
+ }
+ });
+ mAnimation.start();
+ }
+
+ private String dumpState() {
+ return "top:" + mHiddenTop + "->" + mShownTop
+ + " adj:" + mAdjusted + "->" + mTargetAdjusted + "(" + mLastAdjustTop + ")"
+ + " shw:" + mImeWasShown + "->" + mTargetShown
+ + " dims:" + mLastPrimaryDim + "," + mLastSecondaryDim
+ + "->" + mTargetPrimaryDim + "," + mTargetSecondaryDim
+ + " shf:" + mSecondaryHasFocus + " desync:" + (mAnimation != null)
+ + " paus:" + mPaused + "[" + mPausedTargetAdjusted + "]";
+ }
+
+ /** Completely aborts/resets adjustment state */
+ public void pause(int displayId) {
+ if (DEBUG) Slog.d(TAG, "ime pause posting " + dumpState());
+ mHandler.post(() -> {
+ if (DEBUG) Slog.d(TAG, "ime pause run posted " + dumpState());
+ if (mPaused) {
+ return;
+ }
+ mPaused = true;
+ mPausedTargetAdjusted = mTargetAdjusted;
+ mTargetAdjusted = false;
+ mTargetPrimaryDim = mTargetSecondaryDim = 0.f;
+ updateImeAdjustState();
+ startAsyncAnimation();
+ if (mAnimation != null) {
+ mAnimation.end();
+ }
+ });
+ }
+
+ public void resume(int displayId) {
+ if (DEBUG) Slog.d(TAG, "ime resume posting " + dumpState());
+ mHandler.post(() -> {
+ if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState());
+ if (!mPaused) {
+ return;
+ }
+ mPaused = false;
+ mTargetAdjusted = mPausedTargetAdjusted;
+ updateDimTargets();
+ final DividerView view = getView();
+ if ((mTargetAdjusted != mAdjusted) && !mSplits.mSplitScreenController.isMinimized()
+ && view != null) {
+ // End unminimize animations since they conflict with adjustment animations.
+ view.finishAnimations();
+ }
+ updateImeAdjustState();
+ startAsyncAnimation();
+ });
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/src/com/android/wm/shell/tests/WindowManagerShellTest.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java
index 376875b143a1..23d86a00d4bf 100644
--- a/libs/WindowManager/Shell/tests/src/com/android/wm/shell/tests/WindowManagerShellTest.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,27 +14,12 @@
* limitations under the License.
*/
-package com.android.wm.shell.tests;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.WindowManagerShell;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
+package com.android.wm.shell.splitscreen;
/**
- * Tests for the shell.
+ * Class to hold state of divider that needs to persist across configuration changes.
*/
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class WindowManagerShellTest {
-
- WindowManagerShell mShell;
-
- @Test
- public void testNothing() {
- // Do nothing
- }
+final class DividerState {
+ public boolean animateAfterRecentsDrawn;
+ public float mRatioPositionBeforeMinimized;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java
new file mode 100644
index 000000000000..2b14e8bf88d6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java
@@ -0,0 +1,1339 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
+import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
+import static android.view.WindowManager.DOCKED_RIGHT;
+
+import android.animation.AnimationHandler;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Region.Op;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Slog;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver.InternalInsetsInfo;
+import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+
+import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
+import com.android.internal.policy.DockedDividerUtils;
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.FlingAnimationUtils;
+import com.android.wm.shell.animation.Interpolators;
+
+import java.util.function.Consumer;
+
+/**
+ * Docked stack divider.
+ */
+public class DividerView extends FrameLayout implements OnTouchListener,
+ OnComputeInternalInsetsListener {
+ private static final String TAG = "DividerView";
+ private static final boolean DEBUG = SplitScreenController.DEBUG;
+
+ interface DividerCallbacks {
+ void onDraggingStart();
+ void onDraggingEnd();
+ }
+
+ static final long TOUCH_ANIMATION_DURATION = 150;
+ static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;
+
+ public static final int INVALID_RECENTS_GROW_TARGET = -1;
+
+ private static final int LOG_VALUE_RESIZE_50_50 = 0;
+ private static final int LOG_VALUE_RESIZE_DOCKED_SMALLER = 1;
+ private static final int LOG_VALUE_RESIZE_DOCKED_LARGER = 2;
+
+ private static final int LOG_VALUE_UNDOCK_MAX_DOCKED = 0;
+ private static final int LOG_VALUE_UNDOCK_MAX_OTHER = 1;
+
+ private static final int TASK_POSITION_SAME = Integer.MAX_VALUE;
+
+ /**
+ * How much the background gets scaled when we are in the minimized dock state.
+ */
+ private static final float MINIMIZE_DOCK_SCALE = 0f;
+ private static final float ADJUSTED_FOR_IME_SCALE = 0.5f;
+
+ private static final PathInterpolator SLOWDOWN_INTERPOLATOR =
+ new PathInterpolator(0.5f, 1f, 0.5f, 1f);
+ private static final PathInterpolator DIM_INTERPOLATOR =
+ new PathInterpolator(.23f, .87f, .52f, -0.11f);
+ private static final Interpolator IME_ADJUST_INTERPOLATOR =
+ new PathInterpolator(0.2f, 0f, 0.1f, 1f);
+
+ private DividerHandleView mHandle;
+ private View mBackground;
+ private MinimizedDockShadow mMinimizedShadow;
+ private int mStartX;
+ private int mStartY;
+ private int mStartPosition;
+ private int mDockSide;
+ private boolean mMoving;
+ private int mTouchSlop;
+ private boolean mBackgroundLifted;
+ private boolean mIsInMinimizeInteraction;
+ SnapTarget mSnapTargetBeforeMinimized;
+
+ private int mDividerInsets;
+ private final Display mDefaultDisplay;
+
+ private int mDividerSize;
+ private int mTouchElevation;
+ private int mLongPressEntraceAnimDuration;
+
+ private final Rect mDockedRect = new Rect();
+ private final Rect mDockedTaskRect = new Rect();
+ private final Rect mOtherTaskRect = new Rect();
+ private final Rect mOtherRect = new Rect();
+ private final Rect mDockedInsetRect = new Rect();
+ private final Rect mOtherInsetRect = new Rect();
+ private final Rect mLastResizeRect = new Rect();
+ private final Rect mTmpRect = new Rect();
+ private SplitScreenController mSplitScreenController;
+ private WindowManagerProxy mWindowManagerProxy;
+ private DividerWindowManager mWindowManager;
+ private VelocityTracker mVelocityTracker;
+ private FlingAnimationUtils mFlingAnimationUtils;
+ private SplitDisplayLayout mSplitLayout;
+ private DividerImeController mImeController;
+ private DividerCallbacks mCallback;
+ private final AnimationHandler mAnimationHandler = new AnimationHandler();
+
+ private ValueAnimator mCurrentAnimator;
+ private boolean mEntranceAnimationRunning;
+ private boolean mExitAnimationRunning;
+ private int mExitStartPosition;
+ private boolean mDockedStackMinimized;
+ private boolean mHomeStackResizable;
+ private boolean mAdjustedForIme;
+ private DividerState mState;
+
+ private SplitScreenTaskOrganizer mTiles;
+ boolean mFirstLayout = true;
+ int mDividerPositionX;
+ int mDividerPositionY;
+
+ private final Matrix mTmpMatrix = new Matrix();
+ private final float[] mTmpValues = new float[9];
+
+ // The view is removed or in the process of been removed from the system.
+ private boolean mRemoved;
+
+ // Whether the surface for this view has been hidden regardless of actual visibility. This is
+ // used interact with keyguard.
+ private boolean mSurfaceHidden = false;
+
+ private final Handler mHandler = new Handler();
+
+ private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ final DividerSnapAlgorithm snapAlgorithm = getSnapAlgorithm();
+ if (isHorizontalDivision()) {
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
+ mContext.getString(R.string.accessibility_action_divider_top_full)));
+ if (snapAlgorithm.isFirstSplitTargetAvailable()) {
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
+ mContext.getString(R.string.accessibility_action_divider_top_70)));
+ }
+ if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
+ // Only show the middle target if there are more than 1 split target
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
+ mContext.getString(R.string.accessibility_action_divider_top_50)));
+ }
+ if (snapAlgorithm.isLastSplitTargetAvailable()) {
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
+ mContext.getString(R.string.accessibility_action_divider_top_30)));
+ }
+ info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
+ mContext.getString(R.string.accessibility_action_divider_bottom_full)));
+ } else {
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
+ mContext.getString(R.string.accessibility_action_divider_left_full)));
+ if (snapAlgorithm.isFirstSplitTargetAvailable()) {
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
+ mContext.getString(R.string.accessibility_action_divider_left_70)));
+ }
+ if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
+ // Only show the middle target if there are more than 1 split target
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
+ mContext.getString(R.string.accessibility_action_divider_left_50)));
+ }
+ if (snapAlgorithm.isLastSplitTargetAvailable()) {
+ info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
+ mContext.getString(R.string.accessibility_action_divider_left_30)));
+ }
+ info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
+ mContext.getString(R.string.accessibility_action_divider_right_full)));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ int currentPosition = getCurrentPosition();
+ SnapTarget nextTarget = null;
+ DividerSnapAlgorithm snapAlgorithm = mSplitLayout.getSnapAlgorithm();
+ if (action == R.id.action_move_tl_full) {
+ nextTarget = snapAlgorithm.getDismissEndTarget();
+ } else if (action == R.id.action_move_tl_70) {
+ nextTarget = snapAlgorithm.getLastSplitTarget();
+ } else if (action == R.id.action_move_tl_50) {
+ nextTarget = snapAlgorithm.getMiddleTarget();
+ } else if (action == R.id.action_move_tl_30) {
+ nextTarget = snapAlgorithm.getFirstSplitTarget();
+ } else if (action == R.id.action_move_rb_full) {
+ nextTarget = snapAlgorithm.getDismissStartTarget();
+ }
+ if (nextTarget != null) {
+ startDragging(true /* animate */, false /* touching */);
+ stopDragging(currentPosition, nextTarget, 250, Interpolators.FAST_OUT_SLOW_IN);
+ return true;
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ };
+
+ private final Runnable mResetBackgroundRunnable = new Runnable() {
+ @Override
+ public void run() {
+ resetBackground();
+ }
+ };
+
+ private Runnable mUpdateEmbeddedMatrix = () -> {
+ if (getViewRootImpl() == null) {
+ return;
+ }
+ if (isHorizontalDivision()) {
+ mTmpMatrix.setTranslate(0, mDividerPositionY - mDividerInsets);
+ } else {
+ mTmpMatrix.setTranslate(mDividerPositionX - mDividerInsets, 0);
+ }
+ mTmpMatrix.getValues(mTmpValues);
+ try {
+ getViewRootImpl().getAccessibilityEmbeddedConnection().setScreenMatrix(mTmpValues);
+ } catch (RemoteException e) {
+ }
+ };
+
+ public DividerView(Context context) {
+ this(context, null);
+ }
+
+ public DividerView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ final DisplayManager displayManager =
+ (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
+ mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ mAnimationHandler.setProvider(new SfVsyncFrameCallbackProvider());
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mHandle = findViewById(R.id.docked_divider_handle);
+ mBackground = findViewById(R.id.docked_divider_background);
+ mMinimizedShadow = findViewById(R.id.minimized_dock_shadow);
+ mHandle.setOnTouchListener(this);
+ final int dividerWindowWidth = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.docked_stack_divider_thickness);
+ mDividerInsets = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.docked_stack_divider_insets);
+ mDividerSize = dividerWindowWidth - 2 * mDividerInsets;
+ mTouchElevation = getResources().getDimensionPixelSize(
+ R.dimen.docked_stack_divider_lift_elevation);
+ mLongPressEntraceAnimDuration = getResources().getInteger(
+ R.integer.long_press_dock_anim_duration);
+ mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
+ mFlingAnimationUtils = new FlingAnimationUtils(getResources().getDisplayMetrics(), 0.3f);
+ boolean landscape = getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ mHandle.setPointerIcon(PointerIcon.getSystemIcon(getContext(),
+ landscape ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW));
+ getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ mHandle.setAccessibilityDelegate(mHandleDelegate);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // Save the current target if not minimized once attached to window
+ if (mDockSide != WindowManager.DOCKED_INVALID && !mIsInMinimizeInteraction) {
+ saveSnapTargetBeforeMinimized(mSnapTargetBeforeMinimized);
+ }
+ mFirstLayout = true;
+ }
+
+ void onDividerRemoved() {
+ mRemoved = true;
+ mCallback = null;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mFirstLayout) {
+ // Wait for first layout so that the ViewRootImpl surface has been created.
+ initializeSurfaceState();
+ mFirstLayout = false;
+ }
+ int minimizeLeft = 0;
+ int minimizeTop = 0;
+ if (mDockSide == WindowManager.DOCKED_TOP) {
+ minimizeTop = mBackground.getTop();
+ } else if (mDockSide == WindowManager.DOCKED_LEFT) {
+ minimizeLeft = mBackground.getLeft();
+ } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
+ minimizeLeft = mBackground.getRight() - mMinimizedShadow.getWidth();
+ }
+ mMinimizedShadow.layout(minimizeLeft, minimizeTop,
+ minimizeLeft + mMinimizedShadow.getMeasuredWidth(),
+ minimizeTop + mMinimizedShadow.getMeasuredHeight());
+ if (changed) {
+ notifySplitScreenBoundsChanged();
+ }
+ }
+
+ void injectDependencies(SplitScreenController splitScreenController,
+ DividerWindowManager windowManager, DividerState dividerState,
+ DividerCallbacks callback, SplitScreenTaskOrganizer tiles, SplitDisplayLayout sdl,
+ DividerImeController imeController, WindowManagerProxy wmProxy) {
+ mSplitScreenController = splitScreenController;
+ mWindowManager = windowManager;
+ mState = dividerState;
+ mCallback = callback;
+ mTiles = tiles;
+ mSplitLayout = sdl;
+ mImeController = imeController;
+ mWindowManagerProxy = wmProxy;
+
+ if (mState.mRatioPositionBeforeMinimized == 0) {
+ // Set the middle target as the initial state
+ mSnapTargetBeforeMinimized = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
+ } else {
+ repositionSnapTargetBeforeMinimized();
+ }
+ }
+
+ /** Gets non-minimized secondary bounds of split screen. */
+ public Rect getNonMinimizedSplitScreenSecondaryBounds() {
+ mOtherTaskRect.set(mSplitLayout.mSecondary);
+ return mOtherTaskRect;
+ }
+
+ private boolean inSplitMode() {
+ return getVisibility() == VISIBLE;
+ }
+
+ /** Unlike setVisible, this directly hides the surface without changing view visibility. */
+ void setHidden(boolean hidden) {
+ if (mSurfaceHidden == hidden) {
+ return;
+ }
+ mSurfaceHidden = hidden;
+ post(() -> {
+ final SurfaceControl sc = getWindowSurfaceControl();
+ if (sc == null) {
+ return;
+ }
+ Transaction t = mTiles.getTransaction();
+ if (hidden) {
+ t.hide(sc);
+ } else {
+ t.show(sc);
+ }
+ mImeController.setDimsHidden(t, hidden);
+ t.apply();
+ mTiles.releaseTransaction(t);
+ });
+ }
+
+ boolean isHidden() {
+ return mSurfaceHidden;
+ }
+
+ /** Starts dragging the divider bar. */
+ public boolean startDragging(boolean animate, boolean touching) {
+ cancelFlingAnimation();
+ if (touching) {
+ mHandle.setTouching(true, animate);
+ }
+ mDockSide = mSplitLayout.getPrimarySplitSide();
+
+ mWindowManagerProxy.setResizing(true);
+ if (touching) {
+ mWindowManager.setSlippery(false);
+ liftBackground();
+ }
+ if (mCallback != null) {
+ mCallback.onDraggingStart();
+ }
+ return inSplitMode();
+ }
+
+ /** Stops dragging the divider bar. */
+ public void stopDragging(int position, float velocity, boolean avoidDismissStart,
+ boolean logMetrics) {
+ mHandle.setTouching(false, true /* animate */);
+ fling(position, velocity, avoidDismissStart, logMetrics);
+ mWindowManager.setSlippery(true);
+ releaseBackground();
+ }
+
+ private void stopDragging(int position, SnapTarget target, long duration,
+ Interpolator interpolator) {
+ stopDragging(position, target, duration, 0 /* startDelay*/, 0 /* endDelay */, interpolator);
+ }
+
+ private void stopDragging(int position, SnapTarget target, long duration,
+ Interpolator interpolator, long endDelay) {
+ stopDragging(position, target, duration, 0 /* startDelay*/, endDelay, interpolator);
+ }
+
+ private void stopDragging(int position, SnapTarget target, long duration, long startDelay,
+ long endDelay, Interpolator interpolator) {
+ mHandle.setTouching(false, true /* animate */);
+ flingTo(position, target, duration, startDelay, endDelay, interpolator);
+ mWindowManager.setSlippery(true);
+ releaseBackground();
+ }
+
+ private void stopDragging() {
+ mHandle.setTouching(false, true /* animate */);
+ mWindowManager.setSlippery(true);
+ releaseBackground();
+ }
+
+ private void updateDockSide() {
+ mDockSide = mSplitLayout.getPrimarySplitSide();
+ mMinimizedShadow.setDockSide(mDockSide);
+ }
+
+ public DividerSnapAlgorithm getSnapAlgorithm() {
+ return mDockedStackMinimized ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
+ : mSplitLayout.getSnapAlgorithm();
+ }
+
+ public int getCurrentPosition() {
+ return isHorizontalDivision() ? mDividerPositionY : mDividerPositionX;
+ }
+
+ public boolean isMinimized() {
+ return mDockedStackMinimized;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ convertToScreenCoordinates(event);
+ final int action = event.getAction() & MotionEvent.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker = VelocityTracker.obtain();
+ mVelocityTracker.addMovement(event);
+ mStartX = (int) event.getX();
+ mStartY = (int) event.getY();
+ boolean result = startDragging(true /* animate */, true /* touching */);
+ if (!result) {
+
+ // Weren't able to start dragging successfully, so cancel it again.
+ stopDragging();
+ }
+ mStartPosition = getCurrentPosition();
+ mMoving = false;
+ return result;
+ case MotionEvent.ACTION_MOVE:
+ mVelocityTracker.addMovement(event);
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ boolean exceededTouchSlop =
+ isHorizontalDivision() && Math.abs(y - mStartY) > mTouchSlop
+ || (!isHorizontalDivision() && Math.abs(x - mStartX) > mTouchSlop);
+ if (!mMoving && exceededTouchSlop) {
+ mStartX = x;
+ mStartY = y;
+ mMoving = true;
+ }
+ if (mMoving && mDockSide != WindowManager.DOCKED_INVALID) {
+ SnapTarget snapTarget = getSnapAlgorithm().calculateSnapTarget(
+ mStartPosition, 0 /* velocity */, false /* hardDismiss */);
+ resizeStackSurfaces(calculatePosition(x, y), mStartPosition, snapTarget,
+ null /* transaction */);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mVelocityTracker.addMovement(event);
+
+ x = (int) event.getRawX();
+ y = (int) event.getRawY();
+
+ mVelocityTracker.computeCurrentVelocity(1000);
+ int position = calculatePosition(x, y);
+ stopDragging(position, isHorizontalDivision() ? mVelocityTracker.getYVelocity()
+ : mVelocityTracker.getXVelocity(), false /* avoidDismissStart */,
+ true /* log */);
+ mMoving = false;
+ break;
+ }
+ return true;
+ }
+
+ private void logResizeEvent(SnapTarget snapTarget) {
+ if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissStartTarget()) {
+ MetricsLogger.action(
+ mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideTopLeft(mDockSide)
+ ? LOG_VALUE_UNDOCK_MAX_OTHER
+ : LOG_VALUE_UNDOCK_MAX_DOCKED);
+ } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissEndTarget()) {
+ MetricsLogger.action(
+ mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideBottomRight(mDockSide)
+ ? LOG_VALUE_UNDOCK_MAX_OTHER
+ : LOG_VALUE_UNDOCK_MAX_DOCKED);
+ } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getMiddleTarget()) {
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
+ LOG_VALUE_RESIZE_50_50);
+ } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getFirstSplitTarget()) {
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
+ dockSideTopLeft(mDockSide)
+ ? LOG_VALUE_RESIZE_DOCKED_SMALLER
+ : LOG_VALUE_RESIZE_DOCKED_LARGER);
+ } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getLastSplitTarget()) {
+ MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
+ dockSideTopLeft(mDockSide)
+ ? LOG_VALUE_RESIZE_DOCKED_LARGER
+ : LOG_VALUE_RESIZE_DOCKED_SMALLER);
+ }
+ }
+
+ private void convertToScreenCoordinates(MotionEvent event) {
+ event.setLocation(event.getRawX(), event.getRawY());
+ }
+
+ private void fling(int position, float velocity, boolean avoidDismissStart,
+ boolean logMetrics) {
+ DividerSnapAlgorithm currentSnapAlgorithm = getSnapAlgorithm();
+ SnapTarget snapTarget = currentSnapAlgorithm.calculateSnapTarget(position, velocity);
+ if (avoidDismissStart && snapTarget == currentSnapAlgorithm.getDismissStartTarget()) {
+ snapTarget = currentSnapAlgorithm.getFirstSplitTarget();
+ }
+ if (logMetrics) {
+ logResizeEvent(snapTarget);
+ }
+ ValueAnimator anim = getFlingAnimator(position, snapTarget, 0 /* endDelay */);
+ mFlingAnimationUtils.apply(anim, position, snapTarget.position, velocity);
+ anim.start();
+ }
+
+ private void flingTo(int position, SnapTarget target, long duration, long startDelay,
+ long endDelay, Interpolator interpolator) {
+ ValueAnimator anim = getFlingAnimator(position, target, endDelay);
+ anim.setDuration(duration);
+ anim.setStartDelay(startDelay);
+ anim.setInterpolator(interpolator);
+ anim.start();
+ }
+
+ private ValueAnimator getFlingAnimator(int position, final SnapTarget snapTarget,
+ final long endDelay) {
+ if (mCurrentAnimator != null) {
+ cancelFlingAnimation();
+ updateDockSide();
+ }
+ if (DEBUG) Slog.d(TAG, "Getting fling " + position + "->" + snapTarget.position);
+ final boolean taskPositionSameAtEnd = snapTarget.flag == SnapTarget.FLAG_NONE;
+ ValueAnimator anim = ValueAnimator.ofInt(position, snapTarget.position);
+ anim.addUpdateListener(animation -> resizeStackSurfaces((int) animation.getAnimatedValue(),
+ taskPositionSameAtEnd && animation.getAnimatedFraction() == 1f
+ ? TASK_POSITION_SAME
+ : snapTarget.taskPosition,
+ snapTarget, null /* transaction */));
+ Consumer<Boolean> endAction = cancelled -> {
+ if (DEBUG) Slog.d(TAG, "End Fling " + cancelled + " min:" + mIsInMinimizeInteraction);
+ final boolean wasMinimizeInteraction = mIsInMinimizeInteraction;
+ // Reset minimized divider position after unminimized state animation finishes.
+ if (!cancelled && !mDockedStackMinimized && mIsInMinimizeInteraction) {
+ mIsInMinimizeInteraction = false;
+ }
+ boolean dismissed = commitSnapFlags(snapTarget);
+ mWindowManagerProxy.setResizing(false);
+ updateDockSide();
+ mCurrentAnimator = null;
+ mEntranceAnimationRunning = false;
+ mExitAnimationRunning = false;
+ if (!dismissed && !wasMinimizeInteraction) {
+ mWindowManagerProxy.applyResizeSplits(snapTarget.position, mSplitLayout);
+ }
+ if (mCallback != null) {
+ mCallback.onDraggingEnd();
+ }
+
+ // Record last snap target the divider moved to
+ if (!mIsInMinimizeInteraction) {
+ // The last snapTarget position can be negative when the last divider position was
+ // offscreen. In that case, save the middle (default) SnapTarget so calculating next
+ // position isn't negative.
+ final SnapTarget saveTarget;
+ if (snapTarget.position < 0) {
+ saveTarget = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
+ } else {
+ saveTarget = snapTarget;
+ }
+ final DividerSnapAlgorithm snapAlgo = mSplitLayout.getSnapAlgorithm();
+ if (saveTarget.position != snapAlgo.getDismissEndTarget().position
+ && saveTarget.position != snapAlgo.getDismissStartTarget().position) {
+ saveSnapTargetBeforeMinimized(saveTarget);
+ }
+ }
+ notifySplitScreenBoundsChanged();
+ };
+ anim.addListener(new AnimatorListenerAdapter() {
+
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ long delay = 0;
+ if (endDelay != 0) {
+ delay = endDelay;
+ } else if (mCancelled) {
+ delay = 0;
+ }
+ if (delay == 0) {
+ endAction.accept(mCancelled);
+ } else {
+ final Boolean cancelled = mCancelled;
+ if (DEBUG) Slog.d(TAG, "Posting endFling " + cancelled + " d:" + delay + "ms");
+ mHandler.postDelayed(() -> endAction.accept(cancelled), delay);
+ }
+ }
+ });
+ anim.setAnimationHandler(mAnimationHandler);
+ mCurrentAnimator = anim;
+ return anim;
+ }
+
+ private void notifySplitScreenBoundsChanged() {
+ if (mSplitLayout.mPrimary == null || mSplitLayout.mSecondary == null) {
+ return;
+ }
+ mOtherTaskRect.set(mSplitLayout.mSecondary);
+
+ mTmpRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), mHandle.getBottom());
+ if (isHorizontalDivision()) {
+ mTmpRect.offsetTo(0, mDividerPositionY);
+ } else {
+ mTmpRect.offsetTo(mDividerPositionX, 0);
+ }
+ mWindowManagerProxy.setTouchRegion(mTmpRect);
+
+ mTmpRect.set(mSplitLayout.mDisplayLayout.stableInsets());
+ switch (mSplitLayout.getPrimarySplitSide()) {
+ case WindowManager.DOCKED_LEFT:
+ mTmpRect.left = 0;
+ break;
+ case WindowManager.DOCKED_RIGHT:
+ mTmpRect.right = 0;
+ break;
+ case WindowManager.DOCKED_TOP:
+ mTmpRect.top = 0;
+ break;
+ }
+ mSplitScreenController.notifyBoundsChanged(mOtherTaskRect, mTmpRect);
+ }
+
+ private void cancelFlingAnimation() {
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.cancel();
+ }
+ }
+
+ private boolean commitSnapFlags(SnapTarget target) {
+ if (target.flag == SnapTarget.FLAG_NONE) {
+ return false;
+ }
+ final boolean dismissOrMaximize;
+ if (target.flag == SnapTarget.FLAG_DISMISS_START) {
+ dismissOrMaximize = mDockSide == WindowManager.DOCKED_LEFT
+ || mDockSide == WindowManager.DOCKED_TOP;
+ } else {
+ dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT
+ || mDockSide == WindowManager.DOCKED_BOTTOM;
+ }
+ mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, mSplitLayout, dismissOrMaximize);
+ Transaction t = mTiles.getTransaction();
+ setResizeDimLayer(t, true /* primary */, 0f);
+ setResizeDimLayer(t, false /* primary */, 0f);
+ t.apply();
+ mTiles.releaseTransaction(t);
+ return true;
+ }
+
+ private void liftBackground() {
+ if (mBackgroundLifted) {
+ return;
+ }
+ if (isHorizontalDivision()) {
+ mBackground.animate().scaleY(1.4f);
+ } else {
+ mBackground.animate().scaleX(1.4f);
+ }
+ mBackground.animate()
+ .setInterpolator(Interpolators.TOUCH_RESPONSE)
+ .setDuration(TOUCH_ANIMATION_DURATION)
+ .translationZ(mTouchElevation)
+ .start();
+
+ // Lift handle as well so it doesn't get behind the background, even though it doesn't
+ // cast shadow.
+ mHandle.animate()
+ .setInterpolator(Interpolators.TOUCH_RESPONSE)
+ .setDuration(TOUCH_ANIMATION_DURATION)
+ .translationZ(mTouchElevation)
+ .start();
+ mBackgroundLifted = true;
+ }
+
+ private void releaseBackground() {
+ if (!mBackgroundLifted) {
+ return;
+ }
+ mBackground.animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+ .translationZ(0)
+ .scaleX(1f)
+ .scaleY(1f)
+ .start();
+ mHandle.animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+ .translationZ(0)
+ .start();
+ mBackgroundLifted = false;
+ }
+
+ private void initializeSurfaceState() {
+ int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+ // Recalculate the split-layout's internal tile bounds
+ mSplitLayout.resizeSplits(midPos);
+ Transaction t = mTiles.getTransaction();
+ if (mDockedStackMinimized) {
+ int position = mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
+ .getMiddleTarget().position;
+ calculateBoundsForPosition(position, mDockSide, mDockedRect);
+ calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
+ mOtherRect);
+ mDividerPositionX = mDividerPositionY = position;
+ resizeSplitSurfaces(t, mDockedRect, mSplitLayout.mPrimary,
+ mOtherRect, mSplitLayout.mSecondary);
+ } else {
+ resizeSplitSurfaces(t, mSplitLayout.mPrimary, null,
+ mSplitLayout.mSecondary, null);
+ }
+ setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
+ setResizeDimLayer(t, false /* secondary */, 0.f /* alpha */);
+ t.apply();
+ mTiles.releaseTransaction(t);
+
+ // Get the actually-visible bar dimensions (relative to full window). This is a thin
+ // bar going through the center.
+ final Rect dividerBar = isHorizontalDivision()
+ ? new Rect(0, mDividerInsets, mSplitLayout.mDisplayLayout.width(),
+ mDividerInsets + mDividerSize)
+ : new Rect(mDividerInsets, 0, mDividerInsets + mDividerSize,
+ mSplitLayout.mDisplayLayout.height());
+ final Region touchRegion = new Region(dividerBar);
+ // Add in the "draggable" portion. While not visible, this is an expanded area that the
+ // user can interact with.
+ touchRegion.union(new Rect(mHandle.getLeft(), mHandle.getTop(),
+ mHandle.getRight(), mHandle.getBottom()));
+ mWindowManager.setTouchRegion(touchRegion);
+ }
+
+ void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable,
+ Transaction t) {
+ mHomeStackResizable = isHomeStackResizable;
+ updateDockSide();
+ if (!minimized) {
+ resetBackground();
+ }
+ mMinimizedShadow.setAlpha(minimized ? 1f : 0f);
+ if (mDockedStackMinimized != minimized) {
+ mDockedStackMinimized = minimized;
+ if (mSplitLayout.mDisplayLayout.rotation() != mDefaultDisplay.getRotation()) {
+ // Splitscreen to minimize is about to starts after rotating landscape to seascape,
+ // update display info and snap algorithm targets
+ repositionSnapTargetBeforeMinimized();
+ }
+ if (mIsInMinimizeInteraction != minimized || mCurrentAnimator != null) {
+ cancelFlingAnimation();
+ if (minimized) {
+ // Relayout to recalculate the divider shadow when minimizing
+ requestLayout();
+ mIsInMinimizeInteraction = true;
+ resizeStackSurfaces(mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
+ .getMiddleTarget(), t);
+ } else {
+ resizeStackSurfaces(mSnapTargetBeforeMinimized, t);
+ mIsInMinimizeInteraction = false;
+ }
+ }
+ }
+ }
+
+ void enterSplitMode(boolean isHomeStackResizable) {
+ setHidden(false);
+
+ SnapTarget miniMid =
+ mSplitLayout.getMinimizedSnapAlgorithm(isHomeStackResizable).getMiddleTarget();
+ if (mDockedStackMinimized) {
+ mDividerPositionY = mDividerPositionX = miniMid.position;
+ }
+ }
+
+ /**
+ * Tries to grab a surface control from ViewRootImpl. If this isn't available for some reason
+ * (ie. the window isn't ready yet), it will get the surfacecontrol that the WindowlessWM has
+ * assigned to it.
+ */
+ private SurfaceControl getWindowSurfaceControl() {
+ final ViewRootImpl root = getViewRootImpl();
+ if (root == null) {
+ return null;
+ }
+ SurfaceControl out = root.getSurfaceControl();
+ if (out != null && out.isValid()) {
+ return out;
+ }
+ return mWindowManager.mSystemWindows.getViewSurface(this);
+ }
+
+ void exitSplitMode() {
+ // The view is going to be removed right after this function involved, updates the surface
+ // in the current thread instead of posting it to the view's UI thread.
+ final SurfaceControl sc = getWindowSurfaceControl();
+ if (sc == null) {
+ return;
+ }
+ Transaction t = mTiles.getTransaction();
+ t.hide(sc);
+ mImeController.setDimsHidden(t, true);
+ t.apply();
+ mTiles.releaseTransaction(t);
+
+ // Reset tile bounds
+ int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+ mWindowManagerProxy.applyResizeSplits(midPos, mSplitLayout);
+ }
+
+ void setMinimizedDockStack(boolean minimized, long animDuration,
+ boolean isHomeStackResizable) {
+ if (DEBUG) Slog.d(TAG, "setMinDock: " + mDockedStackMinimized + "->" + minimized);
+ mHomeStackResizable = isHomeStackResizable;
+ updateDockSide();
+ if (mDockedStackMinimized != minimized) {
+ mIsInMinimizeInteraction = true;
+ mDockedStackMinimized = minimized;
+ stopDragging(minimized
+ ? mSnapTargetBeforeMinimized.position
+ : getCurrentPosition(),
+ minimized
+ ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
+ .getMiddleTarget()
+ : mSnapTargetBeforeMinimized,
+ animDuration, Interpolators.FAST_OUT_SLOW_IN, 0);
+ setAdjustedForIme(false, animDuration);
+ }
+ if (!minimized) {
+ mBackground.animate().withEndAction(mResetBackgroundRunnable);
+ }
+ mBackground.animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(animDuration)
+ .start();
+ }
+
+ // Needed to end any currently playing animations when they might compete with other anims
+ // (specifically, IME adjust animation immediately after leaving minimized). Someday maybe
+ // these can be unified, but not today.
+ void finishAnimations() {
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.end();
+ }
+ }
+
+ void setAdjustedForIme(boolean adjustedForIme, long animDuration) {
+ if (mAdjustedForIme == adjustedForIme) {
+ return;
+ }
+ updateDockSide();
+ mHandle.animate()
+ .setInterpolator(IME_ADJUST_INTERPOLATOR)
+ .setDuration(animDuration)
+ .alpha(adjustedForIme ? 0f : 1f)
+ .start();
+ if (mDockSide == WindowManager.DOCKED_TOP) {
+ mBackground.setPivotY(0);
+ mBackground.animate()
+ .scaleY(adjustedForIme ? ADJUSTED_FOR_IME_SCALE : 1f);
+ }
+ if (!adjustedForIme) {
+ mBackground.animate().withEndAction(mResetBackgroundRunnable);
+ }
+ mBackground.animate()
+ .setInterpolator(IME_ADJUST_INTERPOLATOR)
+ .setDuration(animDuration)
+ .start();
+ mAdjustedForIme = adjustedForIme;
+ }
+
+ private void saveSnapTargetBeforeMinimized(SnapTarget target) {
+ mSnapTargetBeforeMinimized = target;
+ mState.mRatioPositionBeforeMinimized = (float) target.position
+ / (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
+ : mSplitLayout.mDisplayLayout.width());
+ }
+
+ private void resetBackground() {
+ mBackground.setPivotX(mBackground.getWidth() / 2);
+ mBackground.setPivotY(mBackground.getHeight() / 2);
+ mBackground.setScaleX(1f);
+ mBackground.setScaleY(1f);
+ mMinimizedShadow.setAlpha(0f);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ }
+
+ private void repositionSnapTargetBeforeMinimized() {
+ int position = (int) (mState.mRatioPositionBeforeMinimized
+ * (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
+ : mSplitLayout.mDisplayLayout.width()));
+
+ // Set the snap target before minimized but do not save until divider is attached and not
+ // minimized because it does not know its minimized state yet.
+ mSnapTargetBeforeMinimized =
+ mSplitLayout.getSnapAlgorithm().calculateNonDismissingSnapTarget(position);
+ }
+
+ private int calculatePosition(int touchX, int touchY) {
+ return isHorizontalDivision() ? calculateYPosition(touchY) : calculateXPosition(touchX);
+ }
+
+ public boolean isHorizontalDivision() {
+ return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
+ }
+
+ private int calculateXPosition(int touchX) {
+ return mStartPosition + touchX - mStartX;
+ }
+
+ private int calculateYPosition(int touchY) {
+ return mStartPosition + touchY - mStartY;
+ }
+
+ private void alignTopLeft(Rect containingRect, Rect rect) {
+ int width = rect.width();
+ int height = rect.height();
+ rect.set(containingRect.left, containingRect.top,
+ containingRect.left + width, containingRect.top + height);
+ }
+
+ private void alignBottomRight(Rect containingRect, Rect rect) {
+ int width = rect.width();
+ int height = rect.height();
+ rect.set(containingRect.right - width, containingRect.bottom - height,
+ containingRect.right, containingRect.bottom);
+ }
+
+ private void calculateBoundsForPosition(int position, int dockSide, Rect outRect) {
+ DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outRect,
+ mSplitLayout.mDisplayLayout.width(), mSplitLayout.mDisplayLayout.height(),
+ mDividerSize);
+ }
+
+ private void resizeStackSurfaces(SnapTarget taskSnapTarget, Transaction t) {
+ resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget, t);
+ }
+
+ void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect otherRect) {
+ resizeSplitSurfaces(t, dockedRect, null, otherRect, null);
+ }
+
+ private void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect dockedTaskRect,
+ Rect otherRect, Rect otherTaskRect) {
+ dockedTaskRect = dockedTaskRect == null ? dockedRect : dockedTaskRect;
+ otherTaskRect = otherTaskRect == null ? otherRect : otherTaskRect;
+
+ mDividerPositionX = mSplitLayout.getPrimarySplitSide() == DOCKED_RIGHT
+ ? otherRect.right : dockedRect.right;
+ mDividerPositionY = dockedRect.bottom;
+
+ if (DEBUG) {
+ Slog.d(TAG, "Resizing split surfaces: " + dockedRect + " " + dockedTaskRect
+ + " " + otherRect + " " + otherTaskRect);
+ }
+
+ t.setPosition(mTiles.mPrimarySurface, dockedTaskRect.left, dockedTaskRect.top);
+ Rect crop = new Rect(dockedRect);
+ crop.offsetTo(-Math.min(dockedTaskRect.left - dockedRect.left, 0),
+ -Math.min(dockedTaskRect.top - dockedRect.top, 0));
+ t.setWindowCrop(mTiles.mPrimarySurface, crop);
+ t.setPosition(mTiles.mSecondarySurface, otherTaskRect.left, otherTaskRect.top);
+ crop.set(otherRect);
+ crop.offsetTo(-(otherTaskRect.left - otherRect.left),
+ -(otherTaskRect.top - otherRect.top));
+ t.setWindowCrop(mTiles.mSecondarySurface, crop);
+ final SurfaceControl dividerCtrl = getWindowSurfaceControl();
+ if (dividerCtrl != null) {
+ if (isHorizontalDivision()) {
+ t.setPosition(dividerCtrl, 0, mDividerPositionY - mDividerInsets);
+ } else {
+ t.setPosition(dividerCtrl, mDividerPositionX - mDividerInsets, 0);
+ }
+ }
+ if (getViewRootImpl() != null) {
+ mHandler.removeCallbacks(mUpdateEmbeddedMatrix);
+ mHandler.post(mUpdateEmbeddedMatrix);
+ }
+ }
+
+ void setResizeDimLayer(Transaction t, boolean primary, float alpha) {
+ SurfaceControl dim = primary ? mTiles.mPrimaryDim : mTiles.mSecondaryDim;
+ if (alpha <= 0.001f) {
+ t.hide(dim);
+ } else {
+ t.setAlpha(dim, alpha);
+ t.show(dim);
+ }
+ }
+
+ void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget,
+ Transaction transaction) {
+ if (mRemoved) {
+ // This divider view has been removed so shouldn't have any additional influence.
+ return;
+ }
+ calculateBoundsForPosition(position, mDockSide, mDockedRect);
+ calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
+ mOtherRect);
+
+ if (mDockedRect.equals(mLastResizeRect) && !mEntranceAnimationRunning) {
+ return;
+ }
+
+ // Make sure shadows are updated
+ if (mBackground.getZ() > 0f) {
+ mBackground.invalidate();
+ }
+
+ final boolean ownTransaction = transaction == null;
+ final Transaction t = ownTransaction ? mTiles.getTransaction() : transaction;
+ mLastResizeRect.set(mDockedRect);
+ if (mIsInMinimizeInteraction) {
+ calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, mDockSide,
+ mDockedTaskRect);
+ calculateBoundsForPosition(mSnapTargetBeforeMinimized.position,
+ DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect);
+
+ // Move a right-docked-app to line up with the divider while dragging it
+ if (mDockSide == DOCKED_RIGHT) {
+ mDockedTaskRect.offset(Math.max(position, -mDividerSize)
+ - mDockedTaskRect.left + mDividerSize, 0);
+ }
+ resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
+ if (ownTransaction) {
+ t.apply();
+ mTiles.releaseTransaction(t);
+ }
+ return;
+ }
+
+ if (mEntranceAnimationRunning && taskPosition != TASK_POSITION_SAME) {
+ calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);
+
+ // Move a docked app if from the right in position with the divider up to insets
+ if (mDockSide == DOCKED_RIGHT) {
+ mDockedTaskRect.offset(Math.max(position, -mDividerSize)
+ - mDockedTaskRect.left + mDividerSize, 0);
+ }
+ calculateBoundsForPosition(taskPosition, DockedDividerUtils.invertDockSide(mDockSide),
+ mOtherTaskRect);
+ resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
+ } else if (mExitAnimationRunning && taskPosition != TASK_POSITION_SAME) {
+ calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);
+ mDockedInsetRect.set(mDockedTaskRect);
+ calculateBoundsForPosition(mExitStartPosition,
+ DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect);
+ mOtherInsetRect.set(mOtherTaskRect);
+ applyExitAnimationParallax(mOtherTaskRect, position);
+
+ // Move a right-docked-app to line up with the divider while dragging it
+ if (mDockSide == DOCKED_RIGHT) {
+ mDockedTaskRect.offset(position + mDividerSize, 0);
+ }
+ resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
+ } else if (taskPosition != TASK_POSITION_SAME) {
+ calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
+ mOtherRect);
+ int dockSideInverted = DockedDividerUtils.invertDockSide(mDockSide);
+ int taskPositionDocked =
+ restrictDismissingTaskPosition(taskPosition, mDockSide, taskSnapTarget);
+ int taskPositionOther =
+ restrictDismissingTaskPosition(taskPosition, dockSideInverted, taskSnapTarget);
+ calculateBoundsForPosition(taskPositionDocked, mDockSide, mDockedTaskRect);
+ calculateBoundsForPosition(taskPositionOther, dockSideInverted, mOtherTaskRect);
+ mTmpRect.set(0, 0, mSplitLayout.mDisplayLayout.width(),
+ mSplitLayout.mDisplayLayout.height());
+ alignTopLeft(mDockedRect, mDockedTaskRect);
+ alignTopLeft(mOtherRect, mOtherTaskRect);
+ mDockedInsetRect.set(mDockedTaskRect);
+ mOtherInsetRect.set(mOtherTaskRect);
+ if (dockSideTopLeft(mDockSide)) {
+ alignTopLeft(mTmpRect, mDockedInsetRect);
+ alignBottomRight(mTmpRect, mOtherInsetRect);
+ } else {
+ alignBottomRight(mTmpRect, mDockedInsetRect);
+ alignTopLeft(mTmpRect, mOtherInsetRect);
+ }
+ applyDismissingParallax(mDockedTaskRect, mDockSide, taskSnapTarget, position,
+ taskPositionDocked);
+ applyDismissingParallax(mOtherTaskRect, dockSideInverted, taskSnapTarget, position,
+ taskPositionOther);
+ resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
+ } else {
+ resizeSplitSurfaces(t, mDockedRect, null, mOtherRect, null);
+ }
+ SnapTarget closestDismissTarget = getSnapAlgorithm().getClosestDismissTarget(position);
+ float dimFraction = getDimFraction(position, closestDismissTarget);
+ setResizeDimLayer(t, isDismissTargetPrimary(closestDismissTarget), dimFraction);
+ if (ownTransaction) {
+ t.apply();
+ mTiles.releaseTransaction(t);
+ }
+ }
+
+ private void applyExitAnimationParallax(Rect taskRect, int position) {
+ if (mDockSide == WindowManager.DOCKED_TOP) {
+ taskRect.offset(0, (int) ((position - mExitStartPosition) * 0.25f));
+ } else if (mDockSide == WindowManager.DOCKED_LEFT) {
+ taskRect.offset((int) ((position - mExitStartPosition) * 0.25f), 0);
+ } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
+ taskRect.offset((int) ((mExitStartPosition - position) * 0.25f), 0);
+ }
+ }
+
+ private float getDimFraction(int position, SnapTarget dismissTarget) {
+ if (mEntranceAnimationRunning) {
+ return 0f;
+ }
+ float fraction = getSnapAlgorithm().calculateDismissingFraction(position);
+ fraction = Math.max(0, Math.min(fraction, 1f));
+ fraction = DIM_INTERPOLATOR.getInterpolation(fraction);
+ return fraction;
+ }
+
+ /**
+ * When the snap target is dismissing one side, make sure that the dismissing side doesn't get
+ * 0 size.
+ */
+ private int restrictDismissingTaskPosition(int taskPosition, int dockSide,
+ SnapTarget snapTarget) {
+ if (snapTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(dockSide)) {
+ return Math.max(mSplitLayout.getSnapAlgorithm().getFirstSplitTarget().position,
+ mStartPosition);
+ } else if (snapTarget.flag == SnapTarget.FLAG_DISMISS_END
+ && dockSideBottomRight(dockSide)) {
+ return Math.min(mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position,
+ mStartPosition);
+ } else {
+ return taskPosition;
+ }
+ }
+
+ /**
+ * Applies a parallax to the task when dismissing.
+ */
+ private void applyDismissingParallax(Rect taskRect, int dockSide, SnapTarget snapTarget,
+ int position, int taskPosition) {
+ float fraction = Math.min(1, Math.max(0,
+ mSplitLayout.getSnapAlgorithm().calculateDismissingFraction(position)));
+ SnapTarget dismissTarget = null;
+ SnapTarget splitTarget = null;
+ int start = 0;
+ if (position <= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
+ && dockSideTopLeft(dockSide)) {
+ dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissStartTarget();
+ splitTarget = mSplitLayout.getSnapAlgorithm().getFirstSplitTarget();
+ start = taskPosition;
+ } else if (position >= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
+ && dockSideBottomRight(dockSide)) {
+ dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissEndTarget();
+ splitTarget = mSplitLayout.getSnapAlgorithm().getLastSplitTarget();
+ start = splitTarget.position;
+ }
+ if (dismissTarget != null && fraction > 0f
+ && isDismissing(splitTarget, position, dockSide)) {
+ fraction = calculateParallaxDismissingFraction(fraction, dockSide);
+ int offsetPosition = (int) (start + fraction
+ * (dismissTarget.position - splitTarget.position));
+ int width = taskRect.width();
+ int height = taskRect.height();
+ switch (dockSide) {
+ case WindowManager.DOCKED_LEFT:
+ taskRect.left = offsetPosition - width;
+ taskRect.right = offsetPosition;
+ break;
+ case WindowManager.DOCKED_RIGHT:
+ taskRect.left = offsetPosition + mDividerSize;
+ taskRect.right = offsetPosition + width + mDividerSize;
+ break;
+ case WindowManager.DOCKED_TOP:
+ taskRect.top = offsetPosition - height;
+ taskRect.bottom = offsetPosition;
+ break;
+ case WindowManager.DOCKED_BOTTOM:
+ taskRect.top = offsetPosition + mDividerSize;
+ taskRect.bottom = offsetPosition + height + mDividerSize;
+ break;
+ }
+ }
+ }
+
+ /**
+ * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
+ * slowing down parallax effect
+ */
+ private static float calculateParallaxDismissingFraction(float fraction, int dockSide) {
+ float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
+
+ // Less parallax at the top, just because.
+ if (dockSide == WindowManager.DOCKED_TOP) {
+ result /= 2f;
+ }
+ return result;
+ }
+
+ private static boolean isDismissing(SnapTarget snapTarget, int position, int dockSide) {
+ if (dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT) {
+ return position < snapTarget.position;
+ } else {
+ return position > snapTarget.position;
+ }
+ }
+
+ private boolean isDismissTargetPrimary(SnapTarget dismissTarget) {
+ return (dismissTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(mDockSide))
+ || (dismissTarget.flag == SnapTarget.FLAG_DISMISS_END
+ && dockSideBottomRight(mDockSide));
+ }
+
+ /**
+ * @return true if and only if {@code dockSide} is top or left
+ */
+ private static boolean dockSideTopLeft(int dockSide) {
+ return dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT;
+ }
+
+ /**
+ * @return true if and only if {@code dockSide} is bottom or right
+ */
+ private static boolean dockSideBottomRight(int dockSide) {
+ return dockSide == WindowManager.DOCKED_BOTTOM || dockSide == WindowManager.DOCKED_RIGHT;
+ }
+
+ @Override
+ public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) {
+ inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ inoutInfo.touchableRegion.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(),
+ mHandle.getBottom());
+ inoutInfo.touchableRegion.op(mBackground.getLeft(), mBackground.getTop(),
+ mBackground.getRight(), mBackground.getBottom(), Op.UNION);
+ }
+
+ void onUndockingTask() {
+ int dockSide = mSplitLayout.getPrimarySplitSide();
+ if (inSplitMode()) {
+ startDragging(false /* animate */, false /* touching */);
+ SnapTarget target = dockSideTopLeft(dockSide)
+ ? mSplitLayout.getSnapAlgorithm().getDismissEndTarget()
+ : mSplitLayout.getSnapAlgorithm().getDismissStartTarget();
+
+ // Don't start immediately - give a little bit time to settle the drag resize change.
+ mExitAnimationRunning = true;
+ mExitStartPosition = getCurrentPosition();
+ stopDragging(mExitStartPosition, target, 336 /* duration */, 100 /* startDelay */,
+ 0 /* endDelay */, Interpolators.FAST_OUT_SLOW_IN);
+ }
+ }
+
+ private int calculatePositionForInsetBounds() {
+ mSplitLayout.mDisplayLayout.getStableBounds(mTmpRect);
+ return DockedDividerUtils.calculatePositionForBounds(mTmpRect, mDockSide, mDividerSize);
+ }
+}
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
new file mode 100644
index 000000000000..0b4e17c27398
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+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.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+
+import android.graphics.PixelFormat;
+import android.graphics.Region;
+import android.os.Binder;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.wm.shell.common.SystemWindows;
+
+/**
+ * Manages the window parameters of the docked stack divider.
+ */
+final class DividerWindowManager {
+
+ private static final String WINDOW_TITLE = "DockedStackDivider";
+
+ final SystemWindows mSystemWindows;
+ private WindowManager.LayoutParams mLp;
+ private View mView;
+
+ DividerWindowManager(SystemWindows systemWindows) {
+ mSystemWindows = systemWindows;
+ }
+
+ /** Add a divider view */
+ void add(View view, int width, int height, int displayId) {
+ mLp = new WindowManager.LayoutParams(
+ width, height, TYPE_DOCK_DIVIDER,
+ FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL
+ | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT);
+ mLp.token = new Binder();
+ mLp.setTitle(WINDOW_TITLE);
+ mLp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION;
+ mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ mSystemWindows.addView(view, mLp, displayId, TYPE_DOCK_DIVIDER);
+ mView = view;
+ }
+
+ void remove() {
+ if (mView != null) {
+ mSystemWindows.removeView(mView);
+ }
+ mView = null;
+ }
+
+ void setSlippery(boolean slippery) {
+ boolean changed = false;
+ if (slippery && (mLp.flags & FLAG_SLIPPERY) == 0) {
+ mLp.flags |= FLAG_SLIPPERY;
+ changed = true;
+ } else if (!slippery && (mLp.flags & FLAG_SLIPPERY) != 0) {
+ mLp.flags &= ~FLAG_SLIPPERY;
+ changed = true;
+ }
+ if (changed) {
+ mSystemWindows.updateViewLayout(mView, mLp);
+ }
+ }
+
+ void setTouchable(boolean touchable) {
+ if (mView == null) {
+ return;
+ }
+ boolean changed = false;
+ if (!touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) == 0) {
+ mLp.flags |= FLAG_NOT_TOUCHABLE;
+ changed = true;
+ } else if (touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) != 0) {
+ mLp.flags &= ~FLAG_NOT_TOUCHABLE;
+ changed = true;
+ }
+ if (changed) {
+ mSystemWindows.updateViewLayout(mView, mLp);
+ }
+ }
+
+ /** Sets the touch region to `touchRegion`. Use null to unset.*/
+ void setTouchRegion(Region touchRegion) {
+ if (mView == null) {
+ return;
+ }
+ mSystemWindows.setTouchableRegion(mView, touchRegion);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java
new file mode 100644
index 000000000000..7a1633530148
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
+import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Translucent activity that gets started on top of a task in multi-window to inform the user that
+ * we forced the activity below to be resizable.
+ */
+public class ForcedResizableInfoActivity extends Activity implements OnTouchListener {
+
+ public static final String EXTRA_FORCED_RESIZEABLE_REASON = "extra_forced_resizeable_reason";
+
+ private static final long DISMISS_DELAY = 2500;
+
+ private final Runnable mFinishRunnable = new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ };
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.forced_resizable_activity);
+ TextView tv = findViewById(com.android.internal.R.id.message);
+ int reason = getIntent().getIntExtra(EXTRA_FORCED_RESIZEABLE_REASON, -1);
+ String text;
+ switch (reason) {
+ case FORCED_RESIZEABLE_REASON_SPLIT_SCREEN:
+ text = getString(R.string.dock_forced_resizable);
+ break;
+ case FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY:
+ text = getString(R.string.forced_resizable_secondary_display);
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected forced resizeable reason: "
+ + reason);
+ }
+ tv.setText(text);
+ getWindow().setTitle(text);
+ getWindow().getDecorView().setOnTouchListener(this);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ getWindow().getDecorView().postDelayed(mFinishRunnable, DISMISS_DELAY);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ finish();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ finish();
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ finish();
+ return true;
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ overridePendingTransition(0, R.anim.forced_resizable_exit);
+ }
+
+ @Override
+ public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
+ // Do nothing
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java
new file mode 100644
index 000000000000..1ef142dacb9e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+
+import static com.android.wm.shell.splitscreen.ForcedResizableInfoActivity.EXTRA_FORCED_RESIZEABLE_REASON;
+
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.widget.Toast;
+
+import com.android.wm.shell.R;
+
+import java.util.function.Consumer;
+
+/**
+ * Controller that decides when to show the {@link ForcedResizableInfoActivity}.
+ */
+final class ForcedResizableInfoActivityController implements DividerView.DividerCallbacks {
+
+ private static final String SELF_PACKAGE_NAME = "com.android.systemui";
+
+ private static final int TIMEOUT = 1000;
+ private final Context mContext;
+ private final Handler mHandler = new Handler();
+ private final ArraySet<PendingTaskRecord> mPendingTasks = new ArraySet<>();
+ private final ArraySet<String> mPackagesShownInSession = new ArraySet<>();
+ private boolean mDividerDragging;
+
+ private final Runnable mTimeoutRunnable = this::showPending;
+
+ private final Consumer<Boolean> mDockedStackExistsListener = exists -> {
+ if (!exists) {
+ mPackagesShownInSession.clear();
+ }
+ };
+
+ /** Record of force resized task that's pending to be handled. */
+ private class PendingTaskRecord {
+ int mTaskId;
+ /**
+ * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SPLIT_SCREEN} or
+ * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY}
+ */
+ int mReason;
+
+ PendingTaskRecord(int taskId, int reason) {
+ this.mTaskId = taskId;
+ this.mReason = reason;
+ }
+ }
+
+ ForcedResizableInfoActivityController(Context context,
+ SplitScreenController splitScreenController) {
+ mContext = context;
+ splitScreenController.registerInSplitScreenListener(mDockedStackExistsListener);
+ }
+
+ @Override
+ public void onDraggingStart() {
+ mDividerDragging = true;
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ }
+
+ @Override
+ public void onDraggingEnd() {
+ mDividerDragging = false;
+ showPending();
+ }
+
+ void onAppTransitionFinished() {
+ if (!mDividerDragging) {
+ showPending();
+ }
+ }
+
+ void activityForcedResizable(String packageName, int taskId, int reason) {
+ if (debounce(packageName)) {
+ return;
+ }
+ mPendingTasks.add(new PendingTaskRecord(taskId, reason));
+ postTimeout();
+ }
+
+ void activityDismissingSplitScreen() {
+ Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ void activityLaunchOnSecondaryDisplayFailed() {
+ Toast.makeText(mContext, R.string.activity_launch_on_secondary_display_failed_text,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ private void showPending() {
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ for (int i = mPendingTasks.size() - 1; i >= 0; i--) {
+ PendingTaskRecord pendingRecord = mPendingTasks.valueAt(i);
+ Intent intent = new Intent(mContext, ForcedResizableInfoActivity.class);
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchTaskId(pendingRecord.mTaskId);
+ // Set as task overlay and allow to resume, so that when an app enters split-screen and
+ // becomes paused, the overlay will still be shown.
+ options.setTaskOverlay(true, true /* canResume */);
+ intent.putExtra(EXTRA_FORCED_RESIZEABLE_REASON, pendingRecord.mReason);
+ mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
+ }
+ mPendingTasks.clear();
+ }
+
+ private void postTimeout() {
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ mHandler.postDelayed(mTimeoutRunnable, TIMEOUT);
+ }
+
+ private boolean debounce(String packageName) {
+ if (packageName == null) {
+ return false;
+ }
+
+ // We launch ForcedResizableInfoActivity into a task that was forced resizable, so that
+ // triggers another notification. So ignore our own activity.
+ if (SELF_PACKAGE_NAME.equals(packageName)) {
+ return true;
+ }
+ boolean debounce = mPackagesShownInSession.contains(packageName);
+ mPackagesShownInSession.add(packageName);
+ return debounce;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java
new file mode 100644
index 000000000000..06f4ef109193
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.wm.shell.R;
+
+/**
+ * Shadow for the minimized dock state on homescreen.
+ */
+public class MinimizedDockShadow extends View {
+
+ private final Paint mShadowPaint = new Paint();
+
+ private int mDockSide = WindowManager.DOCKED_INVALID;
+
+ public MinimizedDockShadow(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ void setDockSide(int dockSide) {
+ if (dockSide != mDockSide) {
+ mDockSide = dockSide;
+ updatePaint(getLeft(), getTop(), getRight(), getBottom());
+ invalidate();
+ }
+ }
+
+ private void updatePaint(int left, int top, int right, int bottom) {
+ int startColor = mContext.getResources().getColor(
+ R.color.minimize_dock_shadow_start, null);
+ int endColor = mContext.getResources().getColor(
+ R.color.minimize_dock_shadow_end, null);
+ final int middleColor = Color.argb(
+ (Color.alpha(startColor) + Color.alpha(endColor)) / 2, 0, 0, 0);
+ final int quarter = Color.argb(
+ (int) (Color.alpha(startColor) * 0.25f + Color.alpha(endColor) * 0.75f),
+ 0, 0, 0);
+ if (mDockSide == WindowManager.DOCKED_TOP) {
+ mShadowPaint.setShader(new LinearGradient(
+ 0, 0, 0, bottom - top,
+ new int[] { startColor, middleColor, quarter, endColor },
+ new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP));
+ } else if (mDockSide == WindowManager.DOCKED_LEFT) {
+ mShadowPaint.setShader(new LinearGradient(
+ 0, 0, right - left, 0,
+ new int[] { startColor, middleColor, quarter, endColor },
+ new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP));
+ } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
+ mShadowPaint.setShader(new LinearGradient(
+ right - left, 0, 0, 0,
+ new int[] { startColor, middleColor, quarter, endColor },
+ new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed) {
+ updatePaint(left, top, right, bottom);
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawRect(0, 0, getWidth(), getHeight(), mShadowPaint);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java
new file mode 100644
index 000000000000..3c0f93906795
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java
@@ -0,0 +1,313 @@
+/*
+ * 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.splitscreen;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.util.TypedValue;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.internal.policy.DockedDividerUtils;
+import com.android.wm.shell.common.DisplayLayout;
+
+/**
+ * Handles split-screen related internal display layout. In general, this represents the
+ * WM-facing understanding of the splits.
+ */
+public class SplitDisplayLayout {
+ /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to
+ * restrict IME adjustment so that a min portion of top stack remains visible.*/
+ private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f;
+
+ private static final int DIVIDER_WIDTH_INACTIVE_DP = 4;
+
+ SplitScreenTaskOrganizer mTiles;
+ DisplayLayout mDisplayLayout;
+ Context mContext;
+
+ // Lazy stuff
+ boolean mResourcesValid = false;
+ int mDividerSize;
+ int mDividerSizeInactive;
+ private DividerSnapAlgorithm mSnapAlgorithm = null;
+ private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null;
+ Rect mPrimary = null;
+ Rect mSecondary = null;
+ Rect mAdjustedPrimary = null;
+ Rect mAdjustedSecondary = null;
+
+ public SplitDisplayLayout(Context ctx, DisplayLayout dl, SplitScreenTaskOrganizer taskTiles) {
+ mTiles = taskTiles;
+ mDisplayLayout = dl;
+ mContext = ctx;
+ }
+
+ void rotateTo(int newRotation) {
+ mDisplayLayout.rotateTo(mContext.getResources(), newRotation);
+ final Configuration config = new Configuration();
+ config.unset();
+ config.orientation = mDisplayLayout.getOrientation();
+ Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
+ tmpRect.inset(mDisplayLayout.nonDecorInsets());
+ config.windowConfiguration.setAppBounds(tmpRect);
+ tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
+ tmpRect.inset(mDisplayLayout.stableInsets());
+ config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density());
+ config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density());
+ mContext = mContext.createConfigurationContext(config);
+ mSnapAlgorithm = null;
+ mMinimizedSnapAlgorithm = null;
+ mResourcesValid = false;
+ }
+
+ private void updateResources() {
+ if (mResourcesValid) {
+ return;
+ }
+ mResourcesValid = true;
+ Resources res = mContext.getResources();
+ mDividerSize = DockedDividerUtils.getDividerSize(res,
+ DockedDividerUtils.getDividerInsets(res));
+ mDividerSizeInactive = (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics());
+ }
+
+ int getPrimarySplitSide() {
+ switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) {
+ case DisplayLayout.NAV_BAR_BOTTOM:
+ return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP;
+ case DisplayLayout.NAV_BAR_LEFT:
+ return DOCKED_RIGHT;
+ case DisplayLayout.NAV_BAR_RIGHT:
+ return DOCKED_LEFT;
+ default:
+ return DOCKED_INVALID;
+ }
+ }
+
+ DividerSnapAlgorithm getSnapAlgorithm() {
+ if (mSnapAlgorithm == null) {
+ updateResources();
+ boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
+ mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
+ mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
+ isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide());
+ }
+ return mSnapAlgorithm;
+ }
+
+ DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) {
+ if (mMinimizedSnapAlgorithm == null) {
+ updateResources();
+ boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
+ mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
+ mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
+ isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(),
+ true /* isMinimized */, homeStackResizable);
+ }
+ return mMinimizedSnapAlgorithm;
+ }
+
+ void resizeSplits(int position) {
+ mPrimary = mPrimary == null ? new Rect() : mPrimary;
+ mSecondary = mSecondary == null ? new Rect() : mSecondary;
+ calcSplitBounds(position, mPrimary, mSecondary);
+ }
+
+ void resizeSplits(int position, WindowContainerTransaction t) {
+ resizeSplits(position);
+ t.setBounds(mTiles.mPrimary.token, mPrimary);
+ t.setBounds(mTiles.mSecondary.token, mSecondary);
+
+ t.setSmallestScreenWidthDp(mTiles.mPrimary.token,
+ getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary));
+ t.setSmallestScreenWidthDp(mTiles.mSecondary.token,
+ getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary));
+ }
+
+ void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) {
+ int dockSide = getPrimarySplitSide();
+ DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary,
+ mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
+
+ DockedDividerUtils.calculateBoundsForPosition(position,
+ DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(),
+ mDisplayLayout.height(), mDividerSize);
+ }
+
+ Rect calcResizableMinimizedHomeStackBounds() {
+ DividerSnapAlgorithm.SnapTarget miniMid =
+ getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget();
+ Rect homeBounds = new Rect();
+ DockedDividerUtils.calculateBoundsForPosition(miniMid.position,
+ DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds,
+ mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
+ return homeBounds;
+ }
+
+ /**
+ * Updates the adjustment depending on it's current state.
+ */
+ void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) {
+ adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize,
+ mDividerSizeInactive, mPrimary, mSecondary);
+ }
+
+ /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */
+ private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop,
+ int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) {
+ if (mAdjustedPrimary == null) {
+ mAdjustedPrimary = new Rect();
+ mAdjustedSecondary = new Rect();
+ }
+
+ final Rect displayStableRect = new Rect();
+ dl.getStableBounds(displayStableRect);
+
+ final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop);
+ final int currDividerWidth =
+ (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction));
+
+ // Calculate the highest we can move the bottom of the top stack to keep 30% visible.
+ final int minTopStackBottom = displayStableRect.top
+ + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN);
+ // Based on that, calculate the maximum amount we'll allow the ime to shift things.
+ final int maxOffset = mPrimary.bottom - minTopStackBottom;
+ // Calculate how much we would shift things without limits (basically the height of ime).
+ final int desiredOffset = hiddenTop - shownTop;
+ // Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints.
+ // We want an effect where the adjustment only occurs during the "highest" portion of the
+ // ime animation. This is done by shifting the adjustment values by the difference in
+ // offsets (effectively playing the whole adjustment animation some fixed amount of pixels
+ // below the ime top).
+ final int topCorrection = Math.max(0, desiredOffset - maxOffset);
+ final int adjustedTop = currImeTop + topCorrection;
+ // The actual yOffset is the distance between adjustedTop and the bottom of the display.
+ // Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only
+ // see adjustment upward.
+ final int yOffset = Math.max(0, dl.height() - adjustedTop);
+
+ // TOP
+ // Reduce the offset by an additional small amount to squish the divider bar.
+ mAdjustedPrimary.set(primaryBounds);
+ mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth));
+
+ // BOTTOM
+ mAdjustedSecondary.set(secondaryBounds);
+ mAdjustedSecondary.offset(0, -yOffset);
+ }
+
+ static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl,
+ Rect bounds) {
+ int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(),
+ DockedDividerUtils.getDividerInsets(context.getResources()));
+
+ int minWidth = Integer.MAX_VALUE;
+
+ // Go through all screen orientations and find the orientation in which the task has the
+ // smallest width.
+ Rect tmpRect = new Rect();
+ Rect rotatedDisplayRect = new Rect();
+ Rect displayRect = new Rect(0, 0, dl.width(), dl.height());
+
+ DisplayLayout tmpDL = new DisplayLayout();
+ for (int rotation = 0; rotation < 4; rotation++) {
+ tmpDL.set(dl);
+ tmpDL.rotateTo(context.getResources(), rotation);
+ DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize);
+
+ tmpRect.set(bounds);
+ DisplayLayout.rotateBounds(tmpRect, displayRect, rotation - dl.rotation());
+ rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height());
+ final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect,
+ tmpDL.getOrientation());
+ final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide,
+ dividerSize);
+
+ final int snappedPosition =
+ snap.calculateNonDismissingSnapTarget(position).position;
+ DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect,
+ tmpDL.width(), tmpDL.height(), dividerSize);
+ Rect insettedDisplay = new Rect(rotatedDisplayRect);
+ insettedDisplay.inset(tmpDL.stableInsets());
+ tmpRect.intersect(insettedDisplay);
+ minWidth = Math.min(tmpRect.width(), minWidth);
+ }
+ return (int) (minWidth / dl.density());
+ }
+
+ static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl,
+ int dividerSize) {
+ final Configuration config = new Configuration();
+ config.unset();
+ config.orientation = dl.getOrientation();
+ Rect tmpRect = new Rect(0, 0, dl.width(), dl.height());
+ tmpRect.inset(dl.nonDecorInsets());
+ config.windowConfiguration.setAppBounds(tmpRect);
+ tmpRect.set(0, 0, dl.width(), dl.height());
+ tmpRect.inset(dl.stableInsets());
+ config.screenWidthDp = (int) (tmpRect.width() / dl.density());
+ config.screenHeightDp = (int) (tmpRect.height() / dl.density());
+ final Context rotationContext = context.createConfigurationContext(config);
+ return new DividerSnapAlgorithm(
+ rotationContext.getResources(), dl.width(), dl.height(), dividerSize,
+ config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets());
+ }
+
+ /**
+ * Get the current primary-split side. Determined by its location of {@param bounds} within
+ * {@param displayRect} but if both are the same, it will try to dock to each side and determine
+ * if allowed in its respected {@param orientation}.
+ *
+ * @param bounds bounds of the primary split task to get which side is docked
+ * @param displayRect bounds of the display that contains the primary split task
+ * @param orientation the origination of device
+ * @return current primary-split side
+ */
+ static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) {
+ if (orientation == ORIENTATION_PORTRAIT) {
+ // Portrait mode, docked either at the top or the bottom.
+ final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top);
+ if (diff < 0) {
+ return DOCKED_BOTTOM;
+ } else {
+ // Top is default
+ return DOCKED_TOP;
+ }
+ } else if (orientation == ORIENTATION_LANDSCAPE) {
+ // Landscape mode, docked either on the left or on the right.
+ final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left);
+ if (diff < 0) {
+ return DOCKED_RIGHT;
+ }
+ return DOCKED_LEFT;
+ }
+ return DOCKED_INVALID;
+ }
+}
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
new file mode 100644
index 000000000000..985dff20ad32
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -0,0 +1,86 @@
+/*
+ * 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.splitscreen;
+
+import android.graphics.Rect;
+import android.window.WindowContainerToken;
+
+import java.io.PrintWriter;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Interface to engage split screen feature.
+ */
+public interface SplitScreen {
+ /** Returns {@code true} if split screen is supported on the device. */
+ boolean isSplitScreenSupported();
+
+ /** Called when keyguard showing state changed. */
+ void onKeyguardVisibilityChanged(boolean isShowing);
+
+ /** Returns {@link DividerView}. */
+ DividerView getDividerView();
+
+ /** Returns {@code true} if one of the split screen is in minimized mode. */
+ boolean isMinimized();
+
+ /** Returns {@code true} if the home stack is resizable. */
+ boolean isHomeStackResizable();
+
+ /** Returns {@code true} if the divider is visible. */
+ boolean isDividerVisible();
+
+ /** Switch to minimized state if appropriate. */
+ void setMinimized(boolean minimized);
+
+ /** Called when there's an activity forced resizable. */
+ void onActivityForcedResizable(String packageName, int taskId, int reason);
+
+ /** Called when there's an activity dismissing split screen. */
+ void onActivityDismissingSplitScreen();
+
+ /** Called when there's an activity launch on secondary display failed. */
+ void onActivityLaunchOnSecondaryDisplayFailed();
+
+ /** Called when there's a task undocking. */
+ void onUndockingTask();
+
+ /** Called when app transition finished. */
+ void onAppTransitionFinished();
+
+ /** Dumps current status of Split Screen. */
+ void dump(PrintWriter pw);
+
+ /** Registers listener that gets called whenever the existence of the divider changes. */
+ void registerInSplitScreenListener(Consumer<Boolean> listener);
+
+ /** Registers listener that gets called whenever the split screen bounds changes. */
+ void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener);
+
+ /** @return the container token for the secondary split root task. */
+ WindowContainerToken getSecondaryRoot();
+
+ /**
+ * Splits the primary task if feasible, this is to preserve legacy way to toggle split screen.
+ * Like triggering split screen through long pressing recents app button or through
+ * {@link android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN}.
+ *
+ * @return {@code true} if it successes to split the primary task.
+ */
+ boolean splitPrimaryTask();
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
new file mode 100644
index 000000000000..43e4d62baaf6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -0,0 +1,567 @@
+/*
+ * 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.splitscreen;
+
+import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityTaskManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Toast;
+import android.window.TaskOrganizer;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.TransactionPool;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Controls split screen feature.
+ */
+public class SplitScreenController implements SplitScreen,
+ DisplayController.OnDisplaysChangedListener {
+ static final boolean DEBUG = false;
+
+ private static final String TAG = "SplitScreenCtrl";
+ private static final int DEFAULT_APP_TRANSITION_DURATION = 336;
+
+ private final Context mContext;
+ private final DisplayChangeController.OnDisplayChangingListener mRotationController;
+ private final DisplayController mDisplayController;
+ private final DisplayImeController mImeController;
+ private final DividerImeController mImePositionProcessor;
+ private final DividerState mDividerState = new DividerState();
+ private final ForcedResizableInfoActivityController mForcedResizableController;
+ private final Handler mHandler;
+ private final SplitScreenTaskOrganizer mSplits;
+ private final SystemWindows mSystemWindows;
+ final TransactionPool mTransactionPool;
+ private final WindowManagerProxy mWindowManagerProxy;
+ private final TaskOrganizer mTaskOrganizer;
+
+ private final ArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners =
+ new ArrayList<>();
+ private final ArrayList<WeakReference<BiConsumer<Rect, Rect>>> mBoundsChangedListeners =
+ new ArrayList<>();
+
+
+ private DividerWindowManager mWindowManager;
+ private DividerView mView;
+
+ // Keeps track of real-time split geometry including snap positions and ime adjustments
+ private SplitDisplayLayout mSplitLayout;
+
+ // Transient: this contains the layout calculated for a new rotation requested by WM. This is
+ // kept around so that we can wait for a matching configuration change and then use the exact
+ // layout that we sent back to WM.
+ private SplitDisplayLayout mRotateSplitLayout;
+
+ private boolean mIsKeyguardShowing;
+ private boolean mVisible = false;
+ private boolean mMinimized = false;
+ private boolean mAdjustedForIme = false;
+ private boolean mHomeStackResizable = false;
+
+ public SplitScreenController(Context context,
+ DisplayController displayController, SystemWindows systemWindows,
+ DisplayImeController imeController, Handler handler, TransactionPool transactionPool,
+ ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue) {
+ mContext = context;
+ mDisplayController = displayController;
+ mSystemWindows = systemWindows;
+ mImeController = imeController;
+ mHandler = handler;
+ mForcedResizableController = new ForcedResizableInfoActivityController(context, this);
+ mTransactionPool = transactionPool;
+ mWindowManagerProxy = new WindowManagerProxy(syncQueue, shellTaskOrganizer);
+ mTaskOrganizer = shellTaskOrganizer;
+ mSplits = new SplitScreenTaskOrganizer(this, shellTaskOrganizer);
+ mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mHandler,
+ shellTaskOrganizer);
+ mRotationController =
+ (display, fromRotation, toRotation, wct) -> {
+ if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) {
+ return;
+ }
+ WindowContainerTransaction t = new WindowContainerTransaction();
+ DisplayLayout displayLayout =
+ new DisplayLayout(mDisplayController.getDisplayLayout(display));
+ SplitDisplayLayout sdl =
+ new SplitDisplayLayout(mContext, displayLayout, mSplits);
+ sdl.rotateTo(toRotation);
+ mRotateSplitLayout = sdl;
+ final int position = isDividerVisible()
+ ? (mMinimized ? mView.mSnapTargetBeforeMinimized.position
+ : mView.getCurrentPosition())
+ // snap resets to middle target when not in split-mode
+ : sdl.getSnapAlgorithm().getMiddleTarget().position;
+ DividerSnapAlgorithm snap = sdl.getSnapAlgorithm();
+ final DividerSnapAlgorithm.SnapTarget target =
+ snap.calculateNonDismissingSnapTarget(position);
+ sdl.resizeSplits(target.position, t);
+
+ if (isSplitActive() && mHomeStackResizable) {
+ mWindowManagerProxy
+ .applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t);
+ }
+ if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) {
+ // Because sync transactions are serialized, its possible for an "older"
+ // bounds-change to get applied after a screen rotation. In that case, we
+ // want to actually defer on that rather than apply immediately. Of course,
+ // this means that the bounds may not change until after the rotation so
+ // the user might see some artifacts. This should be rare.
+ Slog.w(TAG, "Screen rotated while other operations were pending, this may"
+ + " result in some graphical artifacts.");
+ } else {
+ wct.merge(t, true /* transfer */);
+ }
+ };
+
+ mWindowManager = new DividerWindowManager(mSystemWindows);
+ mDisplayController.addDisplayWindowListener(this);
+ // Don't initialize the divider or anything until we get the default display.
+ }
+
+ @Override
+ public boolean isSplitScreenSupported() {
+ return mSplits.isSplitScreenSupported();
+ }
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ if (!isSplitActive() || mView == null) {
+ return;
+ }
+ mView.setHidden(showing);
+ if (!showing) {
+ mImePositionProcessor.updateAdjustForIme();
+ }
+ mIsKeyguardShowing = showing;
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId),
+ mDisplayController.getDisplayLayout(displayId), mSplits);
+ mImeController.addPositionProcessor(mImePositionProcessor);
+ mDisplayController.addDisplayChangingController(mRotationController);
+ if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) {
+ removeDivider();
+ return;
+ }
+ try {
+ mSplits.init();
+ // Set starting tile bounds based on middle target
+ final WindowContainerTransaction tct = new WindowContainerTransaction();
+ int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+ mSplitLayout.resizeSplits(midPos, tct);
+ mTaskOrganizer.applyTransaction(tct);
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to register docked stack listener", e);
+ removeDivider();
+ return;
+ }
+ }
+
+ @Override
+ public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+ if (displayId != DEFAULT_DISPLAY || !mSplits.isSplitScreenSupported()) {
+ return;
+ }
+ mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId),
+ mDisplayController.getDisplayLayout(displayId), mSplits);
+ if (mRotateSplitLayout == null) {
+ int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+ final WindowContainerTransaction tct = new WindowContainerTransaction();
+ mSplitLayout.resizeSplits(midPos, tct);
+ mTaskOrganizer.applyTransaction(tct);
+ } else if (mSplitLayout.mDisplayLayout.rotation()
+ == mRotateSplitLayout.mDisplayLayout.rotation()) {
+ mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary);
+ mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary);
+ mRotateSplitLayout = null;
+ }
+ if (isSplitActive()) {
+ update(newConfig);
+ }
+ }
+
+ /** Posts task to handler dealing with divider. */
+ void post(Runnable task) {
+ mHandler.post(task);
+ }
+
+ @Override
+ public DividerView getDividerView() {
+ return mView;
+ }
+
+ @Override
+ public boolean isMinimized() {
+ return mMinimized;
+ }
+
+ @Override
+ public boolean isHomeStackResizable() {
+ return mHomeStackResizable;
+ }
+
+ @Override
+ public boolean isDividerVisible() {
+ return mView != null && mView.getVisibility() == View.VISIBLE;
+ }
+
+ /**
+ * This indicates that at-least one of the splits has content. This differs from
+ * isDividerVisible because the divider is only visible once *everything* is in split mode
+ * while this only cares if some things are (eg. while entering/exiting as well).
+ */
+ private boolean isSplitActive() {
+ return mSplits.mPrimary != null && mSplits.mSecondary != null
+ && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED
+ || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED);
+ }
+
+ private void addDivider(Configuration configuration) {
+ Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId());
+ mView = (DividerView)
+ LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null);
+ DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId());
+ mView.injectDependencies(this, mWindowManager, mDividerState, mForcedResizableController,
+ mSplits, mSplitLayout, mImePositionProcessor, mWindowManagerProxy);
+ mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE);
+ mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */);
+ final int size = dctx.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.docked_stack_divider_thickness);
+ final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE;
+ final int width = landscape ? size : displayLayout.width();
+ final int height = landscape ? displayLayout.height() : size;
+ mWindowManager.add(mView, width, height, mContext.getDisplayId());
+ }
+
+ private void removeDivider() {
+ if (mView != null) {
+ mView.onDividerRemoved();
+ }
+ mWindowManager.remove();
+ }
+
+ private void update(Configuration configuration) {
+ final boolean isDividerHidden = mView != null && mIsKeyguardShowing;
+
+ removeDivider();
+ addDivider(configuration);
+
+ if (mMinimized) {
+ mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */);
+ updateTouchable();
+ }
+ mView.setHidden(isDividerHidden);
+ }
+
+ void onTaskVanished() {
+ mHandler.post(this::removeDivider);
+ }
+
+ private void updateVisibility(final boolean visible) {
+ if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible);
+ if (mVisible != visible) {
+ mVisible = visible;
+ mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+
+ if (visible) {
+ mView.enterSplitMode(mHomeStackResizable);
+ // Update state because animations won't finish.
+ mWindowManagerProxy.runInSync(
+ t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t));
+
+ } else {
+ mView.exitSplitMode();
+ mWindowManagerProxy.runInSync(
+ t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t));
+ }
+ // Notify existence listeners
+ synchronized (mDockedStackExistsListeners) {
+ mDockedStackExistsListeners.removeIf(wf -> {
+ Consumer<Boolean> l = wf.get();
+ if (l != null) l.accept(visible);
+ return l == null;
+ });
+ }
+ }
+ }
+
+ @Override
+ public void setMinimized(final boolean minimized) {
+ if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible);
+ mHandler.post(() -> {
+ if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible);
+ if (!mVisible) {
+ return;
+ }
+ setHomeMinimized(minimized);
+ });
+ }
+
+ private void setHomeMinimized(final boolean minimized) {
+ if (DEBUG) {
+ Slog.d(TAG, "setHomeMinimized min:" + mMinimized + "->" + minimized + " hrsz:"
+ + mHomeStackResizable + " split:" + isDividerVisible());
+ }
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ final boolean minimizedChanged = mMinimized != minimized;
+ // Update minimized state
+ if (minimizedChanged) {
+ mMinimized = minimized;
+ }
+ // Always set this because we could be entering split when mMinimized is already true
+ wct.setFocusable(mSplits.mPrimary.token, !mMinimized);
+
+ // Sync state to DividerView if it exists.
+ if (mView != null) {
+ final int displayId = mView.getDisplay() != null
+ ? mView.getDisplay().getDisplayId() : DEFAULT_DISPLAY;
+ // pause ime here (before updateMinimizedDockedStack)
+ if (mMinimized) {
+ mImePositionProcessor.pause(displayId);
+ }
+ if (minimizedChanged) {
+ // This conflicts with IME adjustment, so only call it when things change.
+ mView.setMinimizedDockStack(minimized, getAnimDuration(), mHomeStackResizable);
+ }
+ if (!mMinimized) {
+ // afterwards so it can end any animations started in view
+ mImePositionProcessor.resume(displayId);
+ }
+ }
+ updateTouchable();
+
+ // If we are only setting focusability, a sync transaction isn't necessary (in fact it
+ // can interrupt other animations), so see if it can be submitted on pending instead.
+ if (!mWindowManagerProxy.queueSyncTransactionIfWaiting(wct)) {
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ }
+
+ void setAdjustedForIme(boolean adjustedForIme) {
+ if (mAdjustedForIme == adjustedForIme) {
+ return;
+ }
+ mAdjustedForIme = adjustedForIme;
+ updateTouchable();
+ }
+
+ private void updateTouchable() {
+ mWindowManager.setTouchable(!mAdjustedForIme);
+ }
+
+ @Override
+ public void onActivityForcedResizable(String packageName, int taskId, int reason) {
+ mForcedResizableController.activityForcedResizable(packageName, taskId, reason);
+ }
+
+ @Override
+ public void onActivityDismissingSplitScreen() {
+ mForcedResizableController.activityDismissingSplitScreen();
+ }
+
+ @Override
+ public void onActivityLaunchOnSecondaryDisplayFailed() {
+ mForcedResizableController.activityLaunchOnSecondaryDisplayFailed();
+ }
+
+ @Override
+ public void onUndockingTask() {
+ if (mView != null) {
+ mView.onUndockingTask();
+ }
+ }
+
+ @Override
+ public void onAppTransitionFinished() {
+ if (mView == null) {
+ return;
+ }
+ mForcedResizableController.onAppTransitionFinished();
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ pw.print(" mVisible="); pw.println(mVisible);
+ pw.print(" mMinimized="); pw.println(mMinimized);
+ pw.print(" mAdjustedForIme="); pw.println(mAdjustedForIme);
+ }
+
+ long getAnimDuration() {
+ float transitionScale = Settings.Global.getFloat(mContext.getContentResolver(),
+ Settings.Global.TRANSITION_ANIMATION_SCALE,
+ mContext.getResources().getFloat(
+ com.android.internal.R.dimen
+ .config_appTransitionAnimationDurationScaleDefault));
+ final long transitionDuration = DEFAULT_APP_TRANSITION_DURATION;
+ return (long) (transitionDuration * transitionScale);
+ }
+
+ @Override
+ public void registerInSplitScreenListener(Consumer<Boolean> listener) {
+ listener.accept(isDividerVisible());
+ synchronized (mDockedStackExistsListeners) {
+ mDockedStackExistsListeners.add(new WeakReference<>(listener));
+ }
+ }
+
+ @Override
+ public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) {
+ synchronized (mBoundsChangedListeners) {
+ mBoundsChangedListeners.add(new WeakReference<>(listener));
+ }
+ }
+
+ @Override
+ public boolean splitPrimaryTask() {
+ try {
+ if (ActivityTaskManager.getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED
+ || isSplitActive()) {
+ return false;
+ }
+
+ // Try fetching the top running task.
+ final List<RunningTaskInfo> runningTasks =
+ ActivityTaskManager.getService().getTasks(1 /* maxNum */);
+ if (runningTasks == null || runningTasks.isEmpty()) {
+ return false;
+ }
+ // Note: The set of running tasks from the system is ordered by recency.
+ final RunningTaskInfo topRunningTask = runningTasks.get(0);
+
+ final int activityType = topRunningTask.configuration.windowConfiguration
+ .getActivityType();
+ if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) {
+ return false;
+ }
+
+ if (!topRunningTask.supportsSplitScreenMultiWindow) {
+ Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text,
+ Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ return ActivityTaskManager.getService().setTaskWindowingModeSplitScreenPrimary(
+ topRunningTask.taskId, true /* onTop */);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /** Notifies the bounds of split screen changed. */
+ void notifyBoundsChanged(Rect secondaryWindowBounds, Rect secondaryWindowInsets) {
+ synchronized (mBoundsChangedListeners) {
+ mBoundsChangedListeners.removeIf(wf -> {
+ BiConsumer<Rect, Rect> l = wf.get();
+ if (l != null) l.accept(secondaryWindowBounds, secondaryWindowInsets);
+ return l == null;
+ });
+ }
+ }
+
+ void startEnterSplit() {
+ update(mDisplayController.getDisplayContext(
+ mContext.getDisplayId()).getResources().getConfiguration());
+ // Set resizable directly here because applyEnterSplit already resizes home stack.
+ mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout);
+ }
+
+ void startDismissSplit() {
+ mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, true /* dismissOrMaximize */);
+ updateVisibility(false /* visible */);
+ mMinimized = false;
+ // Resets divider bar position to undefined, so new divider bar will apply default position
+ // next time entering split mode.
+ mDividerState.mRatioPositionBeforeMinimized = 0;
+ removeDivider();
+ mImePositionProcessor.reset();
+ }
+
+ void ensureMinimizedSplit() {
+ setHomeMinimized(true /* minimized */);
+ if (mView != null && !isDividerVisible()) {
+ // Wasn't in split-mode yet, so enter now.
+ if (DEBUG) {
+ Slog.d(TAG, " entering split mode with minimized=true");
+ }
+ updateVisibility(true /* visible */);
+ }
+ }
+
+ void ensureNormalSplit() {
+ setHomeMinimized(false /* minimized */);
+ if (mView != null && !isDividerVisible()) {
+ // Wasn't in split-mode, so enter now.
+ if (DEBUG) {
+ Slog.d(TAG, " enter split mode unminimized ");
+ }
+ updateVisibility(true /* visible */);
+ }
+ }
+
+ SplitDisplayLayout getSplitLayout() {
+ return mSplitLayout;
+ }
+
+ WindowManagerProxy getWmProxy() {
+ return mWindowManagerProxy;
+ }
+
+ @Override
+ public WindowContainerToken getSecondaryRoot() {
+ if (mSplits == null || mSplits.mSecondary == null) {
+ return null;
+ }
+ return mSplits.mSecondary.token;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java
new file mode 100644
index 000000000000..f763d6d714c4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java
@@ -0,0 +1,249 @@
+/*
+ * 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.splitscreen;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_SPLIT_SCREEN;
+import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Display;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+
+import java.io.PrintWriter;
+
+class SplitScreenTaskOrganizer implements ShellTaskOrganizer.TaskListener {
+ private static final String TAG = "SplitScreenTaskOrg";
+ private static final boolean DEBUG = SplitScreenController.DEBUG;
+
+ private final ShellTaskOrganizer mTaskOrganizer;
+
+ RunningTaskInfo mPrimary;
+ RunningTaskInfo mSecondary;
+ SurfaceControl mPrimarySurface;
+ SurfaceControl mSecondarySurface;
+ SurfaceControl mPrimaryDim;
+ SurfaceControl mSecondaryDim;
+ Rect mHomeBounds = new Rect();
+ final SplitScreenController mSplitScreenController;
+ private boolean mSplitScreenSupported = false;
+
+ final SurfaceSession mSurfaceSession = new SurfaceSession();
+
+ SplitScreenTaskOrganizer(SplitScreenController splitScreenController,
+ ShellTaskOrganizer shellTaskOrganizer) {
+ mSplitScreenController = splitScreenController;
+ mTaskOrganizer = shellTaskOrganizer;
+ mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_SPLIT_SCREEN);
+ }
+
+ void init() throws RemoteException {
+ synchronized (this) {
+ try {
+ mPrimary = mTaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+ mSecondary = mTaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+ } catch (Exception e) {
+ // teardown to prevent callbacks
+ mTaskOrganizer.removeListener(this);
+ throw e;
+ }
+ }
+ }
+
+ boolean isSplitScreenSupported() {
+ return mSplitScreenSupported;
+ }
+
+ SurfaceControl.Transaction getTransaction() {
+ return mSplitScreenController.mTransactionPool.acquire();
+ }
+
+ void releaseTransaction(SurfaceControl.Transaction t) {
+ mSplitScreenController.mTransactionPool.release(t);
+ }
+
+ @Override
+ public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+ synchronized (this) {
+ if (mPrimary == null || mSecondary == null) {
+ Log.w(TAG, "Received onTaskAppeared before creating root tasks " + taskInfo);
+ return;
+ }
+
+ if (taskInfo.token.equals(mPrimary.token)) {
+ mPrimarySurface = leash;
+ } else if (taskInfo.token.equals(mSecondary.token)) {
+ mSecondarySurface = leash;
+ }
+
+ if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) {
+ mSplitScreenSupported = true;
+
+ // Initialize dim surfaces:
+ mPrimaryDim = new SurfaceControl.Builder(mSurfaceSession)
+ .setParent(mPrimarySurface).setColorLayer()
+ .setName("Primary Divider Dim")
+ .setCallsite("SplitScreenTaskOrganizer.onTaskAppeared")
+ .build();
+ mSecondaryDim = new SurfaceControl.Builder(mSurfaceSession)
+ .setParent(mSecondarySurface).setColorLayer()
+ .setName("Secondary Divider Dim")
+ .setCallsite("SplitScreenTaskOrganizer.onTaskAppeared")
+ .build();
+ SurfaceControl.Transaction t = getTransaction();
+ t.setLayer(mPrimaryDim, Integer.MAX_VALUE);
+ t.setColor(mPrimaryDim, new float[]{0f, 0f, 0f});
+ t.setLayer(mSecondaryDim, Integer.MAX_VALUE);
+ t.setColor(mSecondaryDim, new float[]{0f, 0f, 0f});
+ t.apply();
+ releaseTransaction(t);
+ }
+ }
+ }
+
+ @Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ synchronized (this) {
+ final boolean isPrimaryTask = mPrimary != null
+ && taskInfo.token.equals(mPrimary.token);
+ final boolean isSecondaryTask = mSecondary != null
+ && taskInfo.token.equals(mSecondary.token);
+
+ if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) {
+ mSplitScreenSupported = false;
+
+ SurfaceControl.Transaction t = getTransaction();
+ t.remove(mPrimaryDim);
+ t.remove(mSecondaryDim);
+ t.remove(mPrimarySurface);
+ t.remove(mSecondarySurface);
+ t.apply();
+ releaseTransaction(t);
+
+ mSplitScreenController.onTaskVanished();
+ }
+ }
+ }
+
+ @Override
+ public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+ if (taskInfo.displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ mSplitScreenController.post(() -> handleTaskInfoChanged(taskInfo));
+ }
+
+ /**
+ * This is effectively a finite state machine which moves between the various split-screen
+ * presentations based on the contents of the split regions.
+ */
+ private void handleTaskInfoChanged(RunningTaskInfo info) {
+ if (!mSplitScreenSupported) {
+ // This shouldn't happen; but apparently there is a chance that SysUI crashes without
+ // system server receiving binder-death (or maybe it receives binder-death too late?).
+ // In this situation, when sys-ui restarts, the split root-tasks will still exist so
+ // there is a small window of time during init() where WM might send messages here
+ // before init() fails. So, avoid a cycle of crashes by returning early.
+ Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info);
+ return;
+ }
+ final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
+ || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
+ && mSplitScreenController.isHomeStackResizable());
+ final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+ final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+ if (info.token.asBinder() == mPrimary.token.asBinder()) {
+ mPrimary = info;
+ } else if (info.token.asBinder() == mSecondary.token.asBinder()) {
+ mSecondary = info;
+ }
+ final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+ final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+ final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
+ || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
+ && mSplitScreenController.isHomeStackResizable());
+ if (DEBUG) {
+ Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary);
+ }
+ if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty
+ && secondaryImpliedMinimize == secondaryImpliesMinimize) {
+ // No relevant changes
+ return;
+ }
+ if (primaryIsEmpty || secondaryIsEmpty) {
+ // At-least one of the splits is empty which means we are currently transitioning
+ // into or out-of split-screen mode.
+ if (DEBUG) {
+ Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType
+ + " " + mSecondary.topActivityType);
+ }
+ if (mSplitScreenController.isDividerVisible()) {
+ // Was in split-mode, which means we are leaving split, so continue that.
+ // This happens when the stack in the primary-split is dismissed.
+ if (DEBUG) {
+ Log.d(TAG, " was in split, so this means leave it "
+ + mPrimary.topActivityType + " " + mSecondary.topActivityType);
+ }
+ mSplitScreenController.startDismissSplit();
+ } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) {
+ // Wasn't in split-mode (both were empty), but now that the primary split is
+ // populated, we should fully enter split by moving everything else into secondary.
+ // This just tells window-manager to reparent things, the UI will respond
+ // when it gets new task info for the secondary split.
+ if (DEBUG) {
+ Log.d(TAG, " was not in split, but primary is populated, so enter it");
+ }
+ mSplitScreenController.startEnterSplit();
+ }
+ } else if (secondaryImpliesMinimize) {
+ // Both splits are populated but the secondary split has a home/recents stack on top,
+ // so enter minimized mode.
+ mSplitScreenController.ensureMinimizedSplit();
+ } else {
+ // Both splits are populated by normal activities, so make sure we aren't minimized.
+ mSplitScreenController.ensureNormalSplit();
+ }
+ }
+
+ @Override
+ public void dump(@NonNull PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ final String childPrefix = innerPrefix + " ";
+ pw.println(prefix + this);
+ }
+
+ @Override
+ public String toString() {
+ return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_SPLIT_SCREEN);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java
new file mode 100644
index 000000000000..47e7c99d2268
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Display;
+import android.view.SurfaceControl;
+import android.view.WindowManagerGlobal;
+import android.window.TaskOrganizer;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowOrganizer;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Proxy to simplify calls into window manager/activity manager
+ */
+class WindowManagerProxy {
+
+ private static final String TAG = "WindowManagerProxy";
+ private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS};
+
+ @GuardedBy("mDockedRect")
+ private final Rect mDockedRect = new Rect();
+
+ private final Rect mTmpRect1 = new Rect();
+
+ @GuardedBy("mDockedRect")
+ private final Rect mTouchableRegion = new Rect();
+
+ private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+ private final SyncTransactionQueue mSyncTransactionQueue;
+
+ private final Runnable mSetTouchableRegionRunnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ synchronized (mDockedRect) {
+ mTmpRect1.set(mTouchableRegion);
+ }
+ WindowManagerGlobal.getWindowManagerService().setDockedStackDividerTouchRegion(
+ mTmpRect1);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to set touchable region: " + e);
+ }
+ }
+ };
+
+ private final TaskOrganizer mTaskOrganizer;
+
+ WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer) {
+ mSyncTransactionQueue = syncQueue;
+ mTaskOrganizer = taskOrganizer;
+ }
+
+ void dismissOrMaximizeDocked(final SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout,
+ final boolean dismissOrMaximize) {
+ mExecutor.execute(() -> applyDismissSplit(tiles, layout, dismissOrMaximize));
+ }
+
+ public void setResizing(final boolean resizing) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ActivityTaskManager.getService().setSplitScreenResizing(resizing);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error calling setDockedStackResizing: " + e);
+ }
+ }
+ });
+ }
+
+ /** Sets a touch region */
+ public void setTouchRegion(Rect region) {
+ synchronized (mDockedRect) {
+ mTouchableRegion.set(region);
+ }
+ mExecutor.execute(mSetTouchableRegionRunnable);
+ }
+
+ void applyResizeSplits(int position, SplitDisplayLayout splitLayout) {
+ WindowContainerTransaction t = new WindowContainerTransaction();
+ splitLayout.resizeSplits(position, t);
+ new WindowOrganizer().applyTransaction(t);
+ }
+
+ private boolean getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out,
+ WindowContainerToken parent) {
+ boolean resizable = false;
+ List<ActivityManager.RunningTaskInfo> rootTasks = parent == null
+ ? mTaskOrganizer.getRootTasks(Display.DEFAULT_DISPLAY, HOME_AND_RECENTS)
+ : mTaskOrganizer.getChildTasks(parent, HOME_AND_RECENTS);
+ for (int i = 0, n = rootTasks.size(); i < n; ++i) {
+ final ActivityManager.RunningTaskInfo ti = rootTasks.get(i);
+ out.add(ti);
+ if (ti.topActivityType == ACTIVITY_TYPE_HOME) {
+ resizable = ti.isResizeable;
+ }
+ }
+ return resizable;
+ }
+
+ /**
+ * Assign a fixed override-bounds to home tasks that reflect their geometry while the primary
+ * split is minimized. This actually "sticks out" of the secondary split area, but when in
+ * minimized mode, the secondary split gets a 'negative' crop to expose it.
+ */
+ boolean applyHomeTasksMinimized(SplitDisplayLayout layout, WindowContainerToken parent,
+ @NonNull WindowContainerTransaction wct) {
+ // Resize the home/recents stacks to the larger minimized-state size
+ final Rect homeBounds;
+ final ArrayList<ActivityManager.RunningTaskInfo> homeStacks = new ArrayList<>();
+ boolean isHomeResizable = getHomeAndRecentsTasks(homeStacks, parent);
+ if (isHomeResizable) {
+ homeBounds = layout.calcResizableMinimizedHomeStackBounds();
+ } else {
+ // home is not resizable, so lock it to its inherent orientation size.
+ homeBounds = new Rect(0, 0, 0, 0);
+ for (int i = homeStacks.size() - 1; i >= 0; --i) {
+ if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_HOME) {
+ final int orient = homeStacks.get(i).configuration.orientation;
+ final boolean displayLandscape = layout.mDisplayLayout.isLandscape();
+ final boolean isLandscape = orient == ORIENTATION_LANDSCAPE
+ || (orient == ORIENTATION_UNDEFINED && displayLandscape);
+ homeBounds.right = isLandscape == displayLandscape
+ ? layout.mDisplayLayout.width() : layout.mDisplayLayout.height();
+ homeBounds.bottom = isLandscape == displayLandscape
+ ? layout.mDisplayLayout.height() : layout.mDisplayLayout.width();
+ break;
+ }
+ }
+ }
+ for (int i = homeStacks.size() - 1; i >= 0; --i) {
+ // For non-resizable homes, the minimized size is actually the fullscreen-size. As a
+ // result, we don't minimize for recents since it only shows half-size screenshots.
+ if (!isHomeResizable) {
+ if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_RECENTS) {
+ continue;
+ }
+ wct.setWindowingMode(homeStacks.get(i).token, WINDOWING_MODE_FULLSCREEN);
+ }
+ wct.setBounds(homeStacks.get(i).token, homeBounds);
+ }
+ layout.mTiles.mHomeBounds.set(homeBounds);
+ return isHomeResizable;
+ }
+
+ /**
+ * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split.
+ * This assumes there is already something in the primary split since that is usually what
+ * triggers a call to this. In the same transaction, this overrides the home task bounds via
+ * {@link #applyHomeTasksMinimized}.
+ *
+ * @return whether the home stack is resizable
+ */
+ boolean applyEnterSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout) {
+ // Set launchtile first so that any stack created after
+ // getAllRootTaskInfos and before reparent (even if unlikely) are placed
+ // correctly.
+ mTaskOrganizer.setLaunchRoot(DEFAULT_DISPLAY, tiles.mSecondary.token);
+ List<ActivityManager.RunningTaskInfo> rootTasks =
+ mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (rootTasks.isEmpty()) {
+ return false;
+ }
+ ActivityManager.RunningTaskInfo topHomeTask = null;
+ for (int i = rootTasks.size() - 1; i >= 0; --i) {
+ final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i);
+ // Only move resizeable task to split secondary. However, we have an exception
+ // for non-resizable home because we will minimize to show it.
+ if (!rootTask.isResizeable && rootTask.topActivityType != ACTIVITY_TYPE_HOME) {
+ continue;
+ }
+ // Only move fullscreen tasks to split secondary.
+ if (rootTask.configuration.windowConfiguration.getWindowingMode()
+ != WINDOWING_MODE_FULLSCREEN) {
+ continue;
+ }
+ // Since this iterates from bottom to top, update topHomeTask for every fullscreen task
+ // so it will be left with the status of the top one.
+ topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null;
+ wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */);
+ }
+ // Move the secondary split-forward.
+ wct.reorder(tiles.mSecondary.token, true /* onTop */);
+ boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct);
+ if (topHomeTask != null) {
+ // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST
+ // is enabled, this temporarily syncs the home surface position with offset until
+ // sync transaction finishes.
+ wct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds);
+ }
+ applySyncTransaction(wct);
+ return isHomeResizable;
+ }
+
+ boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) {
+ final int atype = ti.configuration.windowConfiguration.getActivityType();
+ return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS;
+ }
+
+ /**
+ * Reparents all tile members back to their display and resets home task override bounds.
+ * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary
+ * split (thus resulting in the top of the secondary split becoming
+ * fullscreen. {@code false} resolves the other way.
+ */
+ void applyDismissSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout,
+ boolean dismissOrMaximize) {
+ // Set launch root first so that any task created after getChildContainers and
+ // before reparent (pretty unlikely) are put into fullscreen.
+ mTaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null);
+ // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished
+ // plus specific APIs to clean this up.
+ List<ActivityManager.RunningTaskInfo> primaryChildren =
+ mTaskOrganizer.getChildTasks(tiles.mPrimary.token, null /* activityTypes */);
+ List<ActivityManager.RunningTaskInfo> secondaryChildren =
+ mTaskOrganizer.getChildTasks(tiles.mSecondary.token, null /* activityTypes */);
+ // In some cases (eg. non-resizable is launched), system-server will leave split-screen.
+ // as a result, the above will not capture any tasks; yet, we need to clean-up the
+ // home task bounds.
+ List<ActivityManager.RunningTaskInfo> freeHomeAndRecents =
+ mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS);
+ // Filter out the root split tasks
+ freeHomeAndRecents.removeIf(p -> p.token.equals(tiles.mSecondary.token)
+ || p.token.equals(tiles.mPrimary.token));
+
+ if (primaryChildren.isEmpty() && secondaryChildren.isEmpty()
+ && freeHomeAndRecents.isEmpty()) {
+ return;
+ }
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (dismissOrMaximize) {
+ // Dismissing, so move all primary split tasks first
+ for (int i = primaryChildren.size() - 1; i >= 0; --i) {
+ wct.reparent(primaryChildren.get(i).token, null /* parent */,
+ true /* onTop */);
+ }
+ boolean homeOnTop = false;
+ // Don't need to worry about home tasks because they are already in the "proper"
+ // order within the secondary split.
+ for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
+ final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
+ wct.reparent(ti.token, null /* parent */, true /* onTop */);
+ if (isHomeOrRecentTask(ti)) {
+ wct.setBounds(ti.token, null);
+ wct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED);
+ if (i == 0) {
+ homeOnTop = true;
+ }
+ }
+ }
+ if (homeOnTop) {
+ // Translate/update-crop of secondary out-of-band with sync transaction -- instead
+ // play this in sync with new home-app frame because until BALST is enabled this
+ // shows up on screen before the syncTransaction returns.
+ // We only have access to the secondary root surface, though, so in order to
+ // position things properly, we have to take into account the existing negative
+ // offset/crop of the minimized-home task.
+ final boolean landscape = layout.mDisplayLayout.isLandscape();
+ final int posX = landscape ? layout.mSecondary.left - tiles.mHomeBounds.left
+ : layout.mSecondary.left;
+ final int posY = landscape ? layout.mSecondary.top
+ : layout.mSecondary.top - tiles.mHomeBounds.top;
+ final SurfaceControl.Transaction sft = new SurfaceControl.Transaction();
+ sft.setPosition(tiles.mSecondarySurface, posX, posY);
+ final Rect crop = new Rect(0, 0, layout.mDisplayLayout.width(),
+ layout.mDisplayLayout.height());
+ crop.offset(-posX, -posY);
+ sft.setWindowCrop(tiles.mSecondarySurface, crop);
+ wct.setBoundsChangeTransaction(tiles.mSecondary.token, sft);
+ }
+ } else {
+ // Maximize, so move non-home secondary split first
+ for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
+ if (isHomeOrRecentTask(secondaryChildren.get(i))) {
+ continue;
+ }
+ wct.reparent(secondaryChildren.get(i).token, null /* parent */,
+ true /* onTop */);
+ }
+ // Find and place home tasks in-between. This simulates the fact that there was
+ // nothing behind the primary split's tasks.
+ for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
+ final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
+ if (isHomeOrRecentTask(ti)) {
+ wct.reparent(ti.token, null /* parent */, true /* onTop */);
+ // reset bounds and mode too
+ wct.setBounds(ti.token, null);
+ wct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED);
+ }
+ }
+ for (int i = primaryChildren.size() - 1; i >= 0; --i) {
+ wct.reparent(primaryChildren.get(i).token, null /* parent */,
+ true /* onTop */);
+ }
+ }
+ for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) {
+ wct.setBounds(freeHomeAndRecents.get(i).token, null);
+ wct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED);
+ }
+ // Reset focusable to true
+ wct.setFocusable(tiles.mPrimary.token, true /* focusable */);
+ applySyncTransaction(wct);
+ }
+
+ /**
+ * Utility to apply a sync transaction serially with other sync transactions.
+ *
+ * @see SyncTransactionQueue#queue
+ */
+ void applySyncTransaction(WindowContainerTransaction wct) {
+ mSyncTransactionQueue.queue(wct);
+ }
+
+ /**
+ * @see SyncTransactionQueue#queueIfWaiting
+ */
+ boolean queueSyncTransactionIfWaiting(WindowContainerTransaction wct) {
+ return mSyncTransactionQueue.queueIfWaiting(wct);
+ }
+
+ /**
+ * @see SyncTransactionQueue#runInSync
+ */
+ void runInSync(SyncTransactionQueue.TransactionRunnable runnable) {
+ mSyncTransactionQueue.runInSync(runnable);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/README.md b/libs/WindowManager/Shell/tests/README.md
new file mode 100644
index 000000000000..c19db76a358c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/README.md
@@ -0,0 +1,15 @@
+# WM Shell Test
+
+This contains all tests written for WM (WindowManager) Shell and it's currently
+divided into 3 categories
+
+- unittest, tests against individual functions, usually @SmallTest and do not
+ require UI automation nor real device to run
+- integration, this maybe a mix of functional and integration tests. Contains
+ tests verify the WM Shell as a whole, like talking to WM core. This usually
+ involves mocking the window manager service or even talking to the real one.
+ Due to this nature, test cases in this package is normally annotated as
+ @LargeTest and runs with UI automation on real device
+- flicker, similar to functional tests with its sole focus on flickerness. See
+ [WM Shell Flicker Test Package](http://cs/android/framework/base/libs/WindowManager/Shell/tests/flicker/)
+ for more details
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
new file mode 100644
index 000000000000..d7afa0e166b3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -0,0 +1,52 @@
+//
+// 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.
+//
+
+android_test {
+ name: "WMShellFlickerTests",
+ srcs: ["src/**/*.java", "src/**/*.kt"],
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTestPhysicalDevices.xml",
+ platform_apis: true,
+ certificate: "platform",
+ test_suites: ["device-tests"],
+ libs: ["android.test.runner"],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "flickerlib",
+ "truth-prebuilt",
+ "app-helpers-core",
+ "launcher-helper-lib",
+ "launcher-aosp-tapl"
+ ],
+}
+
+android_test {
+ name: "WMShellFlickerTestsVirtual",
+ srcs: ["src/**/*.java", "src/**/*.kt"],
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTestVirtualDevices.xml",
+ platform_apis: true,
+ certificate: "platform",
+ libs: ["android.test.runner"],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "flickerlib",
+ "truth-prebuilt",
+ "app-helpers-core",
+ "launcher-helper-lib",
+ "launcher-aosp-tapl"
+ ],
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
new file mode 100644
index 000000000000..8b2f6681554a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.flicker">
+
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+ <!-- 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" />
+ <!-- Write secure settings -->
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <!-- Capture screen contents -->
+ <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
+ <!-- Enable / Disable tracing !-->
+ <uses-permission android:name="android.permission.DUMP" />
+ <!-- Run layers trace -->
+ <uses-permission android:name="android.permission.HARDWARE_TEST"/>
+ <!-- Workaround grant runtime permission exception from b/152733071 -->
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+ <uses-permission android:name="android.permission.READ_LOGS"/>
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.wm.shell.flicker"
+ android:label="WindowManager Shell Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml
new file mode 100644
index 000000000000..9dd9f42bdf81
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright 2020 Google Inc. All Rights Reserved.
+ -->
+<configuration description="Runs WindowManager Shell Flicker Tests">
+ <option name="test-tag" value="FlickerTests" />
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- keeps the screen on during tests -->
+ <option name="screen-always-on" value="on" />
+ <!-- prevents the phone from restarting -->
+ <option name="force-skip-system-props" value="true" />
+ <!-- set WM tracing verbose level to all -->
+ <option name="run-command" value="cmd window tracing level all" />
+ <!-- inform WM to log all transactions -->
+ <option name="run-command" value="cmd window tracing transaction" />
+ <!-- restart launcher to activate TAPL -->
+ <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner">
+ <!-- reboot the device to teardown any crashed tests -->
+ <option name="cleanup-action" value="REBOOT" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="WMShellFlickerTests.apk"/>
+ <option name="test-file-name" value="WMShellFlickerTestApp.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.wm.shell.flicker"/>
+ <option name="include-annotation" value="androidx.test.filters.RequiresDevice" />
+ <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" />
+ <option name="shell-timeout" value="6600s" />
+ <option name="test-timeout" value="6000s" />
+ <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="collect-on-run-ended-only" value="true" />
+ <option name="clean-up" value="true" />
+ </metrics_collector>
+</configuration> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml
new file mode 100644
index 000000000000..afb1166415fc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright 2020 Google Inc. All Rights Reserved.
+ -->
+<configuration description="Runs WindowManager Shell Flicker Tests">
+ <option name="test-tag" value="FlickerTests" />
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- keeps the screen on during tests -->
+ <option name="screen-always-on" value="on" />
+ <!-- prevents the phone from restarting -->
+ <option name="force-skip-system-props" value="true" />
+ <!-- set WM tracing verbose level to all -->
+ <option name="run-command" value="cmd window tracing level all" />
+ <!-- inform WM to log all transactions -->
+ <option name="run-command" value="cmd window tracing transaction" />
+ <!-- restart launcher to activate TAPL -->
+ <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner">
+ <!-- reboot the device to teardown any crashed tests -->
+ <option name="cleanup-action" value="REBOOT" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="WMShellFlickerTests.apk"/>
+ <option name="test-file-name" value="WMShellFlickerTestApp.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.wm.shell.flicker"/>
+ <option name="exclude-annotation" value="androidx.test.filters.RequiresDevice" />
+ <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" />
+ <option name="shell-timeout" value="6600s" />
+ <option name="test-timeout" value="6000s" />
+ <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="collect-on-run-ended-only" value="true" />
+ <option name="clean-up" value="true" />
+ </metrics_collector>
+</configuration> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/README.md b/libs/WindowManager/Shell/tests/flicker/README.md
new file mode 100644
index 000000000000..4502d498a65b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/README.md
@@ -0,0 +1,10 @@
+# WM Shell Flicker Test Package
+
+Please reference the following links
+
+- [Introduction to Flicker Test Library](http://cs/android/platform_testing/libraries/flicker/)
+- [Flicker Test in frameworks/base](http://cs/android/frameworks/base/tests/FlickerTests/)
+
+on what is Flicker Test and how to write a Flicker Test
+
+To run the Flicker Tests for WM Shell, simply run `atest WMShellFlickerTests`
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
new file mode 100644
index 000000000000..8c4f5468906f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -0,0 +1,153 @@
+/*
+ * 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
+
+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.navBarLayerIsAlwaysVisible(
+ bugId: Int = 0,
+ enabled: Boolean = bugId == 0
+) {
+ all("navBarLayerIsAlwaysVisible", bugId, enabled) {
+ this.showsLayer(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE)
+ }
+}
+
+@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.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,
+ enabled: Boolean = bugId == 0
+) {
+ all(enabled = enabled, bugId = bugId) {
+ this.focusChanges(windows)
+ }
+}
+
+fun EventLogAssertion.focusDoesNotChange(
+ bugId: Int = 0,
+ enabled: Boolean = bugId == 0
+) {
+ all(enabled = enabled, bugId = bugId) {
+ this.focusDoesNotChange()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt
new file mode 100644
index 000000000000..99f824bb8341
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt
@@ -0,0 +1,130 @@
+/*
+ * 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
+
+import android.os.RemoteException
+import android.os.SystemClock
+import android.platform.helpers.IAppHelper
+import android.view.Surface
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.Flicker
+
+/**
+ * Base class of all Flicker test that performs common functions for all flicker tests:
+ *
+ *
+ * - Caches transitions so that a transition is run once and the transition results are used by
+ * tests multiple times. This is needed for parameterized tests which call the BeforeClass methods
+ * multiple times.
+ * - Keeps track of all test artifacts and deletes ones which do not need to be reviewed.
+ * - Fails tests if results are not available for any test due to jank.
+ */
+abstract class FlickerTestBase {
+ val instrumentation by lazy {
+ InstrumentationRegistry.getInstrumentation()
+ }
+ val uiDevice by lazy {
+ UiDevice.getInstance(instrumentation)
+ }
+
+ /**
+ * Build a test tag for the test
+ * @param testName Name of the transition(s) being tested
+ * @param app App being launcher
+ * @param rotation Initial screen rotation
+ *
+ * @return test tag with pattern <NAME>__<APP>__<ROTATION>
+ </ROTATION></APP></NAME> */
+ protected fun buildTestTag(testName: String, app: IAppHelper, rotation: Int): String {
+ return buildTestTag(
+ testName, app, rotation, rotation, app2 = null, extraInfo = "")
+ }
+
+ /**
+ * Build a test tag for the test
+ * @param testName Name of the transition(s) being tested
+ * @param app App being launcher
+ * @param beginRotation Initial screen rotation
+ * @param endRotation End screen rotation (if any, otherwise use same as initial)
+ *
+ * @return test tag with pattern <NAME>__<APP>__<BEGIN_ROTATION>-<END_ROTATION>
+ </END_ROTATION></BEGIN_ROTATION></APP></NAME> */
+ protected fun buildTestTag(
+ testName: String,
+ app: IAppHelper,
+ beginRotation: Int,
+ endRotation: Int
+ ): String {
+ return buildTestTag(
+ testName, app, beginRotation, endRotation, app2 = null, extraInfo = "")
+ }
+
+ /**
+ * Build a test tag for the test
+ * @param testName Name of the transition(s) being tested
+ * @param app App being launcher
+ * @param app2 Second app being launched (if any)
+ * @param beginRotation Initial screen rotation
+ * @param endRotation End screen rotation (if any, otherwise use same as initial)
+ * @param extraInfo Additional information to append to the tag
+ *
+ * @return test tag with pattern <NAME>__<APP></APP>(S)>__<ROTATION></ROTATION>(S)>[__<EXTRA>]
+ </EXTRA></NAME> */
+ protected fun buildTestTag(
+ testName: String,
+ app: IAppHelper,
+ beginRotation: Int,
+ endRotation: Int,
+ app2: IAppHelper?,
+ extraInfo: String
+ ): String {
+ var testTag = "${testName}__${app.launcherName}"
+ if (app2 != null) {
+ testTag += "-${app2.launcherName}"
+ }
+ testTag += "__${Surface.rotationToString(beginRotation)}"
+ if (endRotation != beginRotation) {
+ testTag += "-${Surface.rotationToString(endRotation)}"
+ }
+ if (extraInfo.isNotEmpty()) {
+ testTag += "__$extraInfo"
+ }
+ return testTag
+ }
+
+ protected fun Flicker.setRotation(rotation: Int) {
+ try {
+ when (rotation) {
+ Surface.ROTATION_270 -> device.setOrientationLeft()
+ Surface.ROTATION_90 -> device.setOrientationRight()
+ Surface.ROTATION_0 -> device.setOrientationNatural()
+ else -> device.setOrientationNatural()
+ }
+ // Wait for animation to complete
+ SystemClock.sleep(1000)
+ } catch (e: RemoteException) {
+ throw RuntimeException(e)
+ }
+ }
+
+ companion object {
+ const val NAVIGATION_BAR_WINDOW_TITLE = "NavigationBar"
+ const val STATUS_BAR_WINDOW_TITLE = "StatusBar"
+ const val DOCKED_STACK_DIVIDER = "DockedStackDivider"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NonRotationTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NonRotationTestBase.kt
new file mode 100644
index 000000000000..90334ae91e9d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NonRotationTestBase.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.view.Surface
+import org.junit.runners.Parameterized
+
+abstract class NonRotationTestBase(
+ protected val rotationName: String,
+ protected val rotation: Int
+) : FlickerTestBase() {
+ companion object {
+ const val SCREENSHOT_LAYER = "RotationLayer"
+
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<Array<Any>> {
+ val supportedRotations = intArrayOf(Surface.ROTATION_0, Surface.ROTATION_90)
+ return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FlickerAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FlickerAppHelper.kt
new file mode 100644
index 000000000000..47a62ce92d11
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FlickerAppHelper.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import com.android.server.wm.flicker.helpers.StandardAppHelper
+
+abstract class FlickerAppHelper(
+ instr: Instrumentation,
+ launcherName: String,
+ launcherStrategy: ILauncherStrategy
+) : StandardAppHelper(instr, sFlickerPackage, launcherName, launcherStrategy) {
+ companion object {
+ var sFlickerPackage = "com.android.wm.shell.flicker.testapp"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
new file mode 100644
index 000000000000..0cedc0a7147f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.FIND_TIMEOUT
+import com.android.server.wm.flicker.helpers.waitForIME
+import org.junit.Assert
+
+open class ImeAppHelper(
+ instr: Instrumentation,
+ launcherName: String = "ImeApp",
+ launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
+ .getInstance(instr)
+ .launcherStrategy
+) : FlickerAppHelper(instr, launcherName, launcherStrategy) {
+ open fun openIME(device: UiDevice) {
+ val editText = device.wait(
+ Until.findObject(By.res(getPackage(), "plain_text_input")),
+ FIND_TIMEOUT)
+ Assert.assertNotNull("Text field not found, this usually happens when the device " +
+ "was left in an unknown state (e.g. in split screen)", editText)
+ editText.click()
+ if (!device.waitForIME()) {
+ Assert.fail("IME did not appear")
+ }
+ }
+
+ open fun closeIME(device: UiDevice) {
+ device.pressBack()
+ // Using only the AccessibilityInfo it is not possible to identify if the IME is active
+ device.waitForIdle(1000)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
new file mode 100644
index 000000000000..539170202b8a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.helpers.hasPipWindow
+import com.android.server.wm.flicker.helpers.closePipWindow
+import org.junit.Assert
+
+class PipAppHelper(
+ instr: Instrumentation,
+ launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
+ .getInstance(instr)
+ .launcherStrategy
+) : FlickerAppHelper(instr, "PipApp", launcherStrategy) {
+ fun clickEnterPipButton(device: UiDevice) {
+ val enterPipButton = device.findObject(By.res(getPackage(), "enter_pip"))
+ Assert.assertNotNull("Pip button not found, this usually happens when the device " +
+ "was left in an unknown state (e.g. in split screen)", enterPipButton)
+ enterPipButton.click()
+ device.hasPipWindow()
+ }
+
+ fun closePipWindow(device: UiDevice) {
+ device.closePipWindow()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
new file mode 100644
index 000000000000..010aa0d7d832
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.view.Surface
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.dsl.flicker
+import com.android.server.wm.flicker.helpers.closePipWindow
+import com.android.server.wm.flicker.helpers.expandPipWindow
+import com.android.server.wm.flicker.helpers.hasPipWindow
+import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.wm.shell.flicker.navBarLayerIsAlwaysVisible
+import com.android.wm.shell.flicker.navBarLayerRotatesAndScales
+import com.android.wm.shell.flicker.navBarWindowIsAlwaysVisible
+import com.android.wm.shell.flicker.noUncoveredRegions
+import com.android.wm.shell.flicker.statusBarLayerIsAlwaysVisible
+import com.android.wm.shell.flicker.statusBarLayerRotatesScales
+import com.android.wm.shell.flicker.statusBarWindowIsAlwaysVisible
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test Pip launch.
+ * To run this test: `atest WMShellFlickerTests:PipToAppTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@FlakyTest(bugId = 152738416)
+class EnterPipTest(
+ rotationName: String,
+ rotation: Int
+) : PipTestBase(rotationName, rotation) {
+ @Test
+ fun test() {
+ flicker(instrumentation) {
+ withTag { buildTestTag("enterPip", testApp, rotation) }
+ repeat { 1 }
+ setup {
+ test {
+ device.wakeUpAndGoToHomeScreen()
+ }
+ eachRun {
+ device.pressHome()
+ testApp.open()
+ this.setRotation(rotation)
+ }
+ }
+ teardown {
+ eachRun {
+ if (device.hasPipWindow()) {
+ device.closePipWindow()
+ }
+ testApp.exit()
+ this.setRotation(Surface.ROTATION_0)
+ }
+ test {
+ if (device.hasPipWindow()) {
+ device.closePipWindow()
+ }
+ }
+ }
+ transitions {
+ testApp.clickEnterPipButton(device)
+ device.expandPipWindow()
+ }
+ assertions {
+ windowManagerTrace {
+ navBarWindowIsAlwaysVisible()
+ statusBarWindowIsAlwaysVisible()
+ all("pipWindowBecomesVisible") {
+ this.showsAppWindow(testApp.`package`)
+ .then()
+ .showsAppWindow(sPipWindowTitle)
+ }
+ }
+
+ layersTrace {
+ navBarLayerIsAlwaysVisible()
+ statusBarLayerIsAlwaysVisible()
+ noUncoveredRegions(rotation, Surface.ROTATION_0, allStates = false)
+ navBarLayerRotatesAndScales(rotation, Surface.ROTATION_0)
+ statusBarLayerRotatesScales(rotation, Surface.ROTATION_0)
+
+ all("pipLayerBecomesVisible") {
+ this.showsLayer(testApp.launcherName)
+ .then()
+ .showsLayer(sPipWindowTitle)
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<Array<Any>> {
+ val supportedRotations = intArrayOf(Surface.ROTATION_0)
+ return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
new file mode 100644
index 000000000000..43e022538685
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.content.ComponentName
+import android.graphics.Region
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import android.util.Log
+import android.view.Surface
+import android.view.WindowManager
+import androidx.test.filters.RequiresDevice
+import com.android.compatibility.common.util.SystemUtil
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.dsl.runWithFlicker
+import com.android.server.wm.flicker.helpers.closePipWindow
+import com.android.server.wm.flicker.helpers.hasPipWindow
+import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.wm.shell.flicker.helpers.ImeAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+import java.io.IOException
+
+/**
+ * Test Pip launch.
+ * To run this test: `atest WMShellFlickerTests:PipKeyboardTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class PipKeyboardTest(
+ rotationName: String,
+ rotation: Int
+) : PipTestBase(rotationName, rotation) {
+ private val windowManager: WindowManager =
+ instrumentation.context.getSystemService(WindowManager::class.java)
+
+ private val keyboardApp = ImeAppHelper(instrumentation, "ImeApp",
+ LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy)
+
+ private val KEYBOARD_ACTIVITY: ComponentName = ComponentName.createRelative(
+ "com.android.wm.shell.flicker.testapp", ".ImeActivity")
+ private val PIP_ACTIVITY_WINDOW_NAME = "PipActivity"
+ private val INPUT_METHOD_WINDOW_NAME = "InputMethod"
+
+ private val testRepetitions = 10
+
+ private val keyboardScenario: FlickerBuilder
+ get() = FlickerBuilder(instrumentation).apply {
+ repeat { testRepetitions }
+ // disable layer tracing
+ withLayerTracing { null }
+ setup {
+ test {
+ device.wakeUpAndGoToHomeScreen()
+ device.pressHome()
+ // launch our target pip app
+ testApp.open()
+ this.setRotation(rotation)
+ testApp.clickEnterPipButton(device)
+ // open an app with an input field and a keyboard
+ // UiAutomator doesn't support to launch the multiple Activities in a task.
+ // So use launchActivity() for the Keyboard Activity.
+ launchActivity(KEYBOARD_ACTIVITY)
+ }
+ }
+ teardown {
+ test {
+ keyboardApp.exit()
+
+ if (device.hasPipWindow()) {
+ device.closePipWindow()
+ }
+ testApp.exit()
+ this.setRotation(Surface.ROTATION_0)
+ }
+ }
+ }
+
+ /** Ensure the pip window remains visible throughout any keyboard interactions. */
+ @Test
+ fun pipWindow_doesNotLeaveTheScreen_onKeyboardOpenClose() {
+ val testTag = "pipWindow_doesNotLeaveTheScreen_onKeyboardOpenClose"
+ runWithFlicker(keyboardScenario) {
+ withTestName { testTag }
+ transitions {
+ // open the soft keyboard
+ keyboardApp.openIME(device)
+
+ // then close it again
+ keyboardApp.closeIME(device)
+ }
+ assertions {
+ windowManagerTrace {
+ all("PiP window must remain inside visible bounds") {
+ coversAtMostRegion(
+ partialWindowTitle = "PipActivity",
+ region = Region(windowManager.maximumWindowMetrics.bounds)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ /** Ensure the pip window does not obscure the keyboard. */
+ @Test
+ fun pipWindow_doesNotObscure_keyboard() {
+ val testTag = "pipWindow_doesNotObscure_keyboard"
+ runWithFlicker(keyboardScenario) {
+ withTestName { testTag }
+ transitions {
+ // open the soft keyboard
+ keyboardApp.openIME(device)
+ }
+ teardown {
+ eachRun {
+ // close the keyboard
+ keyboardApp.closeIME(device)
+ }
+ }
+ assertions {
+ windowManagerTrace {
+ end {
+ isAboveWindow(INPUT_METHOD_WINDOW_NAME, PIP_ACTIVITY_WINDOW_NAME)
+ }
+ }
+ }
+ }
+ }
+
+ private fun launchActivity(
+ activity: ComponentName? = null,
+ action: String? = null,
+ flags: Set<Int> = setOf(),
+ boolExtras: Map<String, Boolean> = mapOf(),
+ intExtras: Map<String, Int> = mapOf(),
+ stringExtras: Map<String, String> = mapOf()
+ ) {
+ require(activity != null || !action.isNullOrBlank()) {
+ "Cannot launch an activity with neither activity name nor action!"
+ }
+ val command = composeCommand(
+ "start", activity, action, flags, boolExtras, intExtras, stringExtras)
+ executeShellCommand(command)
+ }
+
+ private fun composeCommand(
+ command: String,
+ activity: ComponentName?,
+ action: String?,
+ flags: Set<Int>,
+ boolExtras: Map<String, Boolean>,
+ intExtras: Map<String, Int>,
+ stringExtras: Map<String, String>
+ ): String = buildString {
+ append("am ")
+ append(command)
+ activity?.let {
+ append(" -n ")
+ append(it.flattenToShortString())
+ }
+ action?.let {
+ append(" -a ")
+ append(it)
+ }
+ flags.forEach {
+ append(" -f ")
+ append(it)
+ }
+ boolExtras.forEach {
+ append(it.withFlag("ez"))
+ }
+ intExtras.forEach {
+ append(it.withFlag("ei"))
+ }
+ stringExtras.forEach {
+ append(it.withFlag("es"))
+ }
+ }
+
+ private fun Map.Entry<String, *>.withFlag(flag: String): String = " --$flag $key $value"
+
+ private fun executeShellCommand(cmd: String): String {
+ try {
+ return SystemUtil.runShellCommand(instrumentation, cmd)
+ } catch (e: IOException) {
+ Log.e("FlickerTests", "Error running shell command: $cmd")
+ throw e
+ }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<Array<Any>> {
+ val supportedRotations = intArrayOf(Surface.ROTATION_0)
+ return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) }
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt
new file mode 100644
index 000000000000..3822d69a65f5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import com.android.wm.shell.flicker.NonRotationTestBase
+import com.android.wm.shell.flicker.helpers.PipAppHelper
+
+abstract class PipTestBase(
+ rotationName: String,
+ rotation: Int
+) : NonRotationTestBase(rotationName, rotation) {
+ protected val testApp = PipAppHelper(instrumentation)
+
+ companion object {
+ const val sPipWindowTitle = "PipMenuActivity"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/Android.bp b/libs/WindowManager/Shell/tests/flicker/test-apps/Android.bp
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/Android.bp
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp
new file mode 100644
index 000000000000..d12b49245277
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp
@@ -0,0 +1,20 @@
+// 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.
+
+android_test {
+ name: "WMShellFlickerTestApp",
+ srcs: ["**/*.java"],
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
new file mode 100644
index 000000000000..7f8321f3fa3d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.flicker.testapp">
+
+ <uses-sdk android:minSdkVersion="29"
+ android:targetSdkVersion="29"/>
+ <application android:allowBackup="false"
+ android:supportsRtl="true">
+ <activity android:name=".PipActivity"
+ android:resizeableActivity="true"
+ android:supportsPictureInPicture="true"
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
+ android:taskAffinity="com.android.wm.shell.flicker.testapp.PipActivity"
+ android:label="PipApp"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <activity android:name=".ImeActivity"
+ android:taskAffinity="com.android.wm.shell.flicker.testapp.ImeActivity"
+ android:label="ImeApp"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml
new file mode 100644
index 000000000000..4708cfd48381
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusableInTouchMode="true"
+ android:background="@android:color/holo_green_light">
+ <EditText android:id="@+id/plain_text_input"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:inputType="text"/>
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml
new file mode 100644
index 000000000000..e1870d9c523d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/holo_blue_bright">
+ <Button android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/enter_pip"
+ android:text="Enter PIP"/>
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java
new file mode 100644
index 000000000000..856728715c1c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+public class ImeActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ WindowManager.LayoutParams p = getWindow().getAttributes();
+ p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
+ .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ getWindow().setAttributes(p);
+ setContentView(R.layout.activity_ime);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java
new file mode 100644
index 000000000000..305281691e11
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.testapp;
+
+import android.app.Activity;
+import android.app.PictureInPictureParams;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Rational;
+import android.view.WindowManager;
+import android.widget.Button;
+
+public class PipActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ WindowManager.LayoutParams p = getWindow().getAttributes();
+ p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
+ .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ getWindow().setAttributes(p);
+ setContentView(R.layout.activity_pip);
+ Button enterPip = (Button) findViewById(R.id.enter_pip);
+
+ PictureInPictureParams params = new PictureInPictureParams.Builder()
+ .setAspectRatio(new Rational(1, 1))
+ .setSourceRectHint(new Rect(0, 0, 100, 100))
+ .build();
+
+ enterPip.setOnClickListener((v) -> enterPictureInPictureMode(params));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 78fa45ebdf94..9940ea575873 100644
--- a/libs/WindowManager/Shell/tests/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -13,7 +13,7 @@
// limitations under the License.
android_test {
- name: "WindowManagerShellTests",
+ name: "WMShellUnitTests",
srcs: ["**/*.java"],
@@ -23,21 +23,29 @@ android_test {
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "androidx.dynamicanimation_dynamicanimation",
+ "dagger2",
+ "kotlinx-coroutines-android",
+ "kotlinx-coroutines-core",
"mockito-target-extended-minus-junit4",
"truth-prebuilt",
+ "testables",
],
+
libs: [
"android.test.mock",
"android.test.base",
"android.test.runner",
],
+
jni_libs: [
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
],
- sdk_version: "current",
- platform_apis: true,
+ kotlincflags: ["-Xjvm-default=enable"],
+
+ plugins: ["dagger2-compiler"],
optimize: {
enabled: false,
diff --git a/libs/WindowManager/Shell/tests/AndroidManifest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
index a8f795ec8a8d..a8f795ec8a8d 100644
--- a/libs/WindowManager/Shell/tests/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
diff --git a/libs/WindowManager/Shell/tests/AndroidTest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidTest.xml
index 4dce4db360e4..21ed2c075dff 100644
--- a/libs/WindowManager/Shell/tests/AndroidTest.xml
+++ b/libs/WindowManager/Shell/tests/unittest/AndroidTest.xml
@@ -17,12 +17,12 @@
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="install-arg" value="-t" />
- <option name="test-file-name" value="WindowManagerShellTests.apk" />
+ <option name="test-file-name" value="WMShellUnitTests.apk" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="framework-base-presubmit" />
- <option name="test-tag" value="WindowManagerShellTests" />
+ <option name="test-tag" value="WMShellUnitTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.wm.shell.tests" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/libs/WindowManager/Shell/tests/res/values/config.xml b/libs/WindowManager/Shell/tests/unittest/res/values/config.xml
index c894eb0133b5..c894eb0133b5 100644
--- a/libs/WindowManager/Shell/tests/res/values/config.xml
+++ b/libs/WindowManager/Shell/tests/unittest/res/values/config.xml
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
new file mode 100644
index 000000000000..07a6bda239c7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -0,0 +1,251 @@
+/*
+ * 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;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_MULTI_WINDOW;
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.pm.ParceledListSlice;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.SurfaceControl;
+import android.window.ITaskOrganizer;
+import android.window.ITaskOrganizerController;
+import android.window.TaskAppearedInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TransactionPool;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for the shell task organizer.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ShellTaskOrganizerTests {
+
+ @Mock
+ private ITaskOrganizerController mTaskOrganizerController;
+
+ ShellTaskOrganizer mOrganizer;
+ private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class);
+ private final TransactionPool mTransactionPool = mock(TransactionPool.class);
+ private final ShellExecutor mTestExecutor = mock(ShellExecutor.class);
+
+ private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener {
+ final ArrayList<RunningTaskInfo> appeared = new ArrayList<>();
+ final ArrayList<RunningTaskInfo> vanished = new ArrayList<>();
+ final ArrayList<RunningTaskInfo> infoChanged = new ArrayList<>();
+
+ @Override
+ public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+ appeared.add(taskInfo);
+ }
+
+ @Override
+ public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+ infoChanged.add(taskInfo);
+ }
+
+ @Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ vanished.add(taskInfo);
+ }
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ try {
+ doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList())
+ .when(mTaskOrganizerController).registerTaskOrganizer(any());
+ } catch (RemoteException e) {}
+ mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mSyncTransactionQueue,
+ mTransactionPool, mTestExecutor, mTestExecutor));
+ }
+
+ @Test
+ public void registerOrganizer_sendRegisterTaskOrganizer() throws RemoteException {
+ mOrganizer.registerOrganizer();
+
+ verify(mTaskOrganizerController).registerTaskOrganizer(any(ITaskOrganizer.class));
+ }
+
+ @Test
+ public void testOneListenerPerType() {
+ mOrganizer.addListenerForType(new TrackingTaskListener(), TASK_LISTENER_TYPE_MULTI_WINDOW);
+ try {
+ mOrganizer.addListenerForType(
+ new TrackingTaskListener(), TASK_LISTENER_TYPE_MULTI_WINDOW);
+ fail("Expected exception due to already registered listener");
+ } catch (Exception e) {
+ // Expected failure
+ }
+ }
+
+ @Test
+ public void testRegisterWithExistingTasks() throws RemoteException {
+ // Setup some tasks
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+ ArrayList<TaskAppearedInfo> taskInfos = new ArrayList<>();
+ taskInfos.add(new TaskAppearedInfo(task1, new SurfaceControl()));
+ taskInfos.add(new TaskAppearedInfo(task2, new SurfaceControl()));
+ doReturn(new ParceledListSlice(taskInfos))
+ .when(mTaskOrganizerController).registerTaskOrganizer(any());
+
+ // Register and expect the tasks to be stored
+ mOrganizer.registerOrganizer();
+
+ // Check that the tasks are next reported when the listener is added
+ TrackingTaskListener listener = new TrackingTaskListener();
+ mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW);
+ assertTrue(listener.appeared.contains(task1));
+ assertTrue(listener.appeared.contains(task2));
+ }
+
+ @Test
+ public void testAppearedVanished() {
+ RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ TrackingTaskListener listener = new TrackingTaskListener();
+ mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW);
+ mOrganizer.onTaskAppeared(taskInfo, null);
+ assertTrue(listener.appeared.contains(taskInfo));
+
+ mOrganizer.onTaskVanished(taskInfo);
+ assertTrue(listener.vanished.contains(taskInfo));
+ }
+
+ @Test
+ public void testAddListenerExistingTasks() {
+ RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ mOrganizer.onTaskAppeared(taskInfo, null);
+
+ TrackingTaskListener listener = new TrackingTaskListener();
+ mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW);
+ assertTrue(listener.appeared.contains(taskInfo));
+ }
+
+ @Test
+ public void testWindowingModeChange() {
+ RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ TrackingTaskListener mwListener = new TrackingTaskListener();
+ TrackingTaskListener pipListener = new TrackingTaskListener();
+ mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
+ mOrganizer.addListenerForType(pipListener, TASK_LISTENER_TYPE_PIP);
+ mOrganizer.onTaskAppeared(taskInfo, null);
+ assertTrue(mwListener.appeared.contains(taskInfo));
+ assertTrue(pipListener.appeared.isEmpty());
+
+ taskInfo = createTaskInfo(1, WINDOWING_MODE_PINNED);
+ mOrganizer.onTaskInfoChanged(taskInfo);
+ assertTrue(mwListener.vanished.contains(taskInfo));
+ assertTrue(pipListener.appeared.contains(taskInfo));
+ }
+
+ @Test
+ public void testAddListenerForTaskId_afterTypeListener() {
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ TrackingTaskListener mwListener = new TrackingTaskListener();
+ TrackingTaskListener task1Listener = new TrackingTaskListener();
+ mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
+ mOrganizer.onTaskAppeared(task1, null);
+ assertTrue(mwListener.appeared.contains(task1));
+
+ // Add task 1 specific listener
+ mOrganizer.addListenerForTaskId(task1Listener, 1);
+ assertTrue(mwListener.vanished.contains(task1));
+ assertTrue(task1Listener.appeared.contains(task1));
+ }
+
+ @Test
+ public void testAddListenerForTaskId_beforeTypeListener() {
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ TrackingTaskListener mwListener = new TrackingTaskListener();
+ TrackingTaskListener task1Listener = new TrackingTaskListener();
+ mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.addListenerForTaskId(task1Listener, 1);
+ assertTrue(task1Listener.appeared.contains(task1));
+
+ mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
+ assertFalse(mwListener.appeared.contains(task1));
+ }
+
+ @Test
+ public void testGetTaskListener() {
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+
+ TrackingTaskListener mwListener = new TrackingTaskListener();
+ mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
+
+ TrackingTaskListener cookieListener = new TrackingTaskListener();
+ IBinder cookie = new Binder();
+ task1.addLaunchCookie(cookie);
+ mOrganizer.setPendingLaunchCookieListener(cookie, cookieListener);
+
+ // Priority goes to the cookie listener so we would expect the task appear to show up there
+ // instead of the multi-window type listener.
+ mOrganizer.onTaskAppeared(task1, null);
+ assertTrue(cookieListener.appeared.contains(task1));
+ assertFalse(mwListener.appeared.contains(task1));
+
+ TrackingTaskListener task1Listener = new TrackingTaskListener();
+
+ boolean gotException = false;
+ try {
+ mOrganizer.addListenerForTaskId(task1Listener, 1);
+ } catch (Exception e) {
+ gotException = true;
+ }
+ // It should not be possible to add a task id listener for a task already mapped to a
+ // listener through cookie.
+ assertTrue(gotException);
+ }
+
+ private RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
+ RunningTaskInfo taskInfo = new RunningTaskInfo();
+ taskInfo.taskId = taskId;
+ taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+ return taskInfo;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt
new file mode 100644
index 000000000000..4bd9bed26a82
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt
@@ -0,0 +1,627 @@
+/*
+ * 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.animation
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.util.ArrayMap
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringForce
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.wm.shell.animation.PhysicsAnimator.EndListener
+import com.android.wm.shell.animation.PhysicsAnimator.UpdateListener
+import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames
+import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames
+import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@Ignore("Blocking presubmits - investigating in b/158697054")
+class PhysicsAnimatorTest : SysuiTestCase() {
+ private lateinit var viewGroup: ViewGroup
+ private lateinit var testView: View
+ private lateinit var testView2: View
+
+ private lateinit var animator: PhysicsAnimator<View>
+
+ private val springConfig = PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY)
+ private val flingConfig = PhysicsAnimator.FlingConfig(2f)
+
+ private lateinit var mockUpdateListener: UpdateListener<View>
+ private lateinit var mockEndListener: EndListener<View>
+ private lateinit var mockEndAction: Runnable
+
+ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ mockUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View>
+ mockEndListener = mock(EndListener::class.java) as EndListener<View>
+ mockEndAction = mock(Runnable::class.java)
+
+ viewGroup = FrameLayout(context)
+ testView = View(context)
+ testView2 = View(context)
+ viewGroup.addView(testView)
+ viewGroup.addView(testView2)
+
+ PhysicsAnimatorTestUtils.prepareForTest()
+
+ // Most of our tests involve checking the end state of animations, so we want calls that
+ // start animations to block the test thread until the animations have ended.
+ PhysicsAnimatorTestUtils.setAllAnimationsBlock(true)
+
+ animator = PhysicsAnimator.getInstance(testView)
+ }
+
+ @After
+ fun tearDown() {
+ PhysicsAnimatorTestUtils.tearDown()
+ }
+
+ @Test
+ fun testOneAnimatorPerView() {
+ assertEquals(animator, PhysicsAnimator.getInstance(testView))
+ assertEquals(PhysicsAnimator.getInstance(testView), PhysicsAnimator.getInstance(testView))
+ assertNotEquals(animator, PhysicsAnimator.getInstance(testView2))
+ }
+
+ @Test
+ fun testSpringOneProperty() {
+ animator
+ .spring(DynamicAnimation.TRANSLATION_X, 50f, springConfig)
+ .start()
+
+ assertEquals(testView.translationX, 50f, 1f)
+ }
+
+ @Test
+ fun testSpringMultipleProperties() {
+ animator
+ .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig)
+ .spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig)
+ .spring(DynamicAnimation.SCALE_Y, 1.1f, springConfig)
+ .start()
+
+ assertEquals(10f, testView.translationX, 1f)
+ assertEquals(50f, testView.translationY, 1f)
+ assertEquals(1.1f, testView.scaleY, 0.01f)
+ }
+
+ @Test
+ fun testFling() {
+ val startTime = System.currentTimeMillis()
+
+ animator
+ .fling(DynamicAnimation.TRANSLATION_X, 1000f /* startVelocity */, flingConfig)
+ .fling(DynamicAnimation.TRANSLATION_Y, 500f, flingConfig)
+ .start()
+
+ val elapsedTimeSeconds = (System.currentTimeMillis() - startTime) / 1000f
+
+ // If the fling worked, the view should be somewhere between its starting position and the
+ // and the theoretical no-friction maximum of startVelocity (in pixels per second)
+ // multiplied by elapsedTimeSeconds. We can't calculate an exact expected location for a
+ // fling, so this is close enough.
+ assertTrue(testView.translationX > 0f)
+ assertTrue(testView.translationX < 1000f * elapsedTimeSeconds)
+ assertTrue(testView.translationY > 0f)
+ assertTrue(testView.translationY < 500f * elapsedTimeSeconds)
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ @Ignore("Increasingly flaky")
+ fun testEndListenersAndActions() {
+ PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
+ animator
+ .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig)
+ .spring(DynamicAnimation.TRANSLATION_Y, 500f, springConfig)
+ .addEndListener(mockEndListener)
+ .withEndActions(mockEndAction::run)
+ .start()
+
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X)
+
+ // Once TRANSLATION_X is done, the view should be at x = 10...
+ assertEquals(10f, testView.translationX, 1f)
+
+ // / ...TRANSLATION_Y should still be running...
+ assertTrue(animator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
+
+ // ...and our end listener should have been called with x = 10, velocity = 0, and allEnded =
+ // false since TRANSLATION_Y is still running.
+ verify(mockEndListener).onAnimationEnd(
+ testView,
+ DynamicAnimation.TRANSLATION_X,
+ wasFling = false,
+ canceled = false,
+ finalValue = 10f,
+ finalVelocity = 0f,
+ allRelevantPropertyAnimsEnded = false)
+ verifyNoMoreInteractions(mockEndListener)
+
+ // The end action should not have been run yet.
+ verify(mockEndAction, times(0)).run()
+
+ // Block until TRANSLATION_Y finishes.
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y)
+
+ // The view should have been moved.
+ assertEquals(10f, testView.translationX, 1f)
+ assertEquals(500f, testView.translationY, 1f)
+
+ // The end listener should have been called, this time with TRANSLATION_Y, y = 50, and
+ // allEnded = true.
+ verify(mockEndListener).onAnimationEnd(
+ testView,
+ DynamicAnimation.TRANSLATION_Y,
+ wasFling = false,
+ canceled = false,
+ finalValue = 500f,
+ finalVelocity = 0f,
+ allRelevantPropertyAnimsEnded = true)
+ verifyNoMoreInteractions(mockEndListener)
+
+ // Now that all properties are done animating, the end action should have been called.
+ verify(mockEndAction, times(1)).run()
+ }
+
+ @Test
+ fun testUpdateListeners() {
+ animator
+ .spring(DynamicAnimation.TRANSLATION_X, 100f, springConfig)
+ .spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig)
+ .addUpdateListener(object : UpdateListener<View> {
+ override fun onAnimationUpdateForProperty(
+ target: View,
+ values: UpdateMap<View>
+ ) {
+ mockUpdateListener.onAnimationUpdateForProperty(target, values)
+ }
+ })
+ .start()
+
+ verifyUpdateListenerCalls(animator, mockUpdateListener)
+ }
+
+ @Test
+ fun testListenersNotCalledOnSubsequentAnimations() {
+ animator
+ .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig)
+ .addUpdateListener(mockUpdateListener)
+ .addEndListener(mockEndListener)
+ .withEndActions(mockEndAction::run)
+ .start()
+
+ verifyUpdateListenerCalls(animator, mockUpdateListener)
+ verify(mockEndListener, times(1)).onAnimationEnd(
+ eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(false), anyFloat(),
+ anyFloat(), eq(true))
+ verify(mockEndAction, times(1)).run()
+
+ animator
+ .spring(DynamicAnimation.TRANSLATION_X, 0f, springConfig)
+ .start()
+
+ // We didn't pass any of the listeners/actions to the subsequent animation, so they should
+ // never have been called.
+ verifyNoMoreInteractions(mockUpdateListener)
+ verifyNoMoreInteractions(mockEndListener)
+ verifyNoMoreInteractions(mockEndAction)
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun testAnimationsUpdatedWhileInMotion() {
+ PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
+
+ // Spring towards x = 100f.
+ animator
+ .spring(
+ DynamicAnimation.TRANSLATION_X,
+ 100f,
+ springConfig)
+ .start()
+
+ // Block until it reaches x = 50f.
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
+ animator) { view -> view.translationX > 50f }
+
+ // Translation X value at the time of reversing the animation to spring to x = 0f.
+ val reversalTranslationX = testView.translationX
+
+ // Spring back towards 0f.
+ animator
+ .spring(
+ DynamicAnimation.TRANSLATION_X,
+ 0f,
+ // Lower the stiffness to ensure the update listener receives at least one
+ // update frame where the view has continued to move to the right.
+ springConfig.apply { stiffness = SpringForce.STIFFNESS_LOW })
+ .start()
+
+ // Wait for TRANSLATION_X.
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X)
+
+ // Verify that the animation continued past the X value at the time of reversal, before
+ // springing back. This ensures the change in direction was not abrupt.
+ verifyAnimationUpdateFrames(
+ animator, DynamicAnimation.TRANSLATION_X,
+ { u -> u.value > reversalTranslationX },
+ { u -> u.value < reversalTranslationX })
+
+ // Verify that the view is where it should be.
+ assertEquals(0f, testView.translationX, 1f)
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ @Ignore("Sporadically flaking.")
+ fun testAnimationsUpdatedWhileInMotion_originalListenersStillCalled() {
+ PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
+
+ // Spring TRANSLATION_X to 100f, with an update and end listener provided.
+ animator
+ .spring(
+ DynamicAnimation.TRANSLATION_X,
+ 100f,
+ // Use very low stiffness to ensure that all of the keyframes we're testing
+ // for are reported to the update listener.
+ springConfig.apply { stiffness = SpringForce.STIFFNESS_VERY_LOW })
+ .addUpdateListener(mockUpdateListener)
+ .addEndListener(mockEndListener)
+ .start()
+
+ // Wait until the animation is halfway there.
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
+ animator) { view -> view.translationX > 50f }
+
+ // The end listener shouldn't have been called since the animation hasn't ended.
+ verifyNoMoreInteractions(mockEndListener)
+
+ // Make sure we called the update listener with appropriate values.
+ verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
+ { u -> u.value > 0f },
+ { u -> u.value >= 50f })
+
+ // Mock a second end listener.
+ val secondEndListener = mock(EndListener::class.java) as EndListener<View>
+ val secondUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View>
+
+ // Start a new animation that springs both TRANSLATION_X and TRANSLATION_Y, and provide it
+ // the second end listener. This new end listener should be called for the end of
+ // TRANSLATION_X and TRANSLATION_Y, with allEnded = true when both have ended.
+ animator
+ .spring(DynamicAnimation.TRANSLATION_X, 200f, springConfig)
+ .spring(DynamicAnimation.TRANSLATION_Y, 4000f, springConfig)
+ .addUpdateListener(secondUpdateListener)
+ .addEndListener(secondEndListener)
+ .start()
+
+ // Wait for TRANSLATION_X to end.
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X)
+
+ // The update listener provided to the initial animation call (the one that only animated
+ // TRANSLATION_X) should have been called with values on the way to x = 200f. This is
+ // because the second animation call updated the original TRANSLATION_X animation.
+ verifyAnimationUpdateFrames(
+ animator, DynamicAnimation.TRANSLATION_X,
+ { u -> u.value > 100f }, { u -> u.value >= 200f })
+
+ // The original end listener should also have been called, with allEnded = true since it was
+ // provided to an animator that animated only TRANSLATION_X.
+ verify(mockEndListener, times(1))
+ .onAnimationEnd(
+ testView, DynamicAnimation.TRANSLATION_X,
+ wasFling = false,
+ canceled = false,
+ finalValue = 200f,
+ finalVelocity = 0f,
+ allRelevantPropertyAnimsEnded = true)
+ verifyNoMoreInteractions(mockEndListener)
+
+ // The second end listener should have been called, but with allEnded = false since it was
+ // provided to an animator that animated both TRANSLATION_X and TRANSLATION_Y.
+ verify(secondEndListener, times(1))
+ .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
+ wasFling = false,
+ canceled = false,
+ finalValue = 200f,
+ finalVelocity = 0f,
+ allRelevantPropertyAnimsEnded = false)
+ verifyNoMoreInteractions(secondEndListener)
+
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y)
+
+ // The original end listener shouldn't receive any callbacks because it was not provided to
+ // an animator that animated TRANSLATION_Y.
+ verifyNoMoreInteractions(mockEndListener)
+
+ verify(secondEndListener, times(1))
+ .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y,
+ wasFling = false,
+ canceled = false,
+ finalValue = 4000f,
+ finalVelocity = 0f,
+ allRelevantPropertyAnimsEnded = true)
+ verifyNoMoreInteractions(secondEndListener)
+ }
+
+ @Test
+ fun testFlingRespectsMinMax() {
+ animator
+ .fling(DynamicAnimation.TRANSLATION_X,
+ startVelocity = 1000f,
+ friction = 1.1f,
+ max = 10f)
+ .addEndListener(mockEndListener)
+ .start()
+
+ // Ensure that the view stopped at x = 10f, and the end listener was called once with that
+ // value.
+ assertEquals(10f, testView.translationX, 1f)
+ verify(mockEndListener, times(1))
+ .onAnimationEnd(
+ eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
+ eq(10f), anyFloat(), eq(true))
+
+ animator
+ .fling(
+ DynamicAnimation.TRANSLATION_X,
+ startVelocity = -1000f,
+ friction = 1.1f,
+ min = -5f)
+ .addEndListener(mockEndListener)
+ .start()
+
+ // Ensure that the view stopped at x = -5f, and the end listener was called once with that
+ // value.
+ assertEquals(-5f, testView.translationX, 1f)
+ verify(mockEndListener, times(1))
+ .onAnimationEnd(
+ eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
+ eq(-5f), anyFloat(), eq(true))
+ }
+
+ @Test
+ fun testIsPropertyAnimating() {
+ PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
+
+ testView.physicsAnimator
+ .spring(DynamicAnimation.TRANSLATION_X, 500f, springConfig)
+ .fling(DynamicAnimation.TRANSLATION_Y, 10f, flingConfig)
+ .spring(DynamicAnimation.TRANSLATION_Z, 1000f, springConfig)
+ .start()
+
+ // All of the properties we just started should be animating.
+ assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X))
+ assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
+ assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z))
+
+ // Block until x and y end.
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(testView.physicsAnimator,
+ DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)
+
+ // Verify that x and y are no longer animating, but that Z is (it's springing to 1000f).
+ assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X))
+ assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
+ assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z))
+
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Z)
+
+ assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X))
+ assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
+ assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z))
+ }
+
+ @Test
+ fun testExtensionProperty() {
+ testView
+ .physicsAnimator
+ .spring(DynamicAnimation.TRANSLATION_X, 200f)
+ .start()
+
+ assertEquals(200f, testView.translationX, 1f)
+ }
+
+ @Test
+ @Ignore("Sporadically flaking.")
+ fun testFlingThenSpring() {
+ PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
+
+ // Start at 500f and fling hard to the left. We should quickly reach the 250f minimum, fly
+ // past it since there's so much velocity remaining, then spring back to 250f.
+ testView.translationX = 500f
+ animator
+ .flingThenSpring(
+ DynamicAnimation.TRANSLATION_X,
+ -5000f,
+ flingConfig.apply { min = 250f },
+ springConfig)
+ .addUpdateListener(mockUpdateListener)
+ .addEndListener(mockEndListener)
+ .withEndActions(mockEndAction::run)
+ .start()
+
+ // Block until we pass the minimum.
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
+ animator) { v -> v.translationX <= 250f }
+
+ // Double check that the view is there.
+ assertTrue(testView.translationX <= 250f)
+
+ // The update listener should have been called with a value < 500f, and then a value less
+ // than or equal to the 250f minimum.
+ verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
+ { u -> u.value < 500f },
+ { u -> u.value <= 250f })
+
+ // Despite the fact that the fling has ended, the end listener shouldn't have been called
+ // since we're about to begin springing the same property.
+ verifyNoMoreInteractions(mockEndListener)
+ verifyNoMoreInteractions(mockEndAction)
+
+ // Wait for the spring to finish.
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_X)
+
+ // Make sure we continued past 250f since the spring should have been started with some
+ // remaining negative velocity from the fling.
+ verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
+ { u -> u.value < 250f })
+
+ // At this point, the animation end listener should have been called once, and only once,
+ // when the spring ended at 250f.
+ verify(mockEndListener).onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
+ wasFling = false,
+ canceled = false,
+ finalValue = 250f,
+ finalVelocity = 0f,
+ allRelevantPropertyAnimsEnded = true)
+ verifyNoMoreInteractions(mockEndListener)
+
+ // The end action should also have been called once.
+ verify(mockEndAction, times(1)).run()
+ verifyNoMoreInteractions(mockEndAction)
+
+ assertEquals(250f, testView.translationX)
+ }
+
+ @Test
+ fun testFlingThenSpring_objectOutsideFlingBounds() {
+ // Start the view at x = -500, well outside the fling bounds of min = 0f, with strong
+ // negative velocity.
+ testView.translationX = -500f
+ animator
+ .flingThenSpring(
+ DynamicAnimation.TRANSLATION_X,
+ -5000f,
+ flingConfig.apply { min = 0f },
+ springConfig)
+ .addUpdateListener(mockUpdateListener)
+ .addEndListener(mockEndListener)
+ .withEndActions(mockEndAction::run)
+ .start()
+
+ // The initial -5000f velocity should result in frames to the left of -500f before the view
+ // springs back towards 0f.
+ verifyAnimationUpdateFrames(
+ animator, DynamicAnimation.TRANSLATION_X,
+ { u -> u.value < -500f },
+ { u -> u.value > -500f })
+
+ // We should end up at the fling min.
+ assertEquals(0f, testView.translationX, 1f)
+ }
+
+ @Test
+ fun testFlingToMinMaxThenSpring() {
+ // Start at x = 500f.
+ testView.translationX = 500f
+
+ // Fling to the left at the very sad rate of -1 pixels per second. That won't get us much of
+ // anywhere, and certainly not to the 0f min.
+ animator
+ // Good thing we have flingToMinMaxThenSpring!
+ .flingThenSpring(
+ DynamicAnimation.TRANSLATION_X,
+ -10000f,
+ flingConfig.apply { min = 0f },
+ springConfig,
+ flingMustReachMinOrMax = true)
+ .addUpdateListener(mockUpdateListener)
+ .addEndListener(mockEndListener)
+ .withEndActions(mockEndAction::run)
+ .start()
+
+ // Thanks, flingToMinMaxThenSpring, for adding enough velocity to get us here.
+ assertEquals(0f, testView.translationX, 1f)
+ }
+
+ /**
+ * Verifies that the calls to the mock update listener match the animation update frames
+ * reported by the test internal listener, in order.
+ */
+ private fun <T : Any> verifyUpdateListenerCalls(
+ animator: PhysicsAnimator<T>,
+ mockUpdateListener: UpdateListener<T>
+ ) {
+ val updates = getAnimationUpdateFrames(animator)
+
+ for (invocation in Mockito.mockingDetails(mockUpdateListener).invocations) {
+
+ // Grab the update map of Property -> AnimationUpdate that was passed to the mock update
+ // listener.
+ val updateMap = invocation.arguments[1]
+ as ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
+
+ //
+ for ((property, update) in updateMap) {
+ val updatesForProperty = updates[property]!!
+
+ // This update should be the next one in the list for this property.
+ if (update != updatesForProperty[0]) {
+ Assert.fail("The update listener was called with an unexpected value: $update.")
+ }
+
+ updatesForProperty.remove(update)
+ }
+
+ val target = animator.weakTarget.get()
+ assertNotNull(target)
+ // Mark this invocation verified.
+ verify(mockUpdateListener).onAnimationUpdateForProperty(target!!, updateMap)
+ }
+
+ verifyNoMoreInteractions(mockUpdateListener)
+
+ // Since we were removing values as matching invocations were found, there should no longer
+ // be any values remaining. If there are, it means the update listener wasn't notified when
+ // it should have been.
+ assertEquals(0,
+ updates.values.fold(0, { count, propertyUpdates -> count + propertyUpdates.size }))
+
+ clearAnimationUpdateFrames(animator)
+ }
+}
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
new file mode 100644
index 000000000000..080cddc58a09
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.view.Display.DEFAULT_DISPLAY;
+import static android.view.InsetsState.ITYPE_IME;
+import static android.view.Surface.ROTATION_0;
+
+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.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.view.IInputMethodManager;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class DisplayImeControllerTest {
+
+ private SurfaceControl.Transaction mT;
+ private DisplayImeController.PerDisplay mPerDisplay;
+ private IInputMethodManager mMock;
+
+ @Before
+ public void setUp() throws Exception {
+ mT = mock(SurfaceControl.Transaction.class);
+ mMock = mock(IInputMethodManager.class);
+ mPerDisplay = new DisplayImeController(null, null, Runnable::run, new TransactionPool() {
+ @Override
+ public SurfaceControl.Transaction acquire() {
+ return mT;
+ }
+
+ @Override
+ public void release(SurfaceControl.Transaction t) {
+ }
+ }) {
+ @Override
+ public IInputMethodManager getImms() {
+ return mMock;
+ }
+ }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
+ }
+
+ @Test
+ public void reappliesVisibilityToChangedLeash() {
+ verifyZeroInteractions(mT);
+
+ mPerDisplay.mImeShowing = false;
+ mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[] {
+ new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0))
+ });
+
+ verify(mT).hide(any());
+
+ mPerDisplay.mImeShowing = true;
+ mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[] {
+ new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0))
+ });
+
+ verify(mT).show(any());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
new file mode 100644
index 000000000000..2b5b77e49e3a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 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.content.res.Configuration.UI_MODE_TYPE_NORMAL;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.view.DisplayCutout;
+import android.view.DisplayInfo;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+
+import org.junit.Test;
+
+@SmallTest
+public class DisplayLayoutTest {
+
+ @Test
+ public void testInsets() {
+ Resources res = createResources(40, 50, false, 30, 40);
+ // Test empty display, no bars or anything
+ DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0);
+ DisplayLayout dl = new DisplayLayout(info, res, false, false);
+ assertEquals(new Rect(0, 0, 0, 0), dl.stableInsets());
+ assertEquals(new Rect(0, 0, 0, 0), dl.nonDecorInsets());
+
+ // Test with bars
+ dl = new DisplayLayout(info, res, true, true);
+ assertEquals(new Rect(0, 40, 0, 50), dl.stableInsets());
+ assertEquals(new Rect(0, 0, 0, 50), dl.nonDecorInsets());
+
+ // Test just cutout
+ info = createDisplayInfo(1000, 1500, 60, ROTATION_0);
+ dl = new DisplayLayout(info, res, false, false);
+ assertEquals(new Rect(0, 60, 0, 0), dl.stableInsets());
+ assertEquals(new Rect(0, 60, 0, 0), dl.nonDecorInsets());
+
+ // Test with bars and cutout
+ dl = new DisplayLayout(info, res, true, true);
+ assertEquals(new Rect(0, 60, 0, 50), dl.stableInsets());
+ assertEquals(new Rect(0, 60, 0, 50), dl.nonDecorInsets());
+ }
+
+ @Test
+ public void testRotate() {
+ // Basic rotate utility
+ Rect testParent = new Rect(0, 0, 1000, 600);
+ Rect testInner = new Rect(40, 20, 120, 80);
+ Rect testResult = new Rect(testInner);
+ DisplayLayout.rotateBounds(testResult, testParent, 1);
+ assertEquals(new Rect(20, 880, 80, 960), testResult);
+ testResult.set(testInner);
+ DisplayLayout.rotateBounds(testResult, testParent, 2);
+ assertEquals(new Rect(880, 20, 960, 80), testResult);
+ testResult.set(testInner);
+ DisplayLayout.rotateBounds(testResult, testParent, 3);
+ assertEquals(new Rect(520, 40, 580, 120), testResult);
+
+ Resources res = createResources(40, 50, false, 30, 40);
+ DisplayInfo info = createDisplayInfo(1000, 1500, 60, ROTATION_0);
+ DisplayLayout dl = new DisplayLayout(info, res, true, true);
+ assertEquals(new Rect(0, 60, 0, 50), dl.stableInsets());
+ assertEquals(new Rect(0, 60, 0, 50), dl.nonDecorInsets());
+
+ // Rotate to 90
+ dl.rotateTo(res, ROTATION_90);
+ assertEquals(new Rect(60, 30, 0, 40), dl.stableInsets());
+ assertEquals(new Rect(60, 0, 0, 40), dl.nonDecorInsets());
+
+ // Rotate with moving navbar
+ res = createResources(40, 50, true, 30, 40);
+ dl = new DisplayLayout(info, res, true, true);
+ dl.rotateTo(res, ROTATION_270);
+ assertEquals(new Rect(40, 30, 60, 0), dl.stableInsets());
+ assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets());
+ }
+
+ private Resources createResources(
+ int navLand, int navPort, boolean navMoves, int statusLand, int statusPort) {
+ Configuration cfg = new Configuration();
+ cfg.uiMode = UI_MODE_TYPE_NORMAL;
+ Resources res = mock(Resources.class);
+ doReturn(navLand).when(res).getDimensionPixelSize(
+ R.dimen.navigation_bar_height_landscape_car_mode);
+ doReturn(navPort).when(res).getDimensionPixelSize(R.dimen.navigation_bar_height_car_mode);
+ doReturn(navLand).when(res).getDimensionPixelSize(R.dimen.navigation_bar_width_car_mode);
+ doReturn(navLand).when(res).getDimensionPixelSize(R.dimen.navigation_bar_height_landscape);
+ doReturn(navPort).when(res).getDimensionPixelSize(R.dimen.navigation_bar_height);
+ doReturn(navLand).when(res).getDimensionPixelSize(R.dimen.navigation_bar_width);
+ doReturn(navMoves).when(res).getBoolean(R.bool.config_navBarCanMove);
+ doReturn(statusLand).when(res).getDimensionPixelSize(R.dimen.status_bar_height_landscape);
+ doReturn(statusPort).when(res).getDimensionPixelSize(R.dimen.status_bar_height_portrait);
+ doReturn(cfg).when(res).getConfiguration();
+ return res;
+ }
+
+ private DisplayInfo createDisplayInfo(int width, int height, int cutoutHeight, int rotation) {
+ DisplayInfo info = new DisplayInfo();
+ info.logicalWidth = width;
+ info.logicalHeight = height;
+ info.rotation = rotation;
+ if (cutoutHeight > 0) {
+ info.displayCutout = new DisplayCutout(
+ Insets.of(0, cutoutHeight, 0, 0) /* safeInsets */, null /* boundLeft */,
+ new Rect(width / 2 - cutoutHeight, 0, width / 2 + cutoutHeight,
+ cutoutHeight) /* boundTop */, null /* boundRight */,
+ null /* boundBottom */);
+ } else {
+ info.displayCutout = DisplayCutout.NO_CUTOUT;
+ }
+ info.logicalDensityDpi = 300;
+ return info;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
new file mode 100644
index 000000000000..fe536411d5ed
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
@@ -0,0 +1,454 @@
+/*
+ * 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.magnetictarget
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.MotionEvent
+import android.view.View
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.animation.PhysicsAnimatorTestUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class MagnetizedObjectTest : SysuiTestCase() {
+ /** Incrementing value for fake MotionEvent timestamps. */
+ private var time = 0L
+
+ /** Value to add to each new MotionEvent's timestamp. */
+ private var timeStep = 100
+
+ private val underlyingObject = this
+
+ private lateinit var targetView: View
+
+ private val targetSize = 200
+ private val targetCenterX = 500
+ private val targetCenterY = 900
+ private val magneticFieldRadius = 200
+
+ private var objectX = 0f
+ private var objectY = 0f
+ private val objectSize = 50f
+
+ private lateinit var magneticTarget: MagnetizedObject.MagneticTarget
+ private lateinit var magnetizedObject: MagnetizedObject<*>
+ private lateinit var magnetListener: MagnetizedObject.MagnetListener
+
+ private val xProperty = object : FloatPropertyCompat<MagnetizedObjectTest>("") {
+ override fun setValue(target: MagnetizedObjectTest?, value: Float) {
+ objectX = value
+ }
+ override fun getValue(target: MagnetizedObjectTest?): Float {
+ return objectX
+ }
+ }
+
+ private val yProperty = object : FloatPropertyCompat<MagnetizedObjectTest>("") {
+ override fun setValue(target: MagnetizedObjectTest?, value: Float) {
+ objectY = value
+ }
+
+ override fun getValue(target: MagnetizedObjectTest?): Float {
+ return objectY
+ }
+ }
+
+ @Before
+ fun setup() {
+ PhysicsAnimatorTestUtils.prepareForTest()
+
+ // Mock the view since a real view's getLocationOnScreen() won't work unless it's attached
+ // to a real window (it'll always return x = 0, y = 0).
+ targetView = mock(View::class.java)
+ `when`(targetView.context).thenReturn(context)
+
+ // The mock target view will pretend that it's 200x200, and at (400, 800). This means it's
+ // occupying the bounds (400, 800, 600, 1000) and it has a center of (500, 900).
+ `when`(targetView.width).thenReturn(targetSize) // width = 200
+ `when`(targetView.height).thenReturn(targetSize) // height = 200
+ doAnswer { invocation ->
+ (invocation.arguments[0] as IntArray).also { location ->
+ // Return the top left of the target.
+ location[0] = targetCenterX - targetSize / 2 // x = 400
+ location[1] = targetCenterY - targetSize / 2 // y = 800
+ }
+ }.`when`(targetView).getLocationOnScreen(ArgumentMatchers.any())
+ doAnswer { invocation ->
+ (invocation.arguments[0] as Runnable).run()
+ true
+ }.`when`(targetView).post(ArgumentMatchers.any())
+ `when`(targetView.context).thenReturn(context)
+
+ magneticTarget = MagnetizedObject.MagneticTarget(targetView, magneticFieldRadius)
+
+ magnetListener = mock(MagnetizedObject.MagnetListener::class.java)
+ magnetizedObject = object : MagnetizedObject<MagnetizedObjectTest>(
+ context, underlyingObject, xProperty, yProperty) {
+ override fun getWidth(underlyingObject: MagnetizedObjectTest): Float {
+ return objectSize
+ }
+
+ override fun getHeight(underlyingObject: MagnetizedObjectTest): Float {
+ return objectSize
+ }
+
+ override fun getLocationOnScreen(
+ underlyingObject: MagnetizedObjectTest,
+ loc: IntArray
+ ) {
+ loc[0] = objectX.toInt()
+ loc[1] = objectY.toInt() }
+ }
+
+ magnetizedObject.magnetListener = magnetListener
+ magnetizedObject.addTarget(magneticTarget)
+
+ timeStep = 100
+ }
+
+ @Test
+ fun testMotionEventConsumption() {
+ // Start at (0, 0). No magnetic field here.
+ assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = 0, y = 0, action = MotionEvent.ACTION_DOWN)))
+
+ // Move to (400, 400), which is solidly outside the magnetic field.
+ assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = 200, y = 200)))
+
+ // Move to (305, 705). This would be in the magnetic field radius if magnetic fields were
+ // square. It's not, because they're not.
+ assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = targetCenterX - magneticFieldRadius + 5,
+ y = targetCenterY - magneticFieldRadius + 5)))
+
+ // Move to (400, 800). That's solidly in the radius so the magnetic target should begin
+ // consuming events.
+ assertTrue(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = targetCenterX - 100,
+ y = targetCenterY - 100)))
+
+ // Release at (400, 800). Since we're in the magnetic target, it should return true and
+ // consume the ACTION_UP.
+ assertTrue(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = 400, y = 800, action = MotionEvent.ACTION_UP)))
+
+ // ACTION_DOWN outside the field.
+ assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = 200, y = 200, action = MotionEvent.ACTION_DOWN)))
+
+ // Move to the center. We absolutely should consume events there.
+ assertTrue(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = targetCenterX,
+ y = targetCenterY)))
+
+ // Drag out to (0, 0) and we should be returning false again.
+ assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = 0, y = 0)))
+
+ // The ACTION_UP event shouldn't be consumed either since it's outside the field.
+ assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = 0, y = 0, action = MotionEvent.ACTION_UP)))
+ }
+
+ @Test
+ fun testMotionEventConsumption_downInMagneticField() {
+ // We should not consume DOWN events even if they occur in the field.
+ assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = targetCenterX, y = targetCenterY, action = MotionEvent.ACTION_DOWN)))
+ }
+
+ @Test
+ fun testMoveIntoAroundAndOutOfMagneticField() {
+ // Move around but don't touch the magnetic field.
+ dispatchMotionEvents(
+ getMotionEvent(x = 0, y = 0, action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(x = 100, y = 100),
+ getMotionEvent(x = 200, y = 200))
+
+ // You can't become unstuck if you were never stuck in the first place.
+ verify(magnetListener, never()).onStuckToTarget(magneticTarget)
+ verify(magnetListener, never()).onUnstuckFromTarget(
+ eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
+ eq(false))
+
+ // Move into and then around inside the magnetic field.
+ dispatchMotionEvents(
+ getMotionEvent(x = targetCenterX - 100, y = targetCenterY - 100),
+ getMotionEvent(x = targetCenterX, y = targetCenterY),
+ getMotionEvent(x = targetCenterX + 100, y = targetCenterY + 100))
+
+ // We should only have received one call to onStuckToTarget and none to unstuck.
+ verify(magnetListener, times(1)).onStuckToTarget(magneticTarget)
+ verify(magnetListener, never()).onUnstuckFromTarget(
+ eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
+ eq(false))
+
+ // Move out of the field and then release.
+ dispatchMotionEvents(
+ getMotionEvent(x = 100, y = 100),
+ getMotionEvent(x = 100, y = 100, action = MotionEvent.ACTION_UP))
+
+ // We should have received one unstuck call and no more stuck calls. We also should never
+ // have received an onReleasedInTarget call.
+ verify(magnetListener, times(1)).onUnstuckFromTarget(
+ eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
+ eq(false))
+ verifyNoMoreInteractions(magnetListener)
+ }
+
+ @Test
+ fun testMoveIntoOutOfAndBackIntoMagneticField() {
+ // Move into the field
+ dispatchMotionEvents(
+ getMotionEvent(
+ x = targetCenterX - magneticFieldRadius,
+ y = targetCenterY - magneticFieldRadius,
+ action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(
+ x = targetCenterX, y = targetCenterY))
+
+ verify(magnetListener, times(1)).onStuckToTarget(magneticTarget)
+ verify(magnetListener, never()).onReleasedInTarget(magneticTarget)
+
+ // Move back out.
+ dispatchMotionEvents(
+ getMotionEvent(
+ x = targetCenterX - magneticFieldRadius,
+ y = targetCenterY - magneticFieldRadius))
+
+ verify(magnetListener, times(1)).onUnstuckFromTarget(
+ eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
+ eq(false))
+ verify(magnetListener, never()).onReleasedInTarget(magneticTarget)
+
+ // Move in again and release in the magnetic field.
+ dispatchMotionEvents(
+ getMotionEvent(x = targetCenterX - 100, y = targetCenterY - 100),
+ getMotionEvent(x = targetCenterX + 50, y = targetCenterY + 50),
+ getMotionEvent(x = targetCenterX, y = targetCenterY),
+ getMotionEvent(
+ x = targetCenterX, y = targetCenterY, action = MotionEvent.ACTION_UP))
+
+ verify(magnetListener, times(2)).onStuckToTarget(magneticTarget)
+ verify(magnetListener).onReleasedInTarget(magneticTarget)
+ verifyNoMoreInteractions(magnetListener)
+ }
+
+ @Test
+ fun testFlingTowardsTarget_towardsTarget() {
+ timeStep = 10
+
+ // Forcefully fling the object towards the target (but never touch the magnetic field).
+ dispatchMotionEvents(
+ getMotionEvent(
+ x = targetCenterX,
+ y = 0,
+ action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(
+ x = targetCenterX,
+ y = targetCenterY / 2),
+ getMotionEvent(
+ x = targetCenterX,
+ y = targetCenterY - magneticFieldRadius * 2,
+ action = MotionEvent.ACTION_UP))
+
+ // Nevertheless it should have ended up stuck to the target.
+ verify(magnetListener, times(1)).onStuckToTarget(magneticTarget)
+ }
+
+ @Test
+ fun testFlingTowardsTarget_towardsButTooSlow() {
+ // Very, very slowly fling the object towards the target (but never touch the magnetic
+ // field). This value is only used to create MotionEvent timestamps, it will not block the
+ // test for 10 seconds.
+ timeStep = 10000
+ dispatchMotionEvents(
+ getMotionEvent(
+ x = targetCenterX,
+ y = 0,
+ action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(
+ x = targetCenterX,
+ y = targetCenterY / 2),
+ getMotionEvent(
+ x = targetCenterX,
+ y = targetCenterY - magneticFieldRadius * 2,
+ action = MotionEvent.ACTION_UP))
+
+ // No sticking should have occurred.
+ verifyNoMoreInteractions(magnetListener)
+ }
+
+ @Test
+ fun testFlingTowardsTarget_missTarget() {
+ timeStep = 10
+ // Forcefully fling the object down, but not towards the target.
+ dispatchMotionEvents(
+ getMotionEvent(
+ x = 0,
+ y = 0,
+ action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(
+ x = 0,
+ y = targetCenterY / 2),
+ getMotionEvent(
+ x = 0,
+ y = targetCenterY - magneticFieldRadius * 2,
+ action = MotionEvent.ACTION_UP))
+
+ verifyNoMoreInteractions(magnetListener)
+ }
+
+ @Test
+ fun testMagnetAnimation() {
+ // Make sure the object starts at (0, 0).
+ assertEquals(0f, objectX)
+ assertEquals(0f, objectY)
+
+ // Trigger the magnet animation, and block the test until it ends.
+ PhysicsAnimatorTestUtils.setAllAnimationsBlock(true)
+ magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = targetCenterX - 250,
+ y = targetCenterY - 250,
+ action = MotionEvent.ACTION_DOWN))
+
+ magnetizedObject.maybeConsumeMotionEvent(getMotionEvent(
+ x = targetCenterX,
+ y = targetCenterY))
+
+ // The object's (top-left) position should now position it centered over the target.
+ assertEquals(targetCenterX - objectSize / 2, objectX)
+ assertEquals(targetCenterY - objectSize / 2, objectY)
+ }
+
+ @Test
+ fun testMultipleTargets() {
+ val secondMagneticTarget = getSecondMagneticTarget()
+
+ // Drag into the second target.
+ dispatchMotionEvents(
+ getMotionEvent(x = 0, y = 0, action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(x = 100, y = 900))
+
+ // Verify that we received an onStuck for the second target, and no others.
+ verify(magnetListener).onStuckToTarget(secondMagneticTarget)
+ verifyNoMoreInteractions(magnetListener)
+
+ // Drag into the original target.
+ dispatchMotionEvents(
+ getMotionEvent(x = 0, y = 0),
+ getMotionEvent(x = 500, y = 900))
+
+ // We should have unstuck from the second one and stuck into the original one.
+ verify(magnetListener).onUnstuckFromTarget(
+ eq(secondMagneticTarget), anyFloat(), anyFloat(), eq(false))
+ verify(magnetListener).onStuckToTarget(magneticTarget)
+ verifyNoMoreInteractions(magnetListener)
+ }
+
+ @Test
+ fun testMultipleTargets_flingIntoSecond() {
+ val secondMagneticTarget = getSecondMagneticTarget()
+
+ timeStep = 10
+
+ // Fling towards the second target.
+ dispatchMotionEvents(
+ getMotionEvent(x = 100, y = 0, action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(x = 100, y = 350),
+ getMotionEvent(x = 100, y = 650, action = MotionEvent.ACTION_UP))
+
+ // Verify that we received an onStuck for the second target.
+ verify(magnetListener).onStuckToTarget(secondMagneticTarget)
+
+ // Fling towards the first target.
+ dispatchMotionEvents(
+ getMotionEvent(x = 300, y = 0, action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(x = 400, y = 350),
+ getMotionEvent(x = 500, y = 650, action = MotionEvent.ACTION_UP))
+
+ // Verify that we received onStuck for the original target.
+ verify(magnetListener).onStuckToTarget(magneticTarget)
+ }
+
+ private fun getSecondMagneticTarget(): MagnetizedObject.MagneticTarget {
+ // The first target view is at bounds (400, 800, 600, 1000) and it has a center of
+ // (500, 900). We'll add a second one at bounds (0, 800, 200, 1000) with center (100, 900).
+ val secondTargetView = mock(View::class.java)
+ var secondTargetCenterX = 100
+ var secondTargetCenterY = 900
+
+ `when`(secondTargetView.context).thenReturn(context)
+ `when`(secondTargetView.width).thenReturn(targetSize) // width = 200
+ `when`(secondTargetView.height).thenReturn(targetSize) // height = 200
+ doAnswer { invocation ->
+ (invocation.arguments[0] as Runnable).run()
+ true
+ }.`when`(secondTargetView).post(ArgumentMatchers.any())
+ doAnswer { invocation ->
+ (invocation.arguments[0] as IntArray).also { location ->
+ // Return the top left of the target.
+ location[0] = secondTargetCenterX - targetSize / 2 // x = 0
+ location[1] = secondTargetCenterY - targetSize / 2 // y = 800
+ }
+ }.`when`(secondTargetView).getLocationOnScreen(ArgumentMatchers.any())
+
+ return magnetizedObject.addTarget(secondTargetView, magneticFieldRadius)
+ }
+
+ /**
+ * Return a MotionEvent at the given coordinates, with the given action (or MOVE by default).
+ * The event's time fields will be incremented by 10ms each time this is called, so tha
+ * VelocityTracker works.
+ */
+ private fun getMotionEvent(
+ x: Int,
+ y: Int,
+ action: Int = MotionEvent.ACTION_MOVE
+ ): MotionEvent {
+ return MotionEvent.obtain(time, time, action, x.toFloat(), y.toFloat(), 0)
+ .also { time += timeStep }
+ }
+
+ /** Dispatch all of the provided events to the target view. */
+ private fun dispatchMotionEvents(vararg events: MotionEvent) {
+ events.forEach { magnetizedObject.maybeConsumeMotionEvent(it) }
+ }
+
+ /** Prevents Kotlin from being mad that eq() is nullable. */
+ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java
new file mode 100644
index 000000000000..a8a3a9fd7da2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests against {@link OneHandedAnimationController} to ensure that it sends the right
+ * callbacks
+ * depending on the various interactions.
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class OneHandedAnimationControllerTest extends OneHandedTestCase {
+ private static final int TEST_BOUNDS_WIDTH = 1000;
+ private static final int TEST_BOUNDS_HEIGHT = 1000;
+
+ OneHandedAnimationController mOneHandedAnimationController;
+ OneHandedTutorialHandler mTutorialHandler;
+
+ @Mock
+ private SurfaceControl mMockLeash;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mTutorialHandler = new OneHandedTutorialHandler(mContext);
+ mOneHandedAnimationController = new OneHandedAnimationController(mContext);
+ }
+
+ @Test
+ public void testGetAnimator_withSameBounds_returnAnimator() {
+ final Rect originalBounds = new Rect(0, 0, TEST_BOUNDS_WIDTH, TEST_BOUNDS_HEIGHT);
+ final Rect destinationBounds = originalBounds;
+ destinationBounds.offset(0, 300);
+ final OneHandedAnimationController.OneHandedTransitionAnimator animator =
+ mOneHandedAnimationController
+ .getAnimator(mMockLeash, originalBounds, destinationBounds);
+
+ assertNotNull(animator);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
new file mode 100644
index 000000000000..3645f1e56f92
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.om.IOverlayManager;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedControllerTest extends OneHandedTestCase {
+ Display mDisplay;
+ OneHandedController mOneHandedController;
+ OneHandedTimeoutHandler mTimeoutHandler;
+
+ @Mock
+ DisplayController mMockDisplayController;
+ @Mock
+ OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer;
+ @Mock
+ OneHandedTouchHandler mMockTouchHandler;
+ @Mock
+ OneHandedTutorialHandler mMockTutorialHandler;
+ @Mock
+ OneHandedGestureHandler mMockGestureHandler;
+ @Mock
+ OneHandedTimeoutHandler mMockTimeoutHandler;
+ @Mock
+ IOverlayManager mMockOverlayManager;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mDisplay = mContext.getDisplay();
+ OneHandedController oneHandedController = new OneHandedController(
+ mContext,
+ mMockDisplayController,
+ mMockDisplayAreaOrganizer,
+ mMockTouchHandler,
+ mMockTutorialHandler,
+ mMockGestureHandler,
+ mMockOverlayManager);
+ mOneHandedController = Mockito.spy(oneHandedController);
+ mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get());
+
+ when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay);
+ when(mMockDisplayAreaOrganizer.isInOneHanded()).thenReturn(false);
+ }
+
+ @Test
+ public void testDefaultShouldNotInOneHanded() {
+ final OneHandedAnimationController animationController = new OneHandedAnimationController(
+ mContext);
+ OneHandedDisplayAreaOrganizer displayAreaOrganizer = new OneHandedDisplayAreaOrganizer(
+ mContext, mMockDisplayController, animationController, mMockTutorialHandler);
+
+ assertThat(displayAreaOrganizer.isInOneHanded()).isFalse();
+ }
+
+ @Test
+ public void testRegisterOrganizer() {
+ verify(mMockDisplayAreaOrganizer, atLeastOnce()).registerOrganizer(anyInt());
+ }
+
+ @Test
+ public void testStartOneHanded() {
+ mOneHandedController.startOneHanded();
+
+ verify(mMockDisplayAreaOrganizer).scheduleOffset(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testStopOneHanded() {
+ when(mMockDisplayAreaOrganizer.isInOneHanded()).thenReturn(false);
+ mOneHandedController.stopOneHanded();
+
+ verify(mMockDisplayAreaOrganizer, never()).scheduleOffset(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testRegisterTransitionCallbackAfterInit() {
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockTouchHandler);
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockGestureHandler);
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockTutorialHandler);
+ }
+
+ @Test
+ public void testRegisterTransitionCallback() {
+ OneHandedTransitionCallback callback = new OneHandedTransitionCallback() {};
+ mOneHandedController.registerTransitionCallback(callback);
+
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(callback);
+ }
+
+
+ @Test
+ public void testStopOneHanded_shouldRemoveTimer() {
+ mOneHandedController.stopOneHanded();
+
+ verify(mTimeoutHandler).removeTimer();
+ }
+
+ @Test
+ public void testUpdateIsEnabled() {
+ final boolean enabled = true;
+ mOneHandedController.setOneHandedEnabled(enabled);
+
+ verify(mMockTouchHandler, atLeastOnce()).onOneHandedEnabled(enabled);
+ }
+
+ @Test
+ public void testUpdateSwipeToNotificationIsEnabled() {
+ final boolean enabled = true;
+ mOneHandedController.setSwipeToNotificationEnabled(enabled);
+
+ verify(mMockTouchHandler, atLeastOnce()).onOneHandedEnabled(enabled);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateTapAppToExit() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, 1);
+
+ verify(mOneHandedController).setTaskChangeToExit(true);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateEnabled() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, 1);
+
+ verify(mOneHandedController).setOneHandedEnabled(true);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateTimeout() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT,
+ OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+
+ verify(mMockTimeoutHandler).setTimeout(
+ OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateSwipeToNotification() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1);
+
+ verify(mOneHandedController).setSwipeToNotificationEnabled(true);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java
new file mode 100644
index 000000000000..6d1a3c472245
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.DisplayAreaOrganizer.FEATURE_ONE_HANDED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Configuration;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Display;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
+import android.window.IWindowContainerToken;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase {
+ static final int DISPLAY_WIDTH = 1000;
+ static final int DISPLAY_HEIGHT = 1000;
+
+ DisplayAreaInfo mDisplayAreaInfo;
+ Display mDisplay;
+ OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer;
+ OneHandedTutorialHandler mTutorialHandler;
+ OneHandedAnimationController.OneHandedTransitionAnimator mFakeAnimator;
+ WindowContainerToken mToken;
+ SurfaceControl mLeash;
+ TestableLooper mTestableLooper;
+ @Mock
+ IWindowContainerToken mMockRealToken;
+ @Mock
+ OneHandedAnimationController mMockAnimationController;
+ @Mock
+ OneHandedAnimationController.OneHandedTransitionAnimator mMockAnimator;
+ @Mock
+ OneHandedSurfaceTransactionHelper mMockSurfaceTransactionHelper;
+ @Mock
+ DisplayController mMockDisplayController;
+ @Mock
+ SurfaceControl mMockLeash;
+ @Mock
+ WindowContainerTransaction mMockWindowContainerTransaction;
+
+ Handler mSpyUpdateHandler;
+ Handler.Callback mUpdateCallback = (msg) -> false;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mTestableLooper = TestableLooper.get(this);
+ mToken = new WindowContainerToken(mMockRealToken);
+ mLeash = new SurfaceControl();
+ mDisplay = mContext.getDisplay();
+ mDisplayAreaInfo = new DisplayAreaInfo(mToken, DEFAULT_DISPLAY, FEATURE_ONE_HANDED);
+ mDisplayAreaInfo.configuration.orientation = Configuration.ORIENTATION_PORTRAIT;
+ when(mMockAnimationController.getAnimator(any(), any(), any())).thenReturn(null);
+ when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay);
+ when(mMockSurfaceTransactionHelper.translate(any(), any(), anyFloat())).thenReturn(
+ mMockSurfaceTransactionHelper);
+ when(mMockSurfaceTransactionHelper.crop(any(), any(), any())).thenReturn(
+ mMockSurfaceTransactionHelper);
+ when(mMockSurfaceTransactionHelper.round(any(), any())).thenReturn(
+ mMockSurfaceTransactionHelper);
+ when(mMockAnimator.isRunning()).thenReturn(true);
+ when(mMockAnimator.setDuration(anyInt())).thenReturn(mFakeAnimator);
+ when(mMockAnimator.setOneHandedAnimationCallbacks(any())).thenReturn(mFakeAnimator);
+ when(mMockAnimator.setTransitionDirection(anyInt())).thenReturn(mFakeAnimator);
+ when(mMockLeash.getWidth()).thenReturn(DISPLAY_WIDTH);
+ when(mMockLeash.getHeight()).thenReturn(DISPLAY_HEIGHT);
+
+ mDisplayAreaOrganizer = new OneHandedDisplayAreaOrganizer(mContext,
+ mMockDisplayController,
+ mMockAnimationController,
+ mTutorialHandler);
+ mSpyUpdateHandler = spy(new Handler(OneHandedThread.get().getLooper(), mUpdateCallback));
+ mDisplayAreaOrganizer.setUpdateHandler(mSpyUpdateHandler);
+ }
+
+ @Test
+ public void testOnDisplayAreaAppeared() {
+ mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash);
+ mTestableLooper.processAllMessages();
+
+ verify(mMockAnimationController, never()).getAnimator(any(), any(), any());
+ }
+
+ @Test
+ public void testOnDisplayAreaVanished() {
+ mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash);
+ mTestableLooper.processAllMessages();
+ mDisplayAreaOrganizer.onDisplayAreaVanished(mDisplayAreaInfo);
+
+ assertThat(mDisplayAreaOrganizer.mDisplayAreaMap).isEmpty();
+ }
+
+ @Test
+ public void testScheduleOffset() {
+ final int xOffSet = 0;
+ final int yOffSet = 100;
+ mDisplayAreaOrganizer.scheduleOffset(xOffSet, yOffSet);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_0_to_landscape_90() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 0 -> 90
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_90,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_0_to_seascape_270() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 0 -> 270
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_270,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_180_to_landscape_90() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 180 -> 90
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_90,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_180_to_seascape_270() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 180 -> 270
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_270,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_landscape_90_to_portrait_0() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 90 -> 0
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_0,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_landscape_90_to_portrait_180() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 90 -> 180
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_180,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_Seascape_270_to_portrait_0() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 270 -> 0
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_0,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_seascape_90_to_portrait_180() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 270 -> 180
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_180,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_0_to_portrait_0() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 0 -> 0
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_0,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_0_to_portrait_180() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 0 -> 180
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_180,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_180_to_portrait_180() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 180 -> 180
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_180,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_portrait_180_to_portrait_0() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 180 -> 0
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_0,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_landscape_90_to_landscape_90() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 90 -> 90
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_90,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_landscape_90_to_seascape_270() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 90 -> 270
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_270,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_seascape_270_to_seascape_270() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 270 -> 270
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_270,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+
+ @Test
+ public void testRotation_seascape_90_to_landscape_90() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 270 -> 90
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_90,
+ mMockWindowContainerTransaction);
+ mTestableLooper.processAllMessages();
+
+ verify(mSpyUpdateHandler, never()).sendMessage(any());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java
new file mode 100644
index 000000000000..492c34e10ed5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.testing.UiEventLoggerFake;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+@SmallTest
+public class OneHandedEventsTest extends OneHandedTestCase {
+
+ private UiEventLoggerFake mUiEventLogger;
+
+ @Parameterized.Parameter
+ public int mTag;
+
+ @Parameterized.Parameter(1)
+ public String mExpectedMessage;
+
+ public UiEventLogger.UiEventEnum mUiEvent;
+
+ @Before
+ public void setFakeLoggers() {
+ mUiEventLogger = new UiEventLoggerFake();
+ OneHandedEvents.sUiEventLogger = mUiEventLogger;
+ }
+
+ @Test
+ public void testLogEvent() {
+ if (mUiEvent != null) {
+ assertEquals(1, mUiEventLogger.numLogs());
+ assertEquals(mUiEvent.getId(), mUiEventLogger.eventId(0));
+ }
+ }
+
+ @Parameterized.Parameters(name = "{index}: {2}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][]{
+ // Triggers
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN,
+ "writeEvent one_handed_trigger_gesture_in"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT,
+ "writeEvent one_handed_trigger_gesture_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT,
+ "writeEvent one_handed_trigger_overspace_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT,
+ "writeEvent one_handed_trigger_pop_ime_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT,
+ "writeEvent one_handed_trigger_rotation_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT,
+ "writeEvent one_handed_trigger_app_taps_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT,
+ "writeEvent one_handed_trigger_timeout_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT,
+ "writeEvent one_handed_trigger_screen_off_out"},
+ // Settings toggles
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON,
+ "writeEvent one_handed_settings_enabled_on"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF,
+ "writeEvent one_handed_settings_enabled_off"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON,
+ "writeEvent one_handed_settings_app_taps_exit_on"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF,
+ "writeEvent one_handed_settings_app_taps_exit_off"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON,
+ "writeEvent one_handed_settings_timeout_exit_on"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF,
+ "writeEvent one_handed_settings_timeout_exit_off"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER,
+ "writeEvent one_handed_settings_timeout_seconds_never"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4,
+ "writeEvent one_handed_settings_timeout_seconds_4"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8,
+ "writeEvent one_handed_settings_timeout_seconds_8"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12,
+ "writeEvent one_handed_settings_timeout_seconds_12"}
+ });
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java
new file mode 100644
index 000000000000..fb417c8ca5e8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedGestureHandlerTest extends OneHandedTestCase {
+ OneHandedTutorialHandler mTutorialHandler;
+ OneHandedGestureHandler mGestureHandler;
+ @Mock
+ DisplayController mMockDisplayController;
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mTutorialHandler = new OneHandedTutorialHandler(mContext);
+ mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController);
+ }
+
+ @Test
+ public void testSetGestureEventListener() {
+ OneHandedGestureHandler.OneHandedGestureEventCallback callback =
+ new OneHandedGestureHandler.OneHandedGestureEventCallback() {
+ @Override
+ public void onStart() {}
+
+ @Override
+ public void onStop() {}
+ };
+
+ mGestureHandler.setGestureEventListener(callback);
+ assertThat(mGestureHandler.mGestureEventCallback).isEqualTo(callback);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void testReceiveNewConfig_whenThreeButtonModeEnabled() {
+ mGestureHandler.onOneHandedEnabled(true);
+ mGestureHandler.onThreeButtonModeEnabled(true);
+
+ assertThat(mGestureHandler.mInputMonitor).isNotNull();
+ assertThat(mGestureHandler.mInputEventReceiver).isNotNull();
+ }
+
+ @Test
+ public void testOneHandedDisabled_shouldDisposeInputChannel() {
+ mGestureHandler.onOneHandedEnabled(false);
+
+ assertThat(mGestureHandler.mInputMonitor).isNull();
+ assertThat(mGestureHandler.mInputEventReceiver).isNull();
+ }
+
+ @Test
+ public void testChangeNavBarToNon3Button_shouldDisposeInputChannel() {
+ mGestureHandler.onOneHandedEnabled(true);
+ mGestureHandler.onThreeButtonModeEnabled(false);
+
+ assertThat(mGestureHandler.mInputMonitor).isNull();
+ assertThat(mGestureHandler.mInputEventReceiver).isNull();
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java
new file mode 100644
index 000000000000..7c11138a47aa
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedSettingsUtilTest extends OneHandedTestCase {
+ ContentResolver mContentResolver;
+ ContentObserver mContentObserver;
+ boolean mOnChanged;
+
+ @Before
+ public void setUp() {
+ mContentResolver = mContext.getContentResolver();
+ mContentObserver = new ContentObserver(mContext.getMainThreadHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ mOnChanged = true;
+ }
+ };
+ }
+
+ @Test
+ public void testRegisterSecureKeyObserver() {
+ final Uri result = OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver);
+
+ assertThat(result).isNotNull();
+
+ OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver);
+ }
+
+ @Test
+ public void testUnregisterSecureKeyObserver() {
+ OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver);
+ OneHandedSettingsUtil.unregisterSettingsKeyObserver(mContentResolver, mContentObserver);
+
+ assertThat(mOnChanged).isFalse();
+
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, 0);
+
+ assertThat(mOnChanged).isFalse();
+ }
+
+ @Test
+ public void testGetSettingsIsOneHandedModeEnabled() {
+ assertThat(OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ mContentResolver)).isAnyOf(true, false);
+ }
+
+ @Test
+ public void testGetSettingsTapsAppToExit() {
+ assertThat(OneHandedSettingsUtil.getSettingsTapsAppToExit(
+ mContentResolver)).isAnyOf(true, false);
+ }
+
+ @Test
+ public void testGetSettingsOneHandedModeTimeout() {
+ assertThat(OneHandedSettingsUtil.getSettingsOneHandedModeTimeout(
+ mContentResolver)).isAnyOf(
+ ONE_HANDED_TIMEOUT_NEVER,
+ ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ }
+
+ @Test
+ public void testGetSettingsSwipeToNotificationEnabled() {
+ assertThat(OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ mContentResolver)).isAnyOf(true, false);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java
new file mode 100644
index 000000000000..c7ae2a09ad67
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java
@@ -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.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.SystemProperties;
+import android.provider.Settings;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * Base class that does One Handed specific setup.
+ */
+public abstract class OneHandedTestCase {
+ static boolean sOrigEnabled;
+ static boolean sOrigTapsAppToExitEnabled;
+ static int sOrigTimeout;
+ static boolean sOrigSwipeToNotification;
+
+ protected Context mContext;
+
+ @Before
+ public void setupSettings() {
+ final Context testContext =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final DisplayManager dm = testContext.getSystemService(DisplayManager.class);
+ mContext = testContext.createDisplayContext(dm.getDisplay(DEFAULT_DISPLAY));
+
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+
+ sOrigEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ getContext().getContentResolver());
+ sOrigTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout(
+ getContext().getContentResolver());
+ sOrigTapsAppToExitEnabled = OneHandedSettingsUtil.getSettingsTapsAppToExit(
+ getContext().getContentResolver());
+ sOrigSwipeToNotification = OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ getContext().getContentResolver());
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, 1);
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT, ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, 1);
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1);
+ }
+
+ @Before
+ public void assumeOneHandedModeSupported() {
+ assumeTrue(SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false));
+ }
+
+ @After
+ public void restoreSettings() {
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, sOrigEnabled ? 1 : 0);
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT, sOrigTimeout);
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, sOrigTapsAppToExitEnabled ? 1 : 0);
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED,
+ sOrigSwipeToNotification ? 1 : 0);
+
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+}
+
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java
new file mode 100644
index 000000000000..e2b70c3bcc70
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedTimeoutHandler.ONE_HANDED_TIMEOUT_STOP_MSG;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedTimeoutHandlerTest extends OneHandedTestCase {
+ OneHandedTimeoutHandler mTimeoutHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get());
+ }
+
+ @Test
+ public void testTimeoutHandler_isNotNull() {
+ assertThat(OneHandedTimeoutHandler.get()).isNotNull();
+ }
+
+ @Test
+ public void testTimeoutHandler_getTimeout_defaultMedium() {
+ assertThat(OneHandedTimeoutHandler.get().getTimeout()).isEqualTo(
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ }
+
+ @Test
+ public void testTimeoutHandler_setNewTime_resetTimer() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ verify(mTimeoutHandler).resetTimer();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutNever_neverResetTimer() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_NEVER);
+ assertThat(!mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutShort() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS);
+ verify(mTimeoutHandler).resetTimer();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutMedium() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ verify(mTimeoutHandler).resetTimer();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutLong() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ assertThat(mTimeoutHandler.getTimeout()).isEqualTo(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ }
+
+ @Test
+ public void testDragging_shouldRemoveAndSendEmptyMessageDelay() {
+ final boolean isDragging = true;
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ mTimeoutHandler.resetTimer();
+ TestableLooper.get(this).processAllMessages();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java
new file mode 100644
index 000000000000..c69e385b2602
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedTouchHandlerTest extends OneHandedTestCase {
+ OneHandedTouchHandler mTouchHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTouchHandler = new OneHandedTouchHandler();
+ }
+
+ @Test
+ public void testRegisterTouchEventListener() {
+ OneHandedTouchHandler.OneHandedTouchEventCallback callback = () -> {
+ };
+ mTouchHandler.registerTouchEventListener(callback);
+
+ assertThat(mTouchHandler.mTouchEventCallback).isEqualTo(callback);
+ }
+
+ @Test
+ public void testOneHandedDisabled_shouldDisposeInputChannel() {
+ mTouchHandler.onOneHandedEnabled(false);
+
+ assertThat(mTouchHandler.mInputMonitor).isNull();
+ assertThat(mTouchHandler.mInputEventReceiver).isNull();
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void testOneHandedEnabled_monitorInputChannel() {
+ mTouchHandler.onOneHandedEnabled(true);
+
+ assertThat(mTouchHandler.mInputMonitor).isNotNull();
+ assertThat(mTouchHandler.mInputEventReceiver).isNotNull();
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java
new file mode 100644
index 000000000000..3341c9cbacb9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.onehanded;
+
+import static org.mockito.Mockito.verify;
+
+import android.content.om.IOverlayManager;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedTutorialHandlerTest extends OneHandedTestCase {
+ @Mock
+ OneHandedTouchHandler mTouchHandler;
+ OneHandedTutorialHandler mTutorialHandler;
+ OneHandedGestureHandler mGestureHandler;
+ OneHandedController mOneHandedController;
+ @Mock
+ DisplayController mMockDisplayController;
+ @Mock
+ OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer;
+ @Mock
+ IOverlayManager mMockOverlayManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTutorialHandler = new OneHandedTutorialHandler(mContext);
+ mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController);
+ mOneHandedController = new OneHandedController(
+ getContext(),
+ mMockDisplayController,
+ mMockDisplayAreaOrganizer,
+ mTouchHandler,
+ mTutorialHandler,
+ mGestureHandler,
+ mMockOverlayManager);
+ }
+
+ @Test
+ public void testRegisterForDisplayAreaOrganizer() {
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mTutorialHandler);
+ }
+}
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
new file mode 100644
index 000000000000..255e74917ca0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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 com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.pip.PipAnimationController;
+import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
+import com.android.wm.shell.pip.PipTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests against {@link PipAnimationController} to ensure that it sends the right callbacks
+ * depending on the various interactions.
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class PipAnimationControllerTest extends PipTestCase {
+
+ private PipAnimationController mPipAnimationController;
+
+ private SurfaceControl mLeash;
+
+ @Mock
+ private PipAnimationController.PipAnimationCallback mPipAnimationCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ mPipAnimationController = new PipAnimationController(
+ new PipSurfaceTransactionHelper(mContext));
+ mLeash = new SurfaceControl.Builder()
+ .setContainerLayer()
+ .setName("FakeLeash")
+ .build();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void getAnimator_withAlpha_returnFloatAnimator() {
+ final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
+ .getAnimator(mLeash, new Rect(), 0f, 1f);
+
+ assertEquals("Expect ANIM_TYPE_ALPHA animation",
+ animator.getAnimationType(), PipAnimationController.ANIM_TYPE_ALPHA);
+ }
+
+ @Test
+ public void getAnimator_withBounds_returnBoundsAnimator() {
+ final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
+ .getAnimator(mLeash, new Rect(), new Rect(), null, TRANSITION_DIRECTION_TO_PIP);
+
+ assertEquals("Expect ANIM_TYPE_BOUNDS animation",
+ animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS);
+ }
+
+ @Test
+ public void getAnimator_whenSameTypeRunning_updateExistingAnimator() {
+ 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);
+ oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new);
+ oldAnimator.start();
+
+ final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController
+ .getAnimator(mLeash, startValue, endValue2, null, TRANSITION_DIRECTION_TO_PIP);
+
+ assertEquals("getAnimator with same type returns same animator",
+ oldAnimator, newAnimator);
+ assertEquals("getAnimator with same type updates end value",
+ endValue2, newAnimator.getEndValue());
+ }
+
+ @Test
+ public void getAnimator_setTransitionDirection() {
+ PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
+ .getAnimator(mLeash, new Rect(), 0f, 1f)
+ .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP);
+ assertEquals("Transition to PiP mode",
+ animator.getTransitionDirection(), TRANSITION_DIRECTION_TO_PIP);
+
+ animator = mPipAnimationController
+ .getAnimator(mLeash, new Rect(), 0f, 1f)
+ .setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP);
+ assertEquals("Transition to fullscreen mode",
+ animator.getTransitionDirection(), TRANSITION_DIRECTION_LEAVE_PIP);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void pipTransitionAnimator_updateEndValue() {
+ 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);
+
+ animator.updateEndValue(endValue2);
+
+ assertEquals("updateEndValue updates end value", animator.getEndValue(), endValue2);
+ }
+
+ @Test
+ public void pipTransitionAnimator_setPipAnimationCallback() {
+ 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);
+ animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new);
+
+ animator.setPipAnimationCallback(mPipAnimationCallback);
+
+ // onAnimationStart triggers onPipAnimationStart
+ animator.onAnimationStart(animator);
+ verify(mPipAnimationCallback).onPipAnimationStart(animator);
+
+ // onAnimationCancel triggers onPipAnimationCancel
+ animator.onAnimationCancel(animator);
+ verify(mPipAnimationCallback).onPipAnimationCancel(animator);
+
+ // onAnimationEnd triggers onPipAnimationEnd
+ animator.onAnimationEnd(animator);
+ verify(mPipAnimationCallback).onPipAnimationEnd(any(SurfaceControl.Transaction.class),
+ eq(animator));
+ }
+
+ /**
+ * A dummy {@link SurfaceControl.Transaction} class.
+ * This is created as {@link Mock} does not support method chaining.
+ */
+ public static class DummySurfaceControlTx extends SurfaceControl.Transaction {
+ @Override
+ public SurfaceControl.Transaction setAlpha(SurfaceControl leash, float alpha) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setPosition(SurfaceControl leash, float x, float y) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setWindowCrop(SurfaceControl leash, int w, int h) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setCornerRadius(SurfaceControl leash, float radius) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setMatrix(SurfaceControl leash, Matrix matrix,
+ float[] float9) {
+ return this;
+ }
+
+ @Override
+ public void apply() {}
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsHandlerTest.java
new file mode 100644
index 000000000000..39117bb5912b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsHandlerTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.util.Size;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.pip.PipBoundsHandler;
+import com.android.wm.shell.pip.PipTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests against {@link PipBoundsHandler}, including but not limited to:
+ * - default/movement bounds
+ * - save/restore PiP position on application lifecycle
+ * - save/restore PiP position on screen rotation
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class PipBoundsHandlerTest extends PipTestCase {
+ private static final int ROUNDING_ERROR_MARGIN = 16;
+ private static final float ASPECT_RATIO_ERROR_MARGIN = 0.01f;
+ private static final float DEFAULT_ASPECT_RATIO = 1f;
+ private static final float MIN_ASPECT_RATIO = 0.5f;
+ private static final float MAX_ASPECT_RATIO = 2f;
+ private static final Rect EMPTY_CURRENT_BOUNDS = null;
+ private static final Size EMPTY_MINIMAL_SIZE = null;
+
+ private PipBoundsHandler mPipBoundsHandler;
+ private DisplayInfo mDefaultDisplayInfo;
+ private PipBoundsState mPipBoundsState;
+
+ @Before
+ public void setUp() throws Exception {
+ initializeMockResources();
+ mPipBoundsState = new PipBoundsState();
+ mPipBoundsHandler = new PipBoundsHandler(mContext, mPipBoundsState);
+
+ mPipBoundsHandler.onDisplayInfoChanged(mDefaultDisplayInfo);
+ }
+
+ private void initializeMockResources() {
+ final TestableResources res = mContext.getOrCreateTestableResources();
+ res.addOverride(
+ com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio,
+ DEFAULT_ASPECT_RATIO);
+ res.addOverride(
+ com.android.internal.R.integer.config_defaultPictureInPictureGravity,
+ Gravity.END | Gravity.BOTTOM);
+ res.addOverride(
+ com.android.internal.R.dimen.default_minimal_size_pip_resizable_task, 100);
+ res.addOverride(
+ com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets,
+ "16x16");
+ res.addOverride(
+ com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio,
+ MIN_ASPECT_RATIO);
+ res.addOverride(
+ com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio,
+ MAX_ASPECT_RATIO);
+
+ mDefaultDisplayInfo = new DisplayInfo();
+ mDefaultDisplayInfo.displayId = 1;
+ mDefaultDisplayInfo.logicalWidth = 1000;
+ mDefaultDisplayInfo.logicalHeight = 1500;
+ }
+
+ @Test
+ public void getDefaultAspectRatio() {
+ assertEquals("Default aspect ratio matches resources",
+ DEFAULT_ASPECT_RATIO, mPipBoundsHandler.getDefaultAspectRatio(),
+ ASPECT_RATIO_ERROR_MARGIN);
+ }
+
+ @Test
+ public void onConfigurationChanged_reloadResources() {
+ final float newDefaultAspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
+ final TestableResources res = mContext.getOrCreateTestableResources();
+ res.addOverride(com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio,
+ newDefaultAspectRatio);
+
+ mPipBoundsHandler.onConfigurationChanged(mContext);
+
+ assertEquals("Default aspect ratio should be reloaded",
+ mPipBoundsHandler.getDefaultAspectRatio(), newDefaultAspectRatio,
+ ASPECT_RATIO_ERROR_MARGIN);
+ }
+
+ @Test
+ public void getDestinationBounds_returnBoundsMatchesAspectRatio() {
+ final float[] aspectRatios = new float[] {
+ (MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2,
+ DEFAULT_ASPECT_RATIO,
+ (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
+ };
+ for (float aspectRatio : aspectRatios) {
+ mPipBoundsState.setAspectRatio(aspectRatio);
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+ final float actualAspectRatio =
+ destinationBounds.width() / (destinationBounds.height() * 1f);
+ assertEquals("Destination bounds matches the given aspect ratio",
+ aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
+ }
+ }
+
+ @Test
+ public void getDestinationBounds_invalidAspectRatio_returnsDefaultAspectRatio() {
+ final float[] invalidAspectRatios = new float[] {
+ MIN_ASPECT_RATIO / 2,
+ MAX_ASPECT_RATIO * 2
+ };
+ for (float aspectRatio : invalidAspectRatios) {
+ mPipBoundsState.setAspectRatio(aspectRatio);
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+ final float actualAspectRatio =
+ destinationBounds.width() / (destinationBounds.height() * 1f);
+ assertEquals("Destination bounds fallbacks to default aspect ratio",
+ mPipBoundsHandler.getDefaultAspectRatio(), actualAspectRatio,
+ ASPECT_RATIO_ERROR_MARGIN);
+ }
+ }
+
+ @Test
+ public void getDestinationBounds_withCurrentBounds_returnBoundsMatchesAspectRatio() {
+ final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
+ final Rect currentBounds = new Rect(0, 0, 0, 100);
+ currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left;
+
+ mPipBoundsState.setAspectRatio(aspectRatio);
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(currentBounds,
+ EMPTY_MINIMAL_SIZE);
+
+ final float actualAspectRatio =
+ destinationBounds.width() / (destinationBounds.height() * 1f);
+ assertEquals("Destination bounds matches the given aspect ratio",
+ aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
+ }
+
+ @Test
+ public void getDestinationBounds_withMinSize_returnMinBounds() {
+ final float[] aspectRatios = new float[] {
+ (MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2,
+ DEFAULT_ASPECT_RATIO,
+ (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
+ };
+ final Size[] minimalSizes = new Size[] {
+ new Size((int) (100 * aspectRatios[0]), 100),
+ new Size((int) (100 * aspectRatios[1]), 100),
+ new Size((int) (100 * aspectRatios[2]), 100)
+ };
+ for (int i = 0; i < aspectRatios.length; i++) {
+ final float aspectRatio = aspectRatios[i];
+ final Size minimalSize = minimalSizes[i];
+ mPipBoundsState.setAspectRatio(aspectRatio);
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, minimalSize);
+ assertTrue("Destination bounds is no smaller than minimal requirement",
+ (destinationBounds.width() == minimalSize.getWidth()
+ && destinationBounds.height() >= minimalSize.getHeight())
+ || (destinationBounds.height() == minimalSize.getHeight()
+ && destinationBounds.width() >= minimalSize.getWidth()));
+ final float actualAspectRatio =
+ destinationBounds.width() / (destinationBounds.height() * 1f);
+ assertEquals("Destination bounds matches the given aspect ratio",
+ aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
+ }
+ }
+
+ @Test
+ public void getDestinationBounds_withCurrentBounds_ignoreMinBounds() {
+ final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
+ final Rect currentBounds = new Rect(0, 0, 0, 100);
+ currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left;
+ final Size minSize = new Size(currentBounds.width() / 2, currentBounds.height() / 2);
+
+ mPipBoundsState.setAspectRatio(aspectRatio);
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ currentBounds, minSize);
+
+ assertTrue("Destination bounds ignores minimal size",
+ destinationBounds.width() > minSize.getWidth()
+ && destinationBounds.height() > minSize.getHeight());
+ }
+
+ @Test
+ public void getDestinationBounds_reentryStateExists_restoreLastSize() {
+ mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
+ final Rect reentryBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+ reentryBounds.scale(1.25f);
+ final float reentrySnapFraction = mPipBoundsHandler.getSnapFraction(reentryBounds);
+
+ mPipBoundsState.saveReentryState(reentryBounds, reentrySnapFraction);
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ assertEquals(reentryBounds.width(), destinationBounds.width());
+ assertEquals(reentryBounds.height(), destinationBounds.height());
+ }
+
+ @Test
+ public void getDestinationBounds_reentryStateExists_restoreLastPosition() {
+ mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
+ final Rect reentryBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+ reentryBounds.offset(0, -100);
+ final float reentrySnapFraction = mPipBoundsHandler.getSnapFraction(reentryBounds);
+
+ mPipBoundsState.saveReentryState(reentryBounds, reentrySnapFraction);
+
+ final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ assertBoundsInclusionWithMargin("restoreLastPosition", reentryBounds, destinationBounds);
+ }
+
+ @Test
+ public void setShelfHeight_offsetBounds() {
+ final int shelfHeight = 100;
+ mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
+ final Rect oldPosition = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ mPipBoundsHandler.setShelfHeight(true, shelfHeight);
+ final Rect newPosition = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ oldPosition.offset(0, -shelfHeight);
+ assertBoundsInclusionWithMargin("offsetBounds by shelf", oldPosition, newPosition);
+ }
+
+ @Test
+ public void onImeVisibilityChanged_offsetBounds() {
+ final int imeHeight = 100;
+ mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
+ final Rect oldPosition = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ mPipBoundsHandler.onImeVisibilityChanged(true, imeHeight);
+ final Rect newPosition = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ oldPosition.offset(0, -imeHeight);
+ assertBoundsInclusionWithMargin("offsetBounds by IME", oldPosition, newPosition);
+ }
+
+ @Test
+ public void getDestinationBounds_noReentryState_useDefaultBounds() {
+ mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
+ final Rect defaultBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ mPipBoundsState.clearReentryState();
+
+ final Rect actualBounds = mPipBoundsHandler.getDestinationBounds(
+ EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE);
+
+ assertBoundsInclusionWithMargin("useDefaultBounds", defaultBounds, actualBounds);
+ }
+
+ private void assertBoundsInclusionWithMargin(String from, Rect expected, Rect actual) {
+ final Rect expectedWithMargin = new Rect(expected);
+ expectedWithMargin.inset(-ROUNDING_ERROR_MARGIN, -ROUNDING_ERROR_MARGIN);
+ assertTrue(from + ": expect " + expected
+ + " contains " + actual
+ + " with error margin " + ROUNDING_ERROR_MARGIN,
+ expectedWithMargin.contains(actual));
+ }
+
+ private void assertNonBoundsInclusionWithMargin(String from, Rect expected, Rect actual) {
+ final Rect expectedWithMargin = new Rect(expected);
+ expectedWithMargin.inset(-ROUNDING_ERROR_MARGIN, -ROUNDING_ERROR_MARGIN);
+ assertFalse(from + ": expect " + expected
+ + " not contains " + actual
+ + " with error margin " + ROUNDING_ERROR_MARGIN,
+ expectedWithMargin.contains(actual));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
new file mode 100644
index 000000000000..dc9399edaa3b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.content.ComponentName;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Size;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link PipBoundsState}.
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class PipBoundsStateTest extends PipTestCase {
+
+ private static final Rect DEFAULT_BOUNDS = new Rect(0, 0, 10, 10);
+ private static final float DEFAULT_SNAP_FRACTION = 1.0f;
+
+ private PipBoundsState mPipBoundsState;
+ private ComponentName mTestComponentName1;
+ private ComponentName mTestComponentName2;
+
+ @Before
+ public void setUp() {
+ mPipBoundsState = new PipBoundsState();
+ mTestComponentName1 = new ComponentName(mContext, "component1");
+ mTestComponentName2 = new ComponentName(mContext, "component2");
+ }
+
+ @Test
+ public void testSetBounds() {
+ final Rect bounds = new Rect(0, 0, 100, 100);
+ mPipBoundsState.setBounds(bounds);
+
+ assertEquals(bounds, mPipBoundsState.getBounds());
+ }
+
+ @Test
+ public void testSetReentryState() {
+ final Rect bounds = new Rect(0, 0, 100, 100);
+ final float snapFraction = 0.5f;
+
+ mPipBoundsState.saveReentryState(bounds, snapFraction);
+
+ final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState();
+ assertEquals(new Size(100, 100), state.getSize());
+ assertEquals(snapFraction, state.getSnapFraction(), 0.01);
+ }
+
+ @Test
+ public void testClearReentryState() {
+ final Rect bounds = new Rect(0, 0, 100, 100);
+ final float snapFraction = 0.5f;
+
+ mPipBoundsState.saveReentryState(bounds, snapFraction);
+ mPipBoundsState.clearReentryState();
+
+ assertNull(mPipBoundsState.getReentryState());
+ }
+
+ @Test
+ public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() {
+ mPipBoundsState.setLastPipComponentName(mTestComponentName1);
+ mPipBoundsState.saveReentryState(DEFAULT_BOUNDS, DEFAULT_SNAP_FRACTION);
+
+ mPipBoundsState.setLastPipComponentName(mTestComponentName1);
+
+ final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState();
+ assertNotNull(state);
+ assertEquals(new Size(DEFAULT_BOUNDS.width(), DEFAULT_BOUNDS.height()), state.getSize());
+ assertEquals(DEFAULT_SNAP_FRACTION, state.getSnapFraction(), 0.01);
+ }
+
+ @Test
+ public void testSetLastPipComponentName_changed_clearReentryState() {
+ mPipBoundsState.setLastPipComponentName(mTestComponentName1);
+ mPipBoundsState.saveReentryState(DEFAULT_BOUNDS, DEFAULT_SNAP_FRACTION);
+
+ mPipBoundsState.setLastPipComponentName(mTestComponentName2);
+
+ assertNull(mPipBoundsState.getReentryState());
+ }
+}
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
new file mode 100644
index 000000000000..54543d25b401
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.splitscreen.SplitScreen;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * Unit tests for {@link PipTaskOrganizer}
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class PipTaskOrganizerTest extends PipTestCase {
+ private PipTaskOrganizer mSpiedPipTaskOrganizer;
+
+ @Mock private DisplayController mMockdDisplayController;
+ @Mock private PipBoundsHandler mMockPipBoundsHandler;
+ @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper;
+ @Mock private PipUiEventLogger mMockPipUiEventLogger;
+ @Mock private Optional<SplitScreen> mMockOptionalSplitScreen;
+ @Mock private ShellTaskOrganizer mMockShellTaskOrganizer;
+ @Mock private PipBoundsState mMockPipBoundsState;
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+ mSpiedPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockPipBoundsState,
+ mMockPipBoundsHandler, mMockPipSurfaceTransactionHelper, mMockOptionalSplitScreen,
+ mMockdDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer);
+ }
+
+ @Test
+ public void instantiatePipTaskOrganizer_addsTaskListener() {
+ verify(mMockShellTaskOrganizer).addListenerForType(any(), anyInt());
+ }
+
+ @Test
+ public void instantiatePipTaskOrganizer_addsDisplayWindowListener() {
+ verify(mMockdDisplayController).addDisplayWindowListener(any());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTestCase.java
new file mode 100644
index 000000000000..fdebe4e4e6f5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTestCase.java
@@ -0,0 +1,53 @@
+/*
+ * 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.Display.DEFAULT_DISPLAY;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.testing.TestableContext;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Before;
+
+/**
+ * Base class that does One Handed specific setup.
+ */
+public abstract class PipTestCase {
+
+ protected TestableContext mContext;
+
+ @Before
+ public void setup() {
+ final Context context =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final DisplayManager dm = context.getSystemService(DisplayManager.class);
+ mContext = new TestableContext(
+ context.createDisplayContext(dm.getDisplay(DEFAULT_DISPLAY)));
+
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+}
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
new file mode 100644
index 000000000000..a282a48e8494
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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 static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.wm.shell.WindowManagerShellWrapper;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.pip.PipBoundsHandler;
+import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+import com.android.wm.shell.pip.PipTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link PipController}
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class PipControllerTest extends PipTestCase {
+ private PipController mPipController;
+
+ @Mock private DisplayController mMockDisplayController;
+ @Mock private PipMenuActivityController mMockPipMenuActivityController;
+ @Mock private PipAppOpsListener mMockPipAppOpsListener;
+ @Mock private PipBoundsHandler mMockPipBoundsHandler;
+ @Mock private PipMediaController mMockPipMediaController;
+ @Mock private PipTaskOrganizer mMockPipTaskOrganizer;
+ @Mock private PipTouchHandler mMockPipTouchHandler;
+ @Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper;
+ @Mock private PipBoundsState mMockPipBoundsState;
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+ mPipController = new PipController(mContext, mMockDisplayController,
+ mMockPipAppOpsListener, mMockPipBoundsHandler, mMockPipBoundsState,
+ mMockPipMediaController, mMockPipMenuActivityController, mMockPipTaskOrganizer,
+ mMockPipTouchHandler, mMockWindowManagerShellWrapper);
+ }
+
+ @Test
+ public void instantiatePipController_registersPipTransitionCallback() {
+ verify(mMockPipTaskOrganizer).registerPipTransitionCallback(any());
+ }
+
+ @Test
+ public void instantiatePipController_addsDisplayChangingController() {
+ verify(mMockDisplayController).addDisplayChangingController(any());
+ }
+
+ @Test
+ public void instantiatePipController_addsDisplayWindowListener() {
+ verify(mMockDisplayController).addDisplayWindowListener(any());
+ }
+
+ @Test
+ public void createPip_notSupported_returnsNull() {
+ Context spyContext = spy(mContext);
+ PackageManager mockPackageManager = mock(PackageManager.class);
+ when(mockPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false);
+ when(spyContext.getPackageManager()).thenReturn(mockPackageManager);
+
+ assertNull(PipController.create(spyContext, mMockDisplayController,
+ mMockPipAppOpsListener, mMockPipBoundsHandler, mMockPipBoundsState,
+ mMockPipMediaController, mMockPipMenuActivityController, mMockPipTaskOrganizer,
+ mMockPipTouchHandler, mMockWindowManagerShellWrapper));
+ }
+}
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
new file mode 100644
index 000000000000..3f60cc01f20b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Size;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.pip.PipBoundsHandler;
+import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipSnapAlgorithm;
+import com.android.wm.shell.pip.PipTaskOrganizer;
+import com.android.wm.shell.pip.PipTestCase;
+import com.android.wm.shell.pip.PipUiEventLogger;
+import com.android.wm.shell.pip.phone.PipMenuActivityController;
+import com.android.wm.shell.pip.phone.PipMotionHelper;
+import com.android.wm.shell.pip.phone.PipResizeGestureHandler;
+import com.android.wm.shell.pip.phone.PipTouchHandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests against {@link PipTouchHandler}, including but not limited to:
+ * - Update movement bounds based on new bounds
+ * - Update movement bounds based on IME/shelf
+ * - Update movement bounds to PipResizeHandler
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class PipTouchHandlerTest extends PipTestCase {
+
+ private PipTouchHandler mPipTouchHandler;
+
+ @Mock
+ private PipMenuActivityController mPipMenuActivityController;
+
+ @Mock
+ private PipTaskOrganizer mPipTaskOrganizer;
+
+ @Mock
+ private FloatingContentCoordinator mFloatingContentCoordinator;
+
+ @Mock
+ private PipUiEventLogger mPipUiEventLogger;
+
+ private PipBoundsState mPipBoundsState;
+ private PipBoundsHandler mPipBoundsHandler;
+ private PipSnapAlgorithm mPipSnapAlgorithm;
+ private PipMotionHelper mMotionHelper;
+ private PipResizeGestureHandler mPipResizeGestureHandler;
+
+ private Rect mInsetBounds;
+ private Rect mMinBounds;
+ private Rect mCurBounds;
+ private boolean mFromImeAdjustment;
+ private boolean mFromShelfAdjustment;
+ private int mDisplayRotation;
+ private int mImeHeight;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mPipBoundsState = new PipBoundsState();
+ mPipBoundsHandler = new PipBoundsHandler(mContext, mPipBoundsState);
+ mPipSnapAlgorithm = mPipBoundsHandler.getSnapAlgorithm();
+ mPipSnapAlgorithm = new PipSnapAlgorithm(mContext);
+ mPipTouchHandler = new PipTouchHandler(mContext, mPipMenuActivityController,
+ mPipBoundsHandler, mPipBoundsState, mPipTaskOrganizer, mFloatingContentCoordinator,
+ mPipUiEventLogger);
+ mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper());
+ mPipResizeGestureHandler = Mockito.spy(mPipTouchHandler.getPipResizeGestureHandler());
+ mPipTouchHandler.setPipMotionHelper(mMotionHelper);
+ mPipTouchHandler.setPipResizeGestureHandler(mPipResizeGestureHandler);
+
+ // Assume a display of 1000 x 1000
+ // inset of 10
+ mInsetBounds = new Rect(10, 10, 990, 990);
+ // minBounds of 100x100 bottom right corner
+ mMinBounds = new Rect(890, 890, 990, 990);
+ mCurBounds = new Rect(mMinBounds);
+ mFromImeAdjustment = false;
+ mFromShelfAdjustment = false;
+ mDisplayRotation = 0;
+ mImeHeight = 100;
+ }
+
+ @Test
+ public void updateMovementBounds_minBounds() {
+ Rect expectedMinMovementBounds = new Rect();
+ mPipSnapAlgorithm.getMovementBounds(mMinBounds, mInsetBounds, expectedMinMovementBounds, 0);
+
+ mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds,
+ mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation);
+
+ assertEquals(expectedMinMovementBounds, mPipTouchHandler.mNormalMovementBounds);
+ verify(mPipResizeGestureHandler, times(1))
+ .updateMinSize(mMinBounds.width(), mMinBounds.height());
+ }
+
+ @Test
+ public void updateMovementBounds_maxBounds() {
+ Point displaySize = new Point();
+ mContext.getDisplay().getRealSize(displaySize);
+ Size maxSize = mPipSnapAlgorithm.getSizeForAspectRatio(1,
+ mContext.getResources().getDimensionPixelSize(
+ R.dimen.pip_expanded_shortest_edge_size), displaySize.x, displaySize.y);
+ Rect maxBounds = new Rect(0, 0, maxSize.getWidth(), maxSize.getHeight());
+ Rect expectedMaxMovementBounds = new Rect();
+ mPipSnapAlgorithm.getMovementBounds(maxBounds, mInsetBounds, expectedMaxMovementBounds, 0);
+
+ mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds,
+ mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation);
+
+ assertEquals(expectedMaxMovementBounds, mPipTouchHandler.mExpandedMovementBounds);
+ verify(mPipResizeGestureHandler, times(1))
+ .updateMaxSize(maxBounds.width(), maxBounds.height());
+ }
+
+ @Test
+ public void updateMovementBounds_withImeAdjustment_movesPip() {
+ mFromImeAdjustment = true;
+ mPipTouchHandler.onImeVisibilityChanged(true /* imeVisible */, mImeHeight);
+
+ mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds,
+ mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation);
+
+ verify(mMotionHelper, times(1)).animateToOffset(any(), anyInt());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java
new file mode 100644
index 000000000000..40667f76b17e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 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 static android.view.MotionEvent.ACTION_BUTTON_PRESS;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.pip.PipTestCase;
+import com.android.wm.shell.pip.phone.PipTouchState;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@RunWithLooper
+public class PipTouchStateTest extends PipTestCase {
+
+ private PipTouchState mTouchState;
+ private CountDownLatch mDoubleTapCallbackTriggeredLatch;
+ private CountDownLatch mHoverExitCallbackTriggeredLatch;
+
+ @Before
+ public void setUp() throws Exception {
+ mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1);
+ mHoverExitCallbackTriggeredLatch = new CountDownLatch(1);
+ mTouchState = new PipTouchState(ViewConfiguration.get(getContext()),
+ Handler.createAsync(Looper.myLooper()), () -> {
+ mDoubleTapCallbackTriggeredLatch.countDown();
+ }, () -> {
+ mHoverExitCallbackTriggeredLatch.countDown();
+ });
+ assertFalse(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ }
+
+ @Test
+ public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0));
+ assertFalse(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+ }
+
+ @Test
+ public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
+ assertFalse(mTouchState.isDoubleTap());
+ assertTrue(mTouchState.isWaitingForDoubleTap());
+
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10);
+ mTouchState.scheduleDoubleTapTimeoutCallback();
+
+ // TODO: Remove this sleep. Its only being added because it speeds up this test a bit.
+ Thread.sleep(15);
+ TestableLooper.get(this).processAllMessages();
+ assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0);
+ }
+
+ @Test
+ public void testDoubleTapDrag_doubleTapCanceled() {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500));
+ assertTrue(mTouchState.isDragging());
+ assertFalse(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+ }
+
+ @Test
+ public void testDoubleTap_doubleTapRegistered() {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
+ assertTrue(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+ }
+
+ @Test
+ public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+
+ // TODO: Remove this sleep. Its only being added because it speeds up this test a bit.
+ Thread.sleep(50);
+ TestableLooper.get(this).processAllMessages();
+ assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0);
+ }
+
+ @Test
+ public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+ TestableLooper.get(this).processAllMessages();
+ assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1);
+ }
+
+ @Test
+ public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(),
+ 0, 0));
+
+ // TODO: Remove this sleep. Its only being added because it speeds up this test a bit.
+ Thread.sleep(50);
+ TestableLooper.get(this).processAllMessages();
+ assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1);
+ }
+
+ private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) {
+ return MotionEvent.obtain(0, eventTime, action, x, y, 0);
+ }
+}
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index aa34edf487fe..903ca2aa0783 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -155,10 +155,12 @@ cc_test {
android: {
srcs: [
"tests/BackupData_test.cpp",
+ "tests/BackupHelpers_test.cpp",
+ "tests/CursorWindow_test.cpp",
"tests/ObbFile_test.cpp",
"tests/PosixUtils_test.cpp",
],
- shared_libs: common_test_libs + ["libui"],
+ shared_libs: common_test_libs + ["libbinder", "liblog", "libui"],
},
host: {
static_libs: common_test_libs + ["liblog", "libz"],
@@ -184,9 +186,28 @@ cc_benchmark {
// Actual benchmarks.
"tests/AssetManager2_bench.cpp",
"tests/AttributeResolution_bench.cpp",
+ "tests/CursorWindow_bench.cpp",
"tests/SparseEntry_bench.cpp",
"tests/Theme_bench.cpp",
],
shared_libs: common_test_libs,
data: ["tests/data/**/*.apk"],
}
+
+cc_library {
+ name: "libandroidfw_fuzzer_lib",
+ defaults: ["libandroidfw_defaults"],
+ host_supported: true,
+ srcs: [
+ "CursorWindow.cpp",
+ ],
+ export_include_dirs: ["include"],
+ target: {
+ android: {
+ shared_libs: common_test_libs + ["libbinder", "liblog"],
+ },
+ host: {
+ static_libs: common_test_libs + ["libbinder", "liblog"],
+ },
+ },
+}
diff --git a/libs/androidfw/BackupHelpers.cpp b/libs/androidfw/BackupHelpers.cpp
index 8bfe2b6a259a..e80e9486c8b2 100644
--- a/libs/androidfw/BackupHelpers.cpp
+++ b/libs/androidfw/BackupHelpers.cpp
@@ -479,7 +479,7 @@ void send_tarfile_chunk(BackupDataWriter* writer, const char* buffer, size_t siz
}
int write_tarfile(const String8& packageName, const String8& domain,
- const String8& rootpath, const String8& filepath, off_t* outSize,
+ const String8& rootpath, const String8& filepath, off64_t* outSize,
BackupDataWriter* writer)
{
// In the output stream everything is stored relative to the root
diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp
index 6f05cbd0ebb3..1b8db46c54b6 100644
--- a/libs/androidfw/CursorWindow.cpp
+++ b/libs/androidfw/CursorWindow.cpp
@@ -14,166 +14,275 @@
* limitations under the License.
*/
-#undef LOG_TAG
#define LOG_TAG "CursorWindow"
#include <androidfw/CursorWindow.h>
-#include <binder/Parcel.h>
-#include <utils/Log.h>
-#include <cutils/ashmem.h>
#include <sys/mman.h>
-#include <assert.h>
-#include <string.h>
-#include <stdlib.h>
+#include "android-base/logging.h"
+#include "cutils/ashmem.h"
namespace android {
-CursorWindow::CursorWindow(const String8& name, int ashmemFd,
- void* data, size_t size, bool readOnly) :
- mName(name), mAshmemFd(ashmemFd), mData(data), mSize(size), mReadOnly(readOnly) {
- mHeader = static_cast<Header*>(mData);
+/**
+ * By default windows are lightweight inline allocations of this size;
+ * they're only inflated to ashmem regions when more space is needed.
+ */
+static constexpr const size_t kInlineSize = 16384;
+
+static constexpr const size_t kSlotShift = 4;
+static constexpr const size_t kSlotSizeBytes = 1 << kSlotShift;
+
+CursorWindow::CursorWindow() {
}
CursorWindow::~CursorWindow() {
- ::munmap(mData, mSize);
- ::close(mAshmemFd);
+ if (mAshmemFd != -1) {
+ ::munmap(mData, mSize);
+ ::close(mAshmemFd);
+ } else {
+ free(mData);
+ }
}
-status_t CursorWindow::create(const String8& name, size_t size, CursorWindow** outCursorWindow) {
+status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWindow **outWindow) {
+ *outWindow = nullptr;
+
+ CursorWindow* window = new CursorWindow();
+ if (!window) goto fail;
+
+ window->mName = name;
+ window->mSize = std::min(kInlineSize, inflatedSize);
+ window->mInflatedSize = inflatedSize;
+ window->mData = malloc(window->mSize);
+ if (!window->mData) goto fail;
+ window->mReadOnly = false;
+
+ window->clear();
+ window->updateSlotsData();
+
+ LOG(DEBUG) << "Created: " << window->toString();
+ *outWindow = window;
+ return OK;
+
+fail:
+ LOG(ERROR) << "Failed create";
+fail_silent:
+ delete window;
+ return UNKNOWN_ERROR;
+}
+
+status_t CursorWindow::maybeInflate() {
+ int ashmemFd = 0;
+ void* newData = nullptr;
+
+ // Bail early when we can't expand any further
+ if (mReadOnly || mSize == mInflatedSize) {
+ return INVALID_OPERATION;
+ }
+
String8 ashmemName("CursorWindow: ");
- ashmemName.append(name);
+ ashmemName.append(mName);
- status_t result;
- int ashmemFd = ashmem_create_region(ashmemName.string(), size);
+ ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize);
if (ashmemFd < 0) {
- result = -errno;
- ALOGE("CursorWindow: ashmem_create_region() failed: errno=%d.", errno);
- } else {
- result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE);
- if (result < 0) {
- ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d",errno);
- } else {
- void* data = ::mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0);
- if (data == MAP_FAILED) {
- result = -errno;
- ALOGE("CursorWindow: mmap() failed: errno=%d.", errno);
- } else {
- result = ashmem_set_prot_region(ashmemFd, PROT_READ);
- if (result < 0) {
- ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d.", errno);
- } else {
- CursorWindow* window = new CursorWindow(name, ashmemFd,
- data, size, false /*readOnly*/);
- result = window->clear();
- if (!result) {
- LOG_WINDOW("Created new CursorWindow: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- window->mHeader->freeOffset,
- window->mHeader->numRows,
- window->mHeader->numColumns,
- window->mSize, window->mData);
- *outCursorWindow = window;
- return OK;
- }
- delete window;
- }
- }
- ::munmap(data, size);
- }
- ::close(ashmemFd);
+ PLOG(ERROR) << "Failed ashmem_create_region";
+ goto fail_silent;
+ }
+
+ if (ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE) < 0) {
+ PLOG(ERROR) << "Failed ashmem_set_prot_region";
+ goto fail_silent;
}
- *outCursorWindow = NULL;
- return result;
+
+ newData = ::mmap(nullptr, mInflatedSize, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0);
+ if (newData == MAP_FAILED) {
+ PLOG(ERROR) << "Failed mmap";
+ goto fail_silent;
+ }
+
+ if (ashmem_set_prot_region(ashmemFd, PROT_READ) < 0) {
+ PLOG(ERROR) << "Failed ashmem_set_prot_region";
+ goto fail_silent;
+ }
+
+ {
+ // Migrate existing contents into new ashmem region
+ uint32_t slotsSize = mSize - mSlotsOffset;
+ uint32_t newSlotsOffset = mInflatedSize - slotsSize;
+ memcpy(static_cast<uint8_t*>(newData),
+ static_cast<uint8_t*>(mData), mAllocOffset);
+ memcpy(static_cast<uint8_t*>(newData) + newSlotsOffset,
+ static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);
+
+ free(mData);
+ mAshmemFd = ashmemFd;
+ mData = newData;
+ mSize = mInflatedSize;
+ mSlotsOffset = newSlotsOffset;
+
+ updateSlotsData();
+ }
+
+ LOG(DEBUG) << "Inflated: " << this->toString();
+ return OK;
+
+fail:
+ LOG(ERROR) << "Failed maybeInflate";
+fail_silent:
+ ::munmap(newData, mInflatedSize);
+ ::close(ashmemFd);
+ return UNKNOWN_ERROR;
}
-status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outCursorWindow) {
- String8 name = parcel->readString8();
+status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outWindow) {
+ *outWindow = nullptr;
+
+ CursorWindow* window = new CursorWindow();
+ if (!window) goto fail;
+
+ if (parcel->readString8(&window->mName)) goto fail;
+ if (parcel->readUint32(&window->mNumRows)) goto fail;
+ if (parcel->readUint32(&window->mNumColumns)) goto fail;
+ if (parcel->readUint32(&window->mSize)) goto fail;
- status_t result;
- int actualSize;
- int ashmemFd = parcel->readFileDescriptor();
- if (ashmemFd == int(BAD_TYPE)) {
- result = BAD_TYPE;
- ALOGE("CursorWindow: readFileDescriptor() failed");
+ if ((window->mNumRows * window->mNumColumns * kSlotSizeBytes) > window->mSize) {
+ LOG(ERROR) << "Unexpected size " << window->mSize << " for " << window->mNumRows
+ << " rows and " << window->mNumColumns << " columns";
+ goto fail_silent;
+ }
+
+ bool isAshmem;
+ if (parcel->readBool(&isAshmem)) goto fail;
+ if (isAshmem) {
+ window->mAshmemFd = parcel->readFileDescriptor();
+ if (window->mAshmemFd < 0) {
+ LOG(ERROR) << "Failed readFileDescriptor";
+ goto fail_silent;
+ }
+
+ window->mAshmemFd = ::fcntl(window->mAshmemFd, F_DUPFD_CLOEXEC, 0);
+ if (window->mAshmemFd < 0) {
+ PLOG(ERROR) << "Failed F_DUPFD_CLOEXEC";
+ goto fail_silent;
+ }
+
+ window->mData = ::mmap(nullptr, window->mSize, PROT_READ, MAP_SHARED, window->mAshmemFd, 0);
+ if (window->mData == MAP_FAILED) {
+ PLOG(ERROR) << "Failed mmap";
+ goto fail_silent;
+ }
} else {
- ssize_t size = ashmem_get_size_region(ashmemFd);
- if (size < 0) {
- result = UNKNOWN_ERROR;
- ALOGE("CursorWindow: ashmem_get_size_region() failed: errno=%d.", errno);
- } else {
- int dupAshmemFd = ::fcntl(ashmemFd, F_DUPFD_CLOEXEC, 0);
- if (dupAshmemFd < 0) {
- result = -errno;
- ALOGE("CursorWindow: fcntl() failed: errno=%d.", errno);
- } else {
- // the size of the ashmem descriptor can be modified between ashmem_get_size_region
- // call and mmap, so we'll check again immediately after memory is mapped
- void* data = ::mmap(NULL, size, PROT_READ, MAP_SHARED, dupAshmemFd, 0);
- if (data == MAP_FAILED) {
- result = -errno;
- ALOGE("CursorWindow: mmap() failed: errno=%d.", errno);
- } else if ((actualSize = ashmem_get_size_region(dupAshmemFd)) != size) {
- ::munmap(data, size);
- result = BAD_VALUE;
- ALOGE("CursorWindow: ashmem_get_size_region() returned %d, expected %d"
- " errno=%d",
- actualSize, (int) size, errno);
- } else {
- CursorWindow* window = new CursorWindow(name, dupAshmemFd,
- data, size, true /*readOnly*/);
- LOG_WINDOW("Created CursorWindow from parcel: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- window->mHeader->freeOffset,
- window->mHeader->numRows,
- window->mHeader->numColumns,
- window->mSize, window->mData);
- *outCursorWindow = window;
- return OK;
- }
- ::close(dupAshmemFd);
- }
+ window->mAshmemFd = -1;
+
+ if (window->mSize > kInlineSize) {
+ LOG(ERROR) << "Unexpected size " << window->mSize << " for inline window";
+ goto fail_silent;
}
+
+ window->mData = malloc(window->mSize);
+ if (!window->mData) goto fail;
+
+ if (parcel->read(window->mData, window->mSize)) goto fail;
}
- *outCursorWindow = NULL;
- return result;
+
+ // We just came from a remote source, so we're read-only
+ // and we can't inflate ourselves
+ window->mInflatedSize = window->mSize;
+ window->mReadOnly = true;
+
+ window->updateSlotsData();
+
+ LOG(DEBUG) << "Created from parcel: " << window->toString();
+ *outWindow = window;
+ return OK;
+
+fail:
+ LOG(ERROR) << "Failed createFromParcel";
+fail_silent:
+ delete window;
+ return UNKNOWN_ERROR;
}
status_t CursorWindow::writeToParcel(Parcel* parcel) {
- status_t status = parcel->writeString8(mName);
- if (!status) {
- status = parcel->writeDupFileDescriptor(mAshmemFd);
+ LOG(DEBUG) << "Writing to parcel: " << this->toString();
+
+ if (parcel->writeString8(mName)) goto fail;
+ if (parcel->writeUint32(mNumRows)) goto fail;
+ if (parcel->writeUint32(mNumColumns)) goto fail;
+ if (mAshmemFd != -1) {
+ if (parcel->writeUint32(mSize)) goto fail;
+ if (parcel->writeBool(true)) goto fail;
+ if (parcel->writeDupFileDescriptor(mAshmemFd)) goto fail;
+ } else {
+ // Since we know we're going to be read-only on the remote side,
+ // we can compact ourselves on the wire, with just enough padding
+ // to ensure our slots stay aligned
+ size_t slotsSize = mSize - mSlotsOffset;
+ size_t compactedSize = mAllocOffset + slotsSize;
+ compactedSize = (compactedSize + 3) & ~3;
+ if (parcel->writeUint32(compactedSize)) goto fail;
+ if (parcel->writeBool(false)) goto fail;
+ void* dest = parcel->writeInplace(compactedSize);
+ if (!dest) goto fail;
+ memcpy(static_cast<uint8_t*>(dest),
+ static_cast<uint8_t*>(mData), mAllocOffset);
+ memcpy(static_cast<uint8_t*>(dest) + compactedSize - slotsSize,
+ static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);
}
- return status;
+ return OK;
+
+fail:
+ LOG(ERROR) << "Failed writeToParcel";
+fail_silent:
+ return UNKNOWN_ERROR;
}
status_t CursorWindow::clear() {
if (mReadOnly) {
return INVALID_OPERATION;
}
+ mAllocOffset = 0;
+ mSlotsOffset = mSize;
+ mNumRows = 0;
+ mNumColumns = 0;
+ return OK;
+}
- mHeader->freeOffset = sizeof(Header) + sizeof(RowSlotChunk);
- mHeader->firstChunkOffset = sizeof(Header);
- mHeader->numRows = 0;
- mHeader->numColumns = 0;
+void CursorWindow::updateSlotsData() {
+ mSlotsStart = static_cast<uint8_t*>(mData) + mSize - kSlotSizeBytes;
+ mSlotsEnd = static_cast<uint8_t*>(mData) + mSlotsOffset;
+}
- RowSlotChunk* firstChunk = static_cast<RowSlotChunk*>(offsetToPtr(mHeader->firstChunkOffset));
- firstChunk->nextChunkOffset = 0;
- return OK;
+void* CursorWindow::offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) {
+ if (offset > mSize) {
+ LOG(ERROR) << "Offset " << offset
+ << " out of bounds, max value " << mSize;
+ return nullptr;
+ }
+ if (offset + bufferSize > mSize) {
+ LOG(ERROR) << "End offset " << (offset + bufferSize)
+ << " out of bounds, max value " << mSize;
+ return nullptr;
+ }
+ return static_cast<uint8_t*>(mData) + offset;
+}
+
+uint32_t CursorWindow::offsetFromPtr(void* ptr) {
+ return static_cast<uint8_t*>(ptr) - static_cast<uint8_t*>(mData);
}
status_t CursorWindow::setNumColumns(uint32_t numColumns) {
if (mReadOnly) {
return INVALID_OPERATION;
}
-
- uint32_t cur = mHeader->numColumns;
- if ((cur > 0 || mHeader->numRows > 0) && cur != numColumns) {
- ALOGE("Trying to go from %d columns to %d", cur, numColumns);
+ uint32_t cur = mNumColumns;
+ if ((cur > 0 || mNumRows > 0) && cur != numColumns) {
+ LOG(ERROR) << "Trying to go from " << cur << " columns to " << numColumns;
return INVALID_OPERATION;
}
- mHeader->numColumns = numColumns;
+ mNumColumns = numColumns;
return OK;
}
@@ -181,28 +290,19 @@ status_t CursorWindow::allocRow() {
if (mReadOnly) {
return INVALID_OPERATION;
}
-
- // Fill in the row slot
- RowSlot* rowSlot = allocRowSlot();
- if (rowSlot == NULL) {
- return NO_MEMORY;
- }
-
- // Allocate the slots for the field directory
- size_t fieldDirSize = mHeader->numColumns * sizeof(FieldSlot);
- uint32_t fieldDirOffset = alloc(fieldDirSize, true /*aligned*/);
- if (!fieldDirOffset) {
- mHeader->numRows--;
- LOG_WINDOW("The row failed, so back out the new row accounting "
- "from allocRowSlot %d", mHeader->numRows);
- return NO_MEMORY;
+ size_t size = mNumColumns * kSlotSizeBytes;
+ int32_t newOffset = mSlotsOffset - size;
+ if (newOffset < (int32_t) mAllocOffset) {
+ maybeInflate();
+ newOffset = mSlotsOffset - size;
+ if (newOffset < (int32_t) mAllocOffset) {
+ return NO_MEMORY;
+ }
}
- FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(fieldDirOffset));
- memset(fieldDir, 0, fieldDirSize);
-
- LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %zu bytes at offset %u\n",
- mHeader->numRows - 1, offsetFromPtr(rowSlot), fieldDirSize, fieldDirOffset);
- rowSlot->offset = fieldDirOffset;
+ memset(offsetToPtr(newOffset), 0, size);
+ mSlotsOffset = newOffset;
+ updateSlotsData();
+ mNumRows++;
return OK;
}
@@ -210,83 +310,48 @@ status_t CursorWindow::freeLastRow() {
if (mReadOnly) {
return INVALID_OPERATION;
}
-
- if (mHeader->numRows > 0) {
- mHeader->numRows--;
+ size_t size = mNumColumns * kSlotSizeBytes;
+ size_t newOffset = mSlotsOffset + size;
+ if (newOffset > mSize) {
+ return NO_MEMORY;
}
+ mSlotsOffset = newOffset;
+ updateSlotsData();
+ mNumRows--;
return OK;
}
-uint32_t CursorWindow::alloc(size_t size, bool aligned) {
- uint32_t padding;
- if (aligned) {
- // 4 byte alignment
- padding = (~mHeader->freeOffset + 1) & 3;
- } else {
- padding = 0;
- }
-
- uint32_t offset = mHeader->freeOffset + padding;
- uint32_t nextFreeOffset = offset + size;
- if (nextFreeOffset > mSize) {
- ALOGW("Window is full: requested allocation %zu bytes, "
- "free space %zu bytes, window size %zu bytes",
- size, freeSpace(), mSize);
- return 0;
- }
-
- mHeader->freeOffset = nextFreeOffset;
- return offset;
-}
-
-CursorWindow::RowSlot* CursorWindow::getRowSlot(uint32_t row) {
- uint32_t chunkPos = row;
- RowSlotChunk* chunk = static_cast<RowSlotChunk*>(
- offsetToPtr(mHeader->firstChunkOffset));
- while (chunkPos >= ROW_SLOT_CHUNK_NUM_ROWS) {
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
- }
- return &chunk->slots[chunkPos];
-}
-
-CursorWindow::RowSlot* CursorWindow::allocRowSlot() {
- uint32_t chunkPos = mHeader->numRows;
- RowSlotChunk* chunk = static_cast<RowSlotChunk*>(
- offsetToPtr(mHeader->firstChunkOffset));
- while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) {
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
+status_t CursorWindow::alloc(size_t size, uint32_t* outOffset) {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
}
- if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) {
- if (!chunk->nextChunkOffset) {
- chunk->nextChunkOffset = alloc(sizeof(RowSlotChunk), true /*aligned*/);
- if (!chunk->nextChunkOffset) {
- return NULL;
- }
+ size_t alignedSize = (size + 3) & ~3;
+ size_t newOffset = mAllocOffset + alignedSize;
+ if (newOffset > mSlotsOffset) {
+ maybeInflate();
+ newOffset = mAllocOffset + alignedSize;
+ if (newOffset > mSlotsOffset) {
+ return NO_MEMORY;
}
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunk->nextChunkOffset = 0;
- chunkPos = 0;
}
- mHeader->numRows += 1;
- return &chunk->slots[chunkPos];
+ *outOffset = mAllocOffset;
+ mAllocOffset = newOffset;
+ return OK;
}
CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row, uint32_t column) {
- if (row >= mHeader->numRows || column >= mHeader->numColumns) {
- ALOGE("Failed to read row %d, column %d from a CursorWindow which "
- "has %d rows, %d columns.",
- row, column, mHeader->numRows, mHeader->numColumns);
- return NULL;
- }
- RowSlot* rowSlot = getRowSlot(row);
- if (!rowSlot) {
- ALOGE("Failed to find rowSlot for row %d.", row);
- return NULL;
+ // This is carefully tuned to use as few cycles as
+ // possible, since this is an extremely hot code path;
+ // see CursorWindow_bench.cpp for more details
+ void *result = static_cast<uint8_t*>(mSlotsStart)
+ - (((row * mNumColumns) + column) << kSlotShift);
+ if (result < mSlotsEnd || result > mSlotsStart || column >= mNumColumns) {
+ LOG(ERROR) << "Failed to read row " << row << ", column " << column
+ << " from a window with " << mNumRows << " rows, " << mNumColumns << " columns";
+ return nullptr;
+ } else {
+ return static_cast<FieldSlot*>(result);
}
- FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(rowSlot->offset));
- return &fieldDir[column];
}
status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value, size_t size) {
@@ -309,13 +374,14 @@ status_t CursorWindow::putBlobOrString(uint32_t row, uint32_t column,
return BAD_VALUE;
}
- uint32_t offset = alloc(size);
- if (!offset) {
+ uint32_t offset;
+ if (alloc(size, &offset)) {
return NO_MEMORY;
}
memcpy(offsetToPtr(offset), value, size);
+ fieldSlot = getFieldSlot(row, column);
fieldSlot->type = type;
fieldSlot->data.buffer.offset = offset;
fieldSlot->data.buffer.size = size;
diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp
new file mode 100644
index 000000000000..2dac47b0dac6
--- /dev/null
+++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp
@@ -0,0 +1,31 @@
+cc_fuzz {
+ name: "cursorwindow_fuzzer",
+ srcs: [
+ "cursorwindow_fuzzer.cpp",
+ ],
+ host_supported: true,
+ corpus: ["corpus/*"],
+ static_libs: ["libgmock"],
+ target: {
+ android: {
+ shared_libs: [
+ "libandroidfw_fuzzer_lib",
+ "libbase",
+ "libbinder",
+ "libcutils",
+ "liblog",
+ "libutils",
+ ],
+ },
+ host: {
+ static_libs: [
+ "libandroidfw_fuzzer_lib",
+ "libbase",
+ "libbinder",
+ "libcutils",
+ "liblog",
+ "libutils",
+ ],
+ },
+ },
+}
diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin b/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin
new file mode 100644
index 000000000000..c7e22dd26ea7
--- /dev/null
+++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin
Binary files differ
diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp b/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp
new file mode 100644
index 000000000000..8dce21220199
--- /dev/null
+++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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 <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <string>
+#include <memory>
+
+#include "android-base/logging.h"
+#include "androidfw/CursorWindow.h"
+#include "binder/Parcel.h"
+
+#include <fuzzer/FuzzedDataProvider.h>
+
+using android::CursorWindow;
+using android::Parcel;
+
+extern "C" int LLVMFuzzerInitialize(int *, char ***) {
+ setenv("ANDROID_LOG_TAGS", "*:s", 1);
+ android::base::InitLogging(nullptr, &android::base::StderrLogger);
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ Parcel p;
+ p.setData(data, size);
+
+ CursorWindow* w = nullptr;
+ if (!CursorWindow::createFromParcel(&p, &w)) {
+ LOG(WARNING) << "Valid cursor with " << w->getNumRows() << " rows, "
+ << w->getNumColumns() << " cols";
+
+ // Try obtaining heap allocations for most items; we trim the
+ // search space to speed things up
+ auto rows = std::min(w->getNumRows(), static_cast<uint32_t>(128));
+ auto cols = std::min(w->getNumColumns(), static_cast<uint32_t>(128));
+ for (auto row = 0; row < rows; row++) {
+ for (auto col = 0; col < cols; col++) {
+ auto field = w->getFieldSlot(row, col);
+ if (!field) continue;
+ switch (w->getFieldSlotType(field)) {
+ case CursorWindow::FIELD_TYPE_STRING: {
+ size_t size;
+ w->getFieldSlotValueString(field, &size);
+ break;
+ }
+ case CursorWindow::FIELD_TYPE_BLOB: {
+ size_t size;
+ w->getFieldSlotValueBlob(field, &size);
+ break;
+ }
+ }
+ }
+ }
+
+ // Finally, try obtaining the furthest valid field
+ if (rows > 0 && cols > 0) {
+ w->getFieldSlot(w->getNumRows() - 1, w->getNumColumns() - 1);
+ }
+ }
+ delete w;
+
+ return 0;
+}
diff --git a/libs/androidfw/include/androidfw/BackupHelpers.h b/libs/androidfw/include/androidfw/BackupHelpers.h
index 2da247b77c0a..a0fa13662cb9 100644
--- a/libs/androidfw/include/androidfw/BackupHelpers.h
+++ b/libs/androidfw/include/androidfw/BackupHelpers.h
@@ -137,7 +137,7 @@ int back_up_files(int oldSnapshotFD, BackupDataWriter* dataStream, int newSnapsh
char const* const* files, char const* const *keys, int fileCount);
int write_tarfile(const String8& packageName, const String8& domain,
- const String8& rootPath, const String8& filePath, off_t* outSize,
+ const String8& rootPath, const String8& filePath, off64_t* outSize,
BackupDataWriter* outputStream);
class RestoreHelperBase
diff --git a/libs/androidfw/include/androidfw/CursorWindow.h b/libs/androidfw/include/androidfw/CursorWindow.h
index ad64b246b3f5..6e55a9a0eb8b 100644
--- a/libs/androidfw/include/androidfw/CursorWindow.h
+++ b/libs/androidfw/include/androidfw/CursorWindow.h
@@ -20,38 +20,36 @@
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
+#include <string>
-#include <binder/Parcel.h>
-#include <log/log.h>
-#include <utils/String8.h>
+#include "android-base/stringprintf.h"
+#include "binder/Parcel.h"
+#include "utils/String8.h"
-#if LOG_NDEBUG
-
-#define IF_LOG_WINDOW() if (false)
#define LOG_WINDOW(...)
-#else
-
-#define IF_LOG_WINDOW() IF_ALOG(LOG_DEBUG, "CursorWindow")
-#define LOG_WINDOW(...) ALOG(LOG_DEBUG, "CursorWindow", __VA_ARGS__)
-
-#endif
-
namespace android {
/**
- * This class stores a set of rows from a database in a buffer. The begining of the
- * window has first chunk of RowSlots, which are offsets to the row directory, followed by
- * an offset to the next chunk in a linked-list of additional chunk of RowSlots in case
- * the pre-allocated chunk isn't big enough to refer to all rows. Each row directory has a
- * FieldSlot per column, which has the size, offset, and type of the data for that field.
- * Note that the data types come from sqlite3.h.
+ * This class stores a set of rows from a database in a buffer. Internally
+ * data is structured as a "heap" of string/blob allocations at the bottom
+ * of the memory region, and a "stack" of FieldSlot allocations at the top
+ * of the memory region. Here's an example visual representation:
+ *
+ * +----------------------------------------------------------------+
+ * |heap\0of\0strings\0 222211110000| ...
+ * +-------------------+--------------------------------+-------+---+
+ * ^ ^ ^ ^ ^ ^
+ * | | | | | |
+ * | +- mAllocOffset mSlotsOffset -+ | | |
+ * +- mData mSlotsStart -+ | |
+ * mSize -+ |
+ * mInflatedSize -+
*
* Strings are stored in UTF-8.
*/
class CursorWindow {
- CursorWindow(const String8& name, int ashmemFd,
- void* data, size_t size, bool readOnly);
+ CursorWindow();
public:
/* Field types. */
@@ -88,9 +86,9 @@ public:
inline String8 name() { return mName; }
inline size_t size() { return mSize; }
- inline size_t freeSpace() { return mSize - mHeader->freeOffset; }
- inline uint32_t getNumRows() { return mHeader->numRows; }
- inline uint32_t getNumColumns() { return mHeader->numColumns; }
+ inline size_t freeSpace() { return mSlotsOffset - mAllocOffset; }
+ inline uint32_t getNumRows() { return mNumRows; }
+ inline uint32_t getNumColumns() { return mNumColumns; }
status_t clear();
status_t setNumColumns(uint32_t numColumns);
@@ -138,62 +136,57 @@ public:
return offsetToPtr(fieldSlot->data.buffer.offset, fieldSlot->data.buffer.size);
}
-private:
- static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100;
-
- struct Header {
- // Offset of the lowest unused byte in the window.
- uint32_t freeOffset;
-
- // Offset of the first row slot chunk.
- uint32_t firstChunkOffset;
-
- uint32_t numRows;
- uint32_t numColumns;
- };
-
- struct RowSlot {
- uint32_t offset;
- };
-
- struct RowSlotChunk {
- RowSlot slots[ROW_SLOT_CHUNK_NUM_ROWS];
- uint32_t nextChunkOffset;
- };
+ inline std::string toString() const {
+ return android::base::StringPrintf("CursorWindow{name=%s, fd=%d, size=%d, inflatedSize=%d, "
+ "allocOffset=%d, slotsOffset=%d, numRows=%d, numColumns=%d}", mName.c_str(),
+ mAshmemFd, mSize, mInflatedSize, mAllocOffset, mSlotsOffset, mNumRows, mNumColumns);
+ }
+private:
String8 mName;
- int mAshmemFd;
- void* mData;
- size_t mSize;
- bool mReadOnly;
- Header* mHeader;
-
- inline void* offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) {
- if (offset >= mSize) {
- ALOGE("Offset %" PRIu32 " out of bounds, max value %zu", offset, mSize);
- return NULL;
- }
- if (offset + bufferSize > mSize) {
- ALOGE("End offset %" PRIu32 " out of bounds, max value %zu",
- offset + bufferSize, mSize);
- return NULL;
- }
- return static_cast<uint8_t*>(mData) + offset;
- }
+ int mAshmemFd = -1;
+ void* mData = nullptr;
+ /**
+ * Pointer to the first FieldSlot, used to optimize the extremely
+ * hot code path of getFieldSlot().
+ */
+ void* mSlotsStart = nullptr;
+ void* mSlotsEnd = nullptr;
+ uint32_t mSize = 0;
+ /**
+ * When a window starts as lightweight inline allocation, this value
+ * holds the "full" size to be created after ashmem inflation.
+ */
+ uint32_t mInflatedSize = 0;
+ /**
+ * Offset to the top of the "heap" of string/blob allocations. By
+ * storing these allocations at the bottom of our memory region we
+ * avoid having to rewrite offsets when inflating.
+ */
+ uint32_t mAllocOffset = 0;
+ /**
+ * Offset to the bottom of the "stack" of FieldSlot allocations.
+ */
+ uint32_t mSlotsOffset = 0;
+ uint32_t mNumRows = 0;
+ uint32_t mNumColumns = 0;
+ bool mReadOnly = false;
- inline uint32_t offsetFromPtr(void* ptr) {
- return static_cast<uint8_t*>(ptr) - static_cast<uint8_t*>(mData);
- }
+ void updateSlotsData();
+
+ void* offsetToPtr(uint32_t offset, uint32_t bufferSize);
+ uint32_t offsetFromPtr(void* ptr);
/**
- * Allocate a portion of the window. Returns the offset
- * of the allocation, or 0 if there isn't enough space.
- * If aligned is true, the allocation gets 4 byte alignment.
+ * By default windows are lightweight inline allocations; this method
+ * inflates the window into a larger ashmem region.
*/
- uint32_t alloc(size_t size, bool aligned = false);
+ status_t maybeInflate();
- RowSlot* getRowSlot(uint32_t row);
- RowSlot* allocRowSlot();
+ /**
+ * Allocate a portion of the window.
+ */
+ status_t alloc(size_t size, uint32_t* outOffset);
status_t putBlobOrString(uint32_t row, uint32_t column,
const void* value, size_t size, int32_t type);
diff --git a/libs/androidfw/tests/BackupHelpers_test.cpp b/libs/androidfw/tests/BackupHelpers_test.cpp
new file mode 100644
index 000000000000..86b7fb361228
--- /dev/null
+++ b/libs/androidfw/tests/BackupHelpers_test.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "BackupHelpers_test"
+#include <androidfw/BackupHelpers.h>
+
+#include <gtest/gtest.h>
+
+#include <fcntl.h>
+#include <utils/String8.h>
+#include <android-base/file.h>
+
+namespace android {
+
+class BackupHelpersTest : public testing::Test {
+protected:
+
+ virtual void SetUp() {
+ }
+ virtual void TearDown() {
+ }
+};
+
+TEST_F(BackupHelpersTest, WriteTarFileWithSizeLessThan2GB) {
+ TemporaryFile tf;
+ // Allocate a 1 KB file.
+ off64_t fileSize = 1024;
+ ASSERT_EQ(0, posix_fallocate64(tf.fd, 0, fileSize));
+ off64_t tarSize = 0;
+ int err = write_tarfile(/* packageName */ String8("test-pkg"), /* domain */ String8(""), /* rootpath */ String8(""), /* filePath */ String8(tf.path), /* outSize */ &tarSize, /* writer */ NULL);
+ ASSERT_EQ(err, 0);
+ // Returned tarSize includes 512 B for the header.
+ off64_t expectedTarSize = fileSize + 512;
+ ASSERT_EQ(tarSize, expectedTarSize);
+}
+
+TEST_F(BackupHelpersTest, WriteTarFileWithSizeGreaterThan2GB) {
+ TemporaryFile tf;
+ // Allocate a 2 GB file.
+ off64_t fileSize = 2ll * 1024ll * 1024ll * 1024ll + 512ll;
+ ASSERT_EQ(0, posix_fallocate64(tf.fd, 0, fileSize));
+ off64_t tarSize = 0;
+ int err = write_tarfile(/* packageName */ String8("test-pkg"), /* domain */ String8(""), /* rootpath */ String8(""), /* filePath */ String8(tf.path), /* outSize */ &tarSize, /* writer */ NULL);
+ ASSERT_EQ(err, 0);
+ // Returned tarSize includes 512 B for the header.
+ off64_t expectedTarSize = fileSize + 512;
+ ASSERT_EQ(tarSize, expectedTarSize);
+}
+}
+
diff --git a/libs/androidfw/tests/CursorWindow_bench.cpp b/libs/androidfw/tests/CursorWindow_bench.cpp
new file mode 100644
index 000000000000..f1191c3d7213
--- /dev/null
+++ b/libs/androidfw/tests/CursorWindow_bench.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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 "benchmark/benchmark.h"
+
+#include "androidfw/CursorWindow.h"
+
+namespace android {
+
+static void BM_CursorWindowWrite(benchmark::State& state, size_t rows, size_t cols) {
+ CursorWindow* w;
+ CursorWindow::create(String8("test"), 1 << 21, &w);
+
+ while (state.KeepRunning()) {
+ w->clear();
+ w->setNumColumns(cols);
+ for (int row = 0; row < rows; row++) {
+ w->allocRow();
+ for (int col = 0; col < cols; col++) {
+ w->putLong(row, col, 0xcafe);
+ }
+ }
+ }
+}
+
+static void BM_CursorWindowWrite4x4(benchmark::State& state) {
+ BM_CursorWindowWrite(state, 4, 4);
+}
+BENCHMARK(BM_CursorWindowWrite4x4);
+
+static void BM_CursorWindowWrite1Kx4(benchmark::State& state) {
+ BM_CursorWindowWrite(state, 1024, 4);
+}
+BENCHMARK(BM_CursorWindowWrite1Kx4);
+
+static void BM_CursorWindowWrite16Kx4(benchmark::State& state) {
+ BM_CursorWindowWrite(state, 16384, 4);
+}
+BENCHMARK(BM_CursorWindowWrite16Kx4);
+
+static void BM_CursorWindowRead(benchmark::State& state, size_t rows, size_t cols) {
+ CursorWindow* w;
+ CursorWindow::create(String8("test"), 1 << 21, &w);
+ w->setNumColumns(cols);
+ for (int row = 0; row < rows; row++) {
+ w->allocRow();
+ }
+
+ while (state.KeepRunning()) {
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < cols; col++) {
+ w->getFieldSlot(row, col);
+ }
+ }
+ }
+}
+
+static void BM_CursorWindowRead4x4(benchmark::State& state) {
+ BM_CursorWindowRead(state, 4, 4);
+}
+BENCHMARK(BM_CursorWindowRead4x4);
+
+static void BM_CursorWindowRead1Kx4(benchmark::State& state) {
+ BM_CursorWindowRead(state, 1024, 4);
+}
+BENCHMARK(BM_CursorWindowRead1Kx4);
+
+static void BM_CursorWindowRead16Kx4(benchmark::State& state) {
+ BM_CursorWindowRead(state, 16384, 4);
+}
+BENCHMARK(BM_CursorWindowRead16Kx4);
+
+} // namespace android
diff --git a/libs/androidfw/tests/CursorWindow_test.cpp b/libs/androidfw/tests/CursorWindow_test.cpp
new file mode 100644
index 000000000000..15be80c48192
--- /dev/null
+++ b/libs/androidfw/tests/CursorWindow_test.cpp
@@ -0,0 +1,367 @@
+/*
+ * 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 <utility>
+
+#include "androidfw/CursorWindow.h"
+
+#include "TestHelpers.h"
+
+#define CREATE_WINDOW_1K \
+ CursorWindow* w; \
+ CursorWindow::create(String8("test"), 1 << 10, &w);
+
+#define CREATE_WINDOW_1K_3X3 \
+ CursorWindow* w; \
+ CursorWindow::create(String8("test"), 1 << 10, &w); \
+ ASSERT_EQ(w->setNumColumns(3), OK); \
+ ASSERT_EQ(w->allocRow(), OK); \
+ ASSERT_EQ(w->allocRow(), OK); \
+ ASSERT_EQ(w->allocRow(), OK);
+
+#define CREATE_WINDOW_2M \
+ CursorWindow* w; \
+ CursorWindow::create(String8("test"), 1 << 21, &w);
+
+static constexpr const size_t kHalfInlineSize = 8192;
+static constexpr const size_t kGiantSize = 1048576;
+
+namespace android {
+
+TEST(CursorWindowTest, Empty) {
+ CREATE_WINDOW_1K;
+
+ ASSERT_EQ(w->getNumRows(), 0);
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->size(), 1 << 10);
+ ASSERT_EQ(w->freeSpace(), 1 << 10);
+}
+
+TEST(CursorWindowTest, SetNumColumns) {
+ CREATE_WINDOW_1K;
+
+ // Once we've locked in columns, we can't adjust
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_NE(w->setNumColumns(5), OK);
+ ASSERT_NE(w->setNumColumns(3), OK);
+ ASSERT_EQ(w->getNumColumns(), 4);
+}
+
+TEST(CursorWindowTest, SetNumColumnsAfterRow) {
+ CREATE_WINDOW_1K;
+
+ // Once we've locked in a row, we can't adjust columns
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->allocRow(), OK);
+ ASSERT_NE(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->getNumColumns(), 0);
+}
+
+TEST(CursorWindowTest, AllocRow) {
+ CREATE_WINDOW_1K;
+
+ ASSERT_EQ(w->setNumColumns(4), OK);
+
+ // Rolling forward means we have less free space
+ ASSERT_EQ(w->getNumRows(), 0);
+ auto before = w->freeSpace();
+ ASSERT_EQ(w->allocRow(), OK);
+ ASSERT_LT(w->freeSpace(), before);
+ ASSERT_EQ(w->getNumRows(), 1);
+
+ // Verify we can unwind
+ ASSERT_EQ(w->freeLastRow(), OK);
+ ASSERT_EQ(w->freeSpace(), before);
+ ASSERT_EQ(w->getNumRows(), 0);
+
+ // Can't unwind when no rows left
+ ASSERT_NE(w->freeLastRow(), OK);
+}
+
+TEST(CursorWindowTest, AllocRowBounds) {
+ CREATE_WINDOW_1K;
+
+ // 60 columns is 960 bytes, which means only a single row can fit
+ ASSERT_EQ(w->setNumColumns(60), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+ ASSERT_NE(w->allocRow(), OK);
+}
+
+TEST(CursorWindowTest, StoreNull) {
+ CREATE_WINDOW_1K_3X3;
+
+ ASSERT_EQ(w->putNull(1, 1), OK);
+ ASSERT_EQ(w->putNull(0, 0), OK);
+
+ {
+ auto field = w->getFieldSlot(1, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL);
+ }
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL);
+ }
+}
+
+TEST(CursorWindowTest, StoreLong) {
+ CREATE_WINDOW_1K_3X3;
+
+ ASSERT_EQ(w->putLong(1, 1, 0xf00d), OK);
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ {
+ auto field = w->getFieldSlot(1, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xf00d);
+ }
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+}
+
+TEST(CursorWindowTest, StoreString) {
+ CREATE_WINDOW_1K_3X3;
+
+ ASSERT_EQ(w->putString(1, 1, "food", 5), OK);
+ ASSERT_EQ(w->putString(0, 0, "cafe", 5), OK);
+
+ size_t size;
+ {
+ auto field = w->getFieldSlot(1, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING);
+ auto actual = w->getFieldSlotValueString(field, &size);
+ ASSERT_EQ(std::string(actual), "food");
+ }
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING);
+ auto actual = w->getFieldSlotValueString(field, &size);
+ ASSERT_EQ(std::string(actual), "cafe");
+ }
+}
+
+TEST(CursorWindowTest, StoreBounds) {
+ CREATE_WINDOW_1K_3X3;
+
+ // Can't work with values beyond bounds
+ ASSERT_NE(w->putLong(0, 3, 0xcafe), OK);
+ ASSERT_NE(w->putLong(3, 0, 0xcafe), OK);
+ ASSERT_NE(w->putLong(3, 3, 0xcafe), OK);
+ ASSERT_EQ(w->getFieldSlot(0, 3), nullptr);
+ ASSERT_EQ(w->getFieldSlot(3, 0), nullptr);
+ ASSERT_EQ(w->getFieldSlot(3, 3), nullptr);
+
+ // Can't work with invalid indexes
+ ASSERT_NE(w->putLong(-1, 0, 0xcafe), OK);
+ ASSERT_NE(w->putLong(0, -1, 0xcafe), OK);
+ ASSERT_NE(w->putLong(-1, -1, 0xcafe), OK);
+ ASSERT_EQ(w->getFieldSlot(-1, 0), nullptr);
+ ASSERT_EQ(w->getFieldSlot(0, -1), nullptr);
+ ASSERT_EQ(w->getFieldSlot(-1, -1), nullptr);
+}
+
+TEST(CursorWindowTest, Inflate) {
+ CREATE_WINDOW_2M;
+
+ auto before = w->size();
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+
+ // Scratch buffer that will fit before inflation
+ void* buf = malloc(kHalfInlineSize);
+
+ // Store simple value
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ // Store first object that fits inside
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK);
+ ASSERT_EQ(w->size(), before);
+
+ // Store second simple value
+ ASSERT_EQ(w->putLong(0, 2, 0xface), OK);
+
+ // Store second object that requires inflation
+ memset(buf, 84, kHalfInlineSize);
+ ASSERT_EQ(w->putBlob(0, 3, buf, kHalfInlineSize), OK);
+ ASSERT_GT(w->size(), before);
+
+ // Verify data is intact
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+ {
+ auto field = w->getFieldSlot(0, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kHalfInlineSize);
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_NE(actual, buf);
+ ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0);
+ }
+ {
+ auto field = w->getFieldSlot(0, 2);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xface);
+ }
+ {
+ auto field = w->getFieldSlot(0, 3);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kHalfInlineSize);
+ memset(buf, 84, kHalfInlineSize);
+ ASSERT_NE(actual, buf);
+ ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0);
+ }
+}
+
+TEST(CursorWindowTest, ParcelEmpty) {
+ CREATE_WINDOW_2M;
+
+ Parcel p;
+ w->writeToParcel(&p);
+ p.setDataPosition(0);
+ w = nullptr;
+
+ ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK);
+ ASSERT_EQ(w->getNumRows(), 0);
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->size(), 0);
+ ASSERT_EQ(w->freeSpace(), 0);
+
+ // We can't mutate the window after parceling
+ ASSERT_NE(w->setNumColumns(4), OK);
+ ASSERT_NE(w->allocRow(), OK);
+}
+
+TEST(CursorWindowTest, ParcelSmall) {
+ CREATE_WINDOW_2M;
+
+ auto before = w->size();
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+
+ // Scratch buffer that will fit before inflation
+ void* buf = malloc(kHalfInlineSize);
+
+ // Store simple value
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ // Store first object that fits inside
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK);
+ ASSERT_EQ(w->size(), before);
+
+ // Store second object with zero length
+ ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK);
+ ASSERT_EQ(w->size(), before);
+
+ // Force through a parcel
+ Parcel p;
+ w->writeToParcel(&p);
+ p.setDataPosition(0);
+ w = nullptr;
+
+ ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK);
+ ASSERT_EQ(w->getNumRows(), 1);
+ ASSERT_EQ(w->getNumColumns(), 4);
+
+ // Verify data is intact
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+ {
+ auto field = w->getFieldSlot(0, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kHalfInlineSize);
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_NE(actual, buf);
+ ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0);
+ }
+ {
+ auto field = w->getFieldSlot(0, 2);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, 0);
+ ASSERT_NE(actual, nullptr);
+ }
+}
+
+TEST(CursorWindowTest, ParcelLarge) {
+ CREATE_WINDOW_2M;
+
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+
+ // Store simple value
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ // Store object that forces inflation
+ void* buf = malloc(kGiantSize);
+ memset(buf, 42, kGiantSize);
+ ASSERT_EQ(w->putBlob(0, 1, buf, kGiantSize), OK);
+
+ // Store second object with zero length
+ ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK);
+
+ // Force through a parcel
+ Parcel p;
+ w->writeToParcel(&p);
+ p.setDataPosition(0);
+ w = nullptr;
+
+ ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK);
+ ASSERT_EQ(w->getNumRows(), 1);
+ ASSERT_EQ(w->getNumColumns(), 4);
+
+ // Verify data is intact
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+ {
+ auto field = w->getFieldSlot(0, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kGiantSize);
+ memset(buf, 42, kGiantSize);
+ ASSERT_EQ(memcmp(buf, actual, kGiantSize), 0);
+ }
+ {
+ auto field = w->getFieldSlot(0, 2);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, 0);
+ ASSERT_NE(actual, nullptr);
+ }
+}
+
+} // android
diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp
index e713b98b867e..9d83e491fdc1 100644
--- a/libs/hostgraphics/Android.bp
+++ b/libs/hostgraphics/Android.bp
@@ -5,6 +5,10 @@ cc_library_host_static {
"-Wno-unused-parameter",
],
+ static_libs: [
+ "libbase",
+ ],
+
srcs: [
":libui_host_common",
"Fence.cpp",
@@ -28,4 +32,4 @@ cc_library_host_static {
enabled: true,
}
},
-} \ No newline at end of file
+}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index aa842ff6a7b7..155bb6ba8f75 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -53,8 +53,6 @@ cc_defaults {
host: {
include_dirs: [
"external/vulkan-headers/include",
- "frameworks/native/libs/math/include",
- "frameworks/native/libs/ui/include",
],
cflags: [
"-Wno-unused-variable",
@@ -71,6 +69,10 @@ cc_defaults {
"libminikin",
],
+ static_libs: [
+ "libui-types",
+ ],
+
target: {
android: {
shared_libs: [
@@ -83,7 +85,6 @@ cc_defaults {
"libGLESv2",
"libGLESv3",
"libvulkan",
- "libui",
"libnativedisplay",
"libnativewindow",
"libprotobuf-cpp-lite",
@@ -152,6 +153,45 @@ cc_defaults {
}
// ------------------------
+// framework-graphics jar
+// ------------------------
+
+java_sdk_library {
+ name: "framework-graphics",
+ defaults: ["framework-module-defaults"],
+ visibility: [
+ "//frameworks/base", // Framework
+ ],
+
+ srcs: [
+ ":framework-graphics-srcs",
+ ],
+
+ permitted_packages: [
+ "android.graphics",
+ ],
+
+ // TODO: once framework-graphics is officially part of the
+ // UI-rendering module this line would no longer be
+ // needed.
+ installable: true,
+
+ // Disable api_lint that the defaults enable
+ // TODO: enable this
+ api_lint: {
+ enabled: false,
+ },
+}
+
+filegroup {
+ name: "framework-graphics-srcs",
+ srcs: [
+ "apex/java/**/*.java",
+ ],
+ path: "apex/java"
+}
+
+// ------------------------
// APEX
// ------------------------
@@ -287,6 +327,7 @@ cc_defaults {
"jni/PathMeasure.cpp",
"jni/Picture.cpp",
"jni/Shader.cpp",
+ "jni/RenderEffect.cpp",
"jni/Typeface.cpp",
"jni/Utils.cpp",
"jni/YuvToJpegEncoder.cpp",
@@ -294,6 +335,7 @@ cc_defaults {
"jni/fonts/FontFamily.cpp",
"jni/text/LineBreaker.cpp",
"jni/text/MeasuredText.cpp",
+ "jni/text/TextShaper.cpp",
],
header_libs: [ "android_graphics_jni_headers" ],
@@ -346,6 +388,7 @@ cc_defaults {
"libstatspull",
"libstatssocket",
"libpdfium",
+ "libbinder_ndk",
],
static_libs: [
"libgif",
@@ -457,6 +500,7 @@ cc_defaults {
"service/GraphicsStatsService.cpp",
"thread/CommonPool.cpp",
"utils/GLUtils.cpp",
+ "utils/NdkUtils.cpp",
"utils/StringUtils.cpp",
"AutoBackendTextureRelease.cpp",
"DeferredLayerUpdater.cpp",
@@ -499,6 +543,11 @@ cc_library {
"android_graphics_jni",
],
export_header_lib_headers: ["android_graphics_apex_headers"],
+ target: {
+ android: {
+ version_script: "libhwui.map.txt",
+ }
+ },
}
cc_library_static {
@@ -516,6 +565,7 @@ cc_defaults {
android: {
shared_libs: [
"libgui",
+ "libui",
],
}
},
diff --git a/libs/hwui/AnimationContext.h b/libs/hwui/AnimationContext.h
index 74d5e79c0b77..f8a2072ffbdb 100644
--- a/libs/hwui/AnimationContext.h
+++ b/libs/hwui/AnimationContext.h
@@ -77,8 +77,8 @@ class AnimationContext {
PREVENT_COPY_AND_ASSIGN(AnimationContext);
public:
- ANDROID_API explicit AnimationContext(renderthread::TimeLord& clock);
- ANDROID_API virtual ~AnimationContext();
+ explicit AnimationContext(renderthread::TimeLord& clock);
+ virtual ~AnimationContext();
nsecs_t frameTimeMs() { return mFrameTimeMs; }
bool hasAnimations() {
@@ -87,22 +87,22 @@ public:
// Will always add to the next frame list, which is swapped when
// startFrame() is called
- ANDROID_API void addAnimatingRenderNode(RenderNode& node);
+ void addAnimatingRenderNode(RenderNode& node);
// Marks the start of a frame, which will update the frame time and move all
// next frame animations into the current frame
- ANDROID_API virtual void startFrame(TreeInfo::TraversalMode mode);
+ virtual void startFrame(TreeInfo::TraversalMode mode);
// Runs any animations still left in mCurrentFrameAnimations that were not run
// as part of the standard RenderNode:prepareTree pass.
- ANDROID_API virtual void runRemainingAnimations(TreeInfo& info);
+ virtual void runRemainingAnimations(TreeInfo& info);
- ANDROID_API virtual void callOnFinished(BaseRenderNodeAnimator* animator,
+ virtual void callOnFinished(BaseRenderNodeAnimator* animator,
AnimationListener* listener);
- ANDROID_API virtual void destroy();
+ virtual void destroy();
- ANDROID_API virtual void pauseAnimators() {}
+ virtual void pauseAnimators() {}
private:
friend class AnimationHandle;
diff --git a/libs/hwui/Animator.h b/libs/hwui/Animator.h
index ed7b6eb1cf4a..3c9f1ea1b6e3 100644
--- a/libs/hwui/Animator.h
+++ b/libs/hwui/Animator.h
@@ -39,10 +39,10 @@ class RenderProperties;
class AnimationListener : public VirtualLightRefBase {
public:
- ANDROID_API virtual void onAnimationFinished(BaseRenderNodeAnimator*) = 0;
+ virtual void onAnimationFinished(BaseRenderNodeAnimator*) = 0;
protected:
- ANDROID_API virtual ~AnimationListener() {}
+ virtual ~AnimationListener() {}
};
enum class RepeatMode {
@@ -55,34 +55,34 @@ class BaseRenderNodeAnimator : public VirtualLightRefBase {
PREVENT_COPY_AND_ASSIGN(BaseRenderNodeAnimator);
public:
- ANDROID_API void setStartValue(float value);
- ANDROID_API void setInterpolator(Interpolator* interpolator);
- ANDROID_API void setDuration(nsecs_t durationInMs);
- ANDROID_API nsecs_t duration() { return mDuration; }
- ANDROID_API void setStartDelay(nsecs_t startDelayInMs);
- ANDROID_API nsecs_t startDelay() { return mStartDelay; }
- ANDROID_API void setListener(AnimationListener* listener) { mListener = listener; }
+ void setStartValue(float value);
+ void setInterpolator(Interpolator* interpolator);
+ void setDuration(nsecs_t durationInMs);
+ nsecs_t duration() { return mDuration; }
+ void setStartDelay(nsecs_t startDelayInMs);
+ nsecs_t startDelay() { return mStartDelay; }
+ void setListener(AnimationListener* listener) { mListener = listener; }
AnimationListener* listener() { return mListener.get(); }
- ANDROID_API void setAllowRunningAsync(bool mayRunAsync) { mMayRunAsync = mayRunAsync; }
+ void setAllowRunningAsync(bool mayRunAsync) { mMayRunAsync = mayRunAsync; }
bool mayRunAsync() { return mMayRunAsync; }
- ANDROID_API void start();
- ANDROID_API virtual void reset();
- ANDROID_API void reverse();
+ void start();
+ virtual void reset();
+ void reverse();
// Terminates the animation at its current progress.
- ANDROID_API void cancel();
+ void cancel();
// Terminates the animation and skip to the end of the animation.
- ANDROID_API virtual void end();
+ virtual void end();
void attach(RenderNode* target);
virtual void onAttached() {}
void detach() { mTarget = nullptr; }
- ANDROID_API void pushStaging(AnimationContext& context);
- ANDROID_API bool animate(AnimationContext& context);
+ void pushStaging(AnimationContext& context);
+ bool animate(AnimationContext& context);
// Returns the remaining time in ms for the animation. Note this should only be called during
// an animation on RenderThread.
- ANDROID_API nsecs_t getRemainingPlayTime();
+ nsecs_t getRemainingPlayTime();
bool isRunning() {
return mPlayState == PlayState::Running || mPlayState == PlayState::Reversing;
@@ -90,7 +90,7 @@ public:
bool isFinished() { return mPlayState == PlayState::Finished; }
float finalValue() { return mFinalValue; }
- ANDROID_API virtual uint32_t dirtyMask() = 0;
+ virtual uint32_t dirtyMask() = 0;
void forceEndNow(AnimationContext& context);
RenderNode* target() { return mTarget; }
@@ -196,9 +196,9 @@ public:
ALPHA,
};
- ANDROID_API RenderPropertyAnimator(RenderProperty property, float finalValue);
+ RenderPropertyAnimator(RenderProperty property, float finalValue);
- ANDROID_API virtual uint32_t dirtyMask();
+ virtual uint32_t dirtyMask();
protected:
virtual float getValue(RenderNode* target) const override;
@@ -221,10 +221,10 @@ private:
class CanvasPropertyPrimitiveAnimator : public BaseRenderNodeAnimator {
public:
- ANDROID_API CanvasPropertyPrimitiveAnimator(CanvasPropertyPrimitive* property,
+ CanvasPropertyPrimitiveAnimator(CanvasPropertyPrimitive* property,
float finalValue);
- ANDROID_API virtual uint32_t dirtyMask();
+ virtual uint32_t dirtyMask();
protected:
virtual float getValue(RenderNode* target) const override;
@@ -241,10 +241,10 @@ public:
ALPHA,
};
- ANDROID_API CanvasPropertyPaintAnimator(CanvasPropertyPaint* property, PaintField field,
+ CanvasPropertyPaintAnimator(CanvasPropertyPaint* property, PaintField field,
float finalValue);
- ANDROID_API virtual uint32_t dirtyMask();
+ virtual uint32_t dirtyMask();
protected:
virtual float getValue(RenderNode* target) const override;
@@ -257,9 +257,9 @@ private:
class RevealAnimator : public BaseRenderNodeAnimator {
public:
- ANDROID_API RevealAnimator(int centerX, int centerY, float startValue, float finalValue);
+ RevealAnimator(int centerX, int centerY, float startValue, float finalValue);
- ANDROID_API virtual uint32_t dirtyMask();
+ virtual uint32_t dirtyMask();
protected:
virtual float getValue(RenderNode* target) const override;
diff --git a/libs/hwui/AnimatorManager.h b/libs/hwui/AnimatorManager.h
index 9575391a8b3f..a0df01d5962c 100644
--- a/libs/hwui/AnimatorManager.h
+++ b/libs/hwui/AnimatorManager.h
@@ -54,7 +54,7 @@ public:
void animateNoDamage(TreeInfo& info);
// Hard-ends all animators. May only be called on the UI thread.
- ANDROID_API void endAllStagingAnimators();
+ void endAllStagingAnimators();
// Hard-ends all animators that have been pushed. Used for cleanup if
// the ActivityContext is being destroyed
diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp
index 72747e8fa543..33264d5d5c86 100644
--- a/libs/hwui/AutoBackendTextureRelease.cpp
+++ b/libs/hwui/AutoBackendTextureRelease.cpp
@@ -25,7 +25,8 @@ using namespace android::uirenderer::renderthread;
namespace android {
namespace uirenderer {
-AutoBackendTextureRelease::AutoBackendTextureRelease(GrContext* context, AHardwareBuffer* buffer) {
+AutoBackendTextureRelease::AutoBackendTextureRelease(GrDirectContext* context,
+ AHardwareBuffer* buffer) {
AHardwareBuffer_Desc desc;
AHardwareBuffer_describe(buffer, &desc);
bool createProtectedImage = 0 != (desc.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT);
@@ -67,8 +68,9 @@ static void releaseProc(SkImage::ReleaseContext releaseContext) {
textureRelease->unref(false);
}
-void AutoBackendTextureRelease::makeImage(AHardwareBuffer* buffer, android_dataspace dataspace,
- GrContext* context) {
+void AutoBackendTextureRelease::makeImage(AHardwareBuffer* buffer,
+ android_dataspace dataspace,
+ GrDirectContext* context) {
AHardwareBuffer_Desc desc;
AHardwareBuffer_describe(buffer, &desc);
SkColorType colorType = GrAHardwareBufferUtils::GetSkColorTypeFromBufferFormat(desc.format);
@@ -81,7 +83,7 @@ void AutoBackendTextureRelease::makeImage(AHardwareBuffer* buffer, android_datas
}
}
-void AutoBackendTextureRelease::newBufferContent(GrContext* context) {
+void AutoBackendTextureRelease::newBufferContent(GrDirectContext* context) {
if (mBackendTexture.isValid()) {
mUpdateProc(mImageCtx, context);
}
diff --git a/libs/hwui/AutoBackendTextureRelease.h b/libs/hwui/AutoBackendTextureRelease.h
index acdd63cb7921..06f51fcd1105 100644
--- a/libs/hwui/AutoBackendTextureRelease.h
+++ b/libs/hwui/AutoBackendTextureRelease.h
@@ -31,7 +31,8 @@ namespace uirenderer {
*/
class AutoBackendTextureRelease final {
public:
- AutoBackendTextureRelease(GrContext* context, AHardwareBuffer* buffer);
+ AutoBackendTextureRelease(GrDirectContext* context,
+ AHardwareBuffer* buffer);
const GrBackendTexture& getTexture() const { return mBackendTexture; }
@@ -42,9 +43,11 @@ public:
inline sk_sp<SkImage> getImage() const { return mImage; }
- void makeImage(AHardwareBuffer* buffer, android_dataspace dataspace, GrContext* context);
+ void makeImage(AHardwareBuffer* buffer,
+ android_dataspace dataspace,
+ GrDirectContext* context);
- void newBufferContent(GrContext* context);
+ void newBufferContent(GrDirectContext* context);
private:
// The only way to invoke dtor is with unref, when mUsageCount is 0.
diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp
index 8c37d73366c2..9d03ce5252a3 100644
--- a/libs/hwui/CanvasTransform.cpp
+++ b/libs/hwui/CanvasTransform.cpp
@@ -22,7 +22,6 @@
#include <SkGradientShader.h>
#include <SkPaint.h>
#include <SkShader.h>
-#include <ui/ColorSpace.h>
#include <algorithm>
#include <cmath>
diff --git a/libs/hwui/ColorMode.h b/libs/hwui/ColorMode.h
new file mode 100644
index 000000000000..6d387f9ef43d
--- /dev/null
+++ b/libs/hwui/ColorMode.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+namespace android::uirenderer {
+
+// Must match the constants in ActivityInfo.java
+enum class ColorMode {
+ // SRGB means HWUI will produce buffer in SRGB color space.
+ Default = 0,
+ // WideColorGamut selects the most optimal colorspace & format for the device's display
+ // Most commonly DisplayP3 + RGBA_8888 currently.
+ WideColorGamut = 1,
+ // HDR Rec2020 + F16
+ Hdr = 2,
+ // HDR Rec2020 + 1010102
+ Hdr10 = 3,
+};
+
+} // namespace android::uirenderer
diff --git a/libs/hwui/DamageAccumulator.h b/libs/hwui/DamageAccumulator.h
index 030a20f31c42..2faa9d012d66 100644
--- a/libs/hwui/DamageAccumulator.h
+++ b/libs/hwui/DamageAccumulator.h
@@ -58,7 +58,7 @@ public:
// Returns the current dirty area, *NOT* transformed by pushed transforms
void peekAtDirty(SkRect* dest) const;
- ANDROID_API void computeCurrentTransform(Matrix4* outMatrix) const;
+ void computeCurrentTransform(Matrix4* outMatrix) const;
void finish(SkRect* totalDirty);
diff --git a/libs/hwui/DeferredLayerUpdater.cpp b/libs/hwui/DeferredLayerUpdater.cpp
index 67d8c07e61de..6589dbd50cf7 100644
--- a/libs/hwui/DeferredLayerUpdater.cpp
+++ b/libs/hwui/DeferredLayerUpdater.cpp
@@ -189,7 +189,7 @@ void DeferredLayerUpdater::detachSurfaceTexture() {
sk_sp<SkImage> DeferredLayerUpdater::ImageSlot::createIfNeeded(AHardwareBuffer* buffer,
android_dataspace dataspace,
bool forceCreate,
- GrContext* context) {
+ GrDirectContext* context) {
if (!mTextureRelease || !mTextureRelease->getImage().get() || dataspace != mDataspace ||
forceCreate || mBuffer != buffer) {
if (buffer != mBuffer) {
diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h
index c44c0d537fa7..6731e9c428d6 100644
--- a/libs/hwui/DeferredLayerUpdater.h
+++ b/libs/hwui/DeferredLayerUpdater.h
@@ -44,11 +44,11 @@ class DeferredLayerUpdater : public VirtualLightRefBase, public IGpuContextCallb
public:
// Note that DeferredLayerUpdater assumes it is taking ownership of the layer
// and will not call incrementRef on it as a result.
- ANDROID_API explicit DeferredLayerUpdater(RenderState& renderState);
+ explicit DeferredLayerUpdater(RenderState& renderState);
- ANDROID_API ~DeferredLayerUpdater();
+ ~DeferredLayerUpdater();
- ANDROID_API bool setSize(int width, int height) {
+ bool setSize(int width, int height) {
if (mWidth != width || mHeight != height) {
mWidth = width;
mHeight = height;
@@ -60,7 +60,7 @@ public:
int getWidth() { return mWidth; }
int getHeight() { return mHeight; }
- ANDROID_API bool setBlend(bool blend) {
+ bool setBlend(bool blend) {
if (blend != mBlend) {
mBlend = blend;
return true;
@@ -68,18 +68,18 @@ public:
return false;
}
- ANDROID_API void setSurfaceTexture(AutoTextureRelease&& consumer);
+ void setSurfaceTexture(AutoTextureRelease&& consumer);
- ANDROID_API void updateTexImage() { mUpdateTexImage = true; }
+ void updateTexImage() { mUpdateTexImage = true; }
- ANDROID_API void setTransform(const SkMatrix* matrix) {
+ void setTransform(const SkMatrix* matrix) {
delete mTransform;
mTransform = matrix ? new SkMatrix(*matrix) : nullptr;
}
SkMatrix* getTransform() { return mTransform; }
- ANDROID_API void setPaint(const SkPaint* paint);
+ void setPaint(const SkPaint* paint);
void apply();
@@ -106,7 +106,7 @@ private:
~ImageSlot() { clear(); }
sk_sp<SkImage> createIfNeeded(AHardwareBuffer* buffer, android_dataspace dataspace,
- bool forceCreate, GrContext* context);
+ bool forceCreate, GrDirectContext* context);
private:
void clear();
diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp
index c24224cbbd67..07594715a84c 100644
--- a/libs/hwui/DeviceInfo.cpp
+++ b/libs/hwui/DeviceInfo.cpp
@@ -15,6 +15,8 @@
*/
#include <DeviceInfo.h>
+#include <android/hardware_buffer.h>
+#include <apex/display.h>
#include <log/log.h>
#include <utils/Errors.h>
@@ -30,14 +32,47 @@ DeviceInfo* DeviceInfo::get() {
DeviceInfo::DeviceInfo() {
#if HWUI_NULL_GPU
- mMaxTextureSize = NULL_GPU_MAX_TEXTURE_SIZE;
+ mMaxTextureSize = NULL_GPU_MAX_TEXTURE_SIZE;
#else
- mMaxTextureSize = -1;
+ mMaxTextureSize = -1;
#endif
- updateDisplayInfo();
}
-DeviceInfo::~DeviceInfo() {
- ADisplay_release(mDisplays);
+
+void DeviceInfo::updateDisplayInfo() {
+ if (Properties::isolatedProcess) {
+ return;
+ }
+
+ ADisplay** displays;
+ int size = ADisplay_acquirePhysicalDisplays(&displays);
+
+ if (size <= 0) {
+ LOG_ALWAYS_FATAL("Failed to acquire physical displays for WCG support!");
+ }
+
+ for (int i = 0; i < size; ++i) {
+ // Pick the first internal display for querying the display type
+ // In practice this is controlled by a sysprop so it doesn't really
+ // matter which display we use.
+ if (ADisplay_getDisplayType(displays[i]) == DISPLAY_TYPE_INTERNAL) {
+ // We get the dataspace from DisplayManager already. Allocate space
+ // for the result here but we don't actually care about using it.
+ ADataSpace dataspace;
+ AHardwareBuffer_Format pixelFormat;
+ ADisplay_getPreferredWideColorFormat(displays[i], &dataspace, &pixelFormat);
+
+ if (pixelFormat == AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM) {
+ mWideColorType = SkColorType::kN32_SkColorType;
+ } else if (pixelFormat == AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT) {
+ mWideColorType = SkColorType::kRGBA_F16_SkColorType;
+ } else {
+ LOG_ALWAYS_FATAL("Unreachable: unsupported pixel format: %d", pixelFormat);
+ }
+ ADisplay_release(displays);
+ return;
+ }
+ }
+ LOG_ALWAYS_FATAL("Failed to find a valid physical display for WCG support!");
}
int DeviceInfo::maxTextureSize() const {
@@ -49,75 +84,29 @@ void DeviceInfo::setMaxTextureSize(int maxTextureSize) {
DeviceInfo::get()->mMaxTextureSize = maxTextureSize;
}
+void DeviceInfo::setWideColorDataspace(ADataSpace dataspace) {
+ switch (dataspace) {
+ case ADATASPACE_DISPLAY_P3:
+ get()->mWideColorSpace =
+ SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDisplayP3);
+ break;
+ case ADATASPACE_SCRGB:
+ get()->mWideColorSpace = SkColorSpace::MakeSRGB();
+ break;
+ case ADATASPACE_SRGB:
+ // when sRGB is returned, it means wide color gamut is not supported.
+ get()->mWideColorSpace = SkColorSpace::MakeSRGB();
+ break;
+ default:
+ LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space.");
+ }
+}
+
void DeviceInfo::onRefreshRateChanged(int64_t vsyncPeriod) {
mVsyncPeriod = vsyncPeriod;
}
-void DeviceInfo::updateDisplayInfo() {
- if (Properties::isolatedProcess) {
- return;
- }
-
- if (mCurrentConfig == nullptr) {
- mDisplaysSize = ADisplay_acquirePhysicalDisplays(&mDisplays);
- LOG_ALWAYS_FATAL_IF(mDisplays == nullptr || mDisplaysSize <= 0,
- "Failed to get physical displays: no connected display: %d!", mDisplaysSize);
- for (size_t i = 0; i < mDisplaysSize; i++) {
- ADisplayType type = ADisplay_getDisplayType(mDisplays[i]);
- if (type == ADisplayType::DISPLAY_TYPE_INTERNAL) {
- mPhysicalDisplayIndex = i;
- break;
- }
- }
- LOG_ALWAYS_FATAL_IF(mPhysicalDisplayIndex < 0, "Failed to find a connected physical display!");
-
-
- // Since we now just got the primary display for the first time, then
- // store the primary display metadata here.
- ADisplay* primaryDisplay = mDisplays[mPhysicalDisplayIndex];
- mMaxRefreshRate = ADisplay_getMaxSupportedFps(primaryDisplay);
- ADataSpace dataspace;
- AHardwareBuffer_Format format;
- ADisplay_getPreferredWideColorFormat(primaryDisplay, &dataspace, &format);
- switch (dataspace) {
- case ADATASPACE_DISPLAY_P3:
- mWideColorSpace =
- SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDCIP3);
- break;
- case ADATASPACE_SCRGB:
- mWideColorSpace = SkColorSpace::MakeSRGB();
- break;
- case ADATASPACE_SRGB:
- // when sRGB is returned, it means wide color gamut is not supported.
- mWideColorSpace = SkColorSpace::MakeSRGB();
- break;
- default:
- LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space.");
- }
- switch (format) {
- case AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM:
- mWideColorType = SkColorType::kN32_SkColorType;
- break;
- case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT:
- mWideColorType = SkColorType::kRGBA_F16_SkColorType;
- break;
- default:
- LOG_ALWAYS_FATAL("Unreachable: unsupported pixel format.");
- }
- }
- // This method may have been called when the display config changed, so
- // sync with the current configuration.
- ADisplay* primaryDisplay = mDisplays[mPhysicalDisplayIndex];
- status_t status = ADisplay_getCurrentConfig(primaryDisplay, &mCurrentConfig);
- LOG_ALWAYS_FATAL_IF(status, "Failed to get display config, error %d", status);
-
- mWidth = ADisplayConfig_getWidth(mCurrentConfig);
- mHeight = ADisplayConfig_getHeight(mCurrentConfig);
- mDensity = ADisplayConfig_getDensity(mCurrentConfig);
- mVsyncPeriod = static_cast<int64_t>(1000000000 / ADisplayConfig_getFps(mCurrentConfig));
- mCompositorOffset = ADisplayConfig_getCompositorOffsetNanos(mCurrentConfig);
- mAppOffset = ADisplayConfig_getAppVsyncOffsetNanos(mCurrentConfig);
-}
+std::atomic<float> DeviceInfo::sDensity = 2.0;
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h
index 16a22f4706f5..27be62269959 100644
--- a/libs/hwui/DeviceInfo.h
+++ b/libs/hwui/DeviceInfo.h
@@ -16,8 +16,10 @@
#ifndef DEVICEINFO_H
#define DEVICEINFO_H
-#include <apex/display.h>
#include <SkImageInfo.h>
+#include <android/data_space.h>
+
+#include <mutex>
#include "utils/Macros.h"
@@ -36,16 +38,37 @@ public:
static float getMaxRefreshRate() { return get()->mMaxRefreshRate; }
static int32_t getWidth() { return get()->mWidth; }
static int32_t getHeight() { return get()->mHeight; }
- static float getDensity() { return get()->mDensity; }
+ // Gets the density in density-independent pixels
+ static float getDensity() { return sDensity.load(); }
static int64_t getVsyncPeriod() { return get()->mVsyncPeriod; }
- static int64_t getCompositorOffset() { return get()->mCompositorOffset; }
- static int64_t getAppOffset() { return get()->mAppOffset; }
+ static int64_t getCompositorOffset() { return get()->getCompositorOffsetInternal(); }
+ static int64_t getAppOffset() { return get()->mAppVsyncOffsetNanos; }
+ // Sets the density in density-independent pixels
+ static void setDensity(float density) { sDensity.store(density); }
+ static void setMaxRefreshRate(float refreshRate) { get()->mMaxRefreshRate = refreshRate; }
+ static void setWidth(int32_t width) { get()->mWidth = width; }
+ static void setHeight(int32_t height) { get()->mHeight = height; }
+ static void setRefreshRate(float refreshRate) {
+ get()->mVsyncPeriod = static_cast<int64_t>(1000000000 / refreshRate);
+ }
+ static void setPresentationDeadlineNanos(int64_t deadlineNanos) {
+ get()->mPresentationDeadlineNanos = deadlineNanos;
+ }
+ static void setAppVsyncOffsetNanos(int64_t offsetNanos) {
+ get()->mAppVsyncOffsetNanos = offsetNanos;
+ }
+ static void setWideColorDataspace(ADataSpace dataspace);
// this value is only valid after the GPU has been initialized and there is a valid graphics
// context or if you are using the HWUI_NULL_GPU
int maxTextureSize() const;
sk_sp<SkColorSpace> getWideColorSpace() const { return mWideColorSpace; }
- SkColorType getWideColorType() const { return mWideColorType; }
+ SkColorType getWideColorType() {
+ static std::once_flag kFlag;
+ // lazily update display info from SF here, so that the call is performed by RenderThread.
+ std::call_once(kFlag, [&, this]() { updateDisplayInfo(); });
+ return mWideColorType;
+ }
// This method should be called whenever the display refresh rate changes.
void onRefreshRateChanged(int64_t vsyncPeriod);
@@ -54,24 +77,32 @@ private:
friend class renderthread::RenderThread;
static void setMaxTextureSize(int maxTextureSize);
void updateDisplayInfo();
+ int64_t getCompositorOffsetInternal() const {
+ // Assume that SF takes around a millisecond to latch buffers after
+ // waking up
+ return mVsyncPeriod - (mPresentationDeadlineNanos - 1000000);
+ }
DeviceInfo();
- ~DeviceInfo();
+ ~DeviceInfo() = default;
int mMaxTextureSize;
sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB();
SkColorType mWideColorType = SkColorType::kN32_SkColorType;
- ADisplayConfig* mCurrentConfig = nullptr;
- ADisplay** mDisplays = nullptr;
int mDisplaysSize = 0;
int mPhysicalDisplayIndex = -1;
float mMaxRefreshRate = 60.0;
int32_t mWidth = 1080;
int32_t mHeight = 1920;
- float mDensity = 2.0;
int64_t mVsyncPeriod = 16666666;
- int64_t mCompositorOffset = 0;
- int64_t mAppOffset = 0;
+ // Magically corresponds with an sf offset of 0 for a sane default.
+ int64_t mPresentationDeadlineNanos = 17666666;
+ int64_t mAppVsyncOffsetNanos = 0;
+
+ // Density is not retrieved from the ADisplay apis, so this may potentially
+ // be called on multiple threads.
+ // Unit is density-independent pixels
+ static std::atomic<float> sDensity;
};
} /* namespace uirenderer */
diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp
index 0698775b0021..fd18d2f9192d 100644
--- a/libs/hwui/FrameInfo.cpp
+++ b/libs/hwui/FrameInfo.cpp
@@ -22,6 +22,7 @@ namespace uirenderer {
const std::string FrameInfoNames[] = {
"Flags",
+ "FrameTimelineVsyncId",
"IntendedVsync",
"Vsync",
"OldestInputEvent",
@@ -30,6 +31,7 @@ const std::string FrameInfoNames[] = {
"AnimationStart",
"PerformTraversalsStart",
"DrawStart",
+ "FrameDeadline",
"SyncQueued",
"SyncStart",
"IssueDrawCommandsStart",
@@ -44,7 +46,7 @@ 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) == 17,
+static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 19,
"Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)");
void FrameInfo::importUiThreadInfo(int64_t* info) {
diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h
index 51674fbd557e..bb875e35f6f7 100644
--- a/libs/hwui/FrameInfo.h
+++ b/libs/hwui/FrameInfo.h
@@ -27,10 +27,11 @@
namespace android {
namespace uirenderer {
-#define UI_THREAD_FRAME_INFO_SIZE 9
+#define UI_THREAD_FRAME_INFO_SIZE 11
enum class FrameInfoIndex {
Flags = 0,
+ FrameTimelineVsyncId,
IntendedVsync,
Vsync,
OldestInputEvent,
@@ -39,6 +40,7 @@ enum class FrameInfoIndex {
AnimationStart,
PerformTraversalsStart,
DrawStart,
+ FrameDeadline,
// End of UI frame info
SyncQueued,
@@ -69,13 +71,19 @@ enum {
};
};
-class ANDROID_API UiFrameInfoBuilder {
+class UiFrameInfoBuilder {
public:
+ static constexpr int64_t INVALID_VSYNC_ID = -1;
+
explicit UiFrameInfoBuilder(int64_t* buffer) : mBuffer(buffer) {
memset(mBuffer, 0, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t));
+ set(FrameInfoIndex::FrameTimelineVsyncId) = INVALID_VSYNC_ID;
+ set(FrameInfoIndex::FrameDeadline) = std::numeric_limits<int64_t>::max();
}
- UiFrameInfoBuilder& setVsync(nsecs_t vsyncTime, nsecs_t intendedVsync) {
+ UiFrameInfoBuilder& setVsync(nsecs_t vsyncTime, nsecs_t intendedVsync,
+ int64_t vsyncId, int64_t frameDeadline) {
+ set(FrameInfoIndex::FrameTimelineVsyncId) = vsyncId;
set(FrameInfoIndex::Vsync) = vsyncTime;
set(FrameInfoIndex::IntendedVsync) = intendedVsync;
// Pretend the other fields are all at vsync, too, so that naive
@@ -84,6 +92,7 @@ public:
set(FrameInfoIndex::AnimationStart) = vsyncTime;
set(FrameInfoIndex::PerformTraversalsStart) = vsyncTime;
set(FrameInfoIndex::DrawStart) = vsyncTime;
+ set(FrameInfoIndex::FrameDeadline) = frameDeadline;
return *this;
}
@@ -149,7 +158,7 @@ public:
// GPU start time is approximated to the moment before swapBuffer is invoked.
// We could add an EGLSyncKHR fence at the beginning of the frame, but that is an overhead.
int64_t endTime = get(FrameInfoIndex::GpuCompleted);
- return endTime > 0 ? endTime - get(FrameInfoIndex::SwapBuffers) : -1;
+ return endTime > 0 ? endTime - get(FrameInfoIndex::SwapBuffers) : 0;
}
inline int64_t& set(FrameInfoIndex index) { return mFrameInfo[static_cast<int>(index)]; }
diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp
index a3d552faeb0a..ab9b8b55a4cb 100644
--- a/libs/hwui/HardwareBitmapUploader.cpp
+++ b/libs/hwui/HardwareBitmapUploader.cpp
@@ -16,24 +16,27 @@
#include "HardwareBitmapUploader.h"
-#include "hwui/Bitmap.h"
-#include "renderthread/EglManager.h"
-#include "renderthread/VulkanManager.h"
-#include "thread/ThreadBase.h"
-#include "utils/TimeUtils.h"
-
+#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <GLES3/gl3.h>
-#include <GrContext.h>
+#include <GrDirectContext.h>
#include <SkCanvas.h>
#include <SkImage.h>
#include <utils/GLUtils.h>
+#include <utils/NdkUtils.h>
#include <utils/Trace.h>
#include <utils/TraceUtils.h>
+
#include <thread>
+#include "hwui/Bitmap.h"
+#include "renderthread/EglManager.h"
+#include "renderthread/VulkanManager.h"
+#include "thread/ThreadBase.h"
+#include "utils/TimeUtils.h"
+
namespace android::uirenderer {
class AHBUploader;
@@ -42,7 +45,7 @@ class AHBUploader;
static sp<AHBUploader> sUploader = nullptr;
struct FormatInfo {
- PixelFormat pixelFormat;
+ AHardwareBuffer_Format bufferFormat;
GLint format, type;
VkFormat vkFormat;
bool isSupported = false;
@@ -53,12 +56,6 @@ class AHBUploader : public RefBase {
public:
virtual ~AHBUploader() {}
- // Called to start creation of the Vulkan and EGL contexts on another thread before we actually
- // need to do an upload.
- void initialize() {
- onInitialize();
- }
-
void destroy() {
std::lock_guard _lock{mLock};
LOG_ALWAYS_FATAL_IF(mPendingUploads, "terminate called while uploads in progress");
@@ -71,10 +68,10 @@ public:
}
bool uploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format,
- sp<GraphicBuffer> graphicBuffer) {
+ AHardwareBuffer* ahb) {
ATRACE_CALL();
beginUpload();
- bool result = onUploadHardwareBitmap(bitmap, format, graphicBuffer);
+ bool result = onUploadHardwareBitmap(bitmap, format, ahb);
endUpload();
return result;
}
@@ -88,12 +85,11 @@ protected:
sp<ThreadBase> mUploadThread = nullptr;
private:
- virtual void onInitialize() = 0;
virtual void onIdle() = 0;
virtual void onDestroy() = 0;
virtual bool onUploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format,
- sp<GraphicBuffer> graphicBuffer) = 0;
+ AHardwareBuffer* ahb) = 0;
virtual void onBeginUpload() = 0;
bool shouldTimeOutLocked() {
@@ -138,7 +134,6 @@ private:
class EGLUploader : public AHBUploader {
private:
- void onInitialize() override {}
void onDestroy() override {
mEglManager.destroy();
}
@@ -165,16 +160,16 @@ private:
}
bool onUploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format,
- sp<GraphicBuffer> graphicBuffer) override {
+ AHardwareBuffer* ahb) override {
ATRACE_CALL();
EGLDisplay display = getUploadEglDisplay();
LOG_ALWAYS_FATAL_IF(display == EGL_NO_DISPLAY, "Failed to get EGL_DEFAULT_DISPLAY! err=%s",
uirenderer::renderthread::EglManager::eglErrorString());
- // We use an EGLImage to access the content of the GraphicBuffer
+ // We use an EGLImage to access the content of the buffer
// The EGL image is later bound to a 2D texture
- EGLClientBuffer clientBuffer = (EGLClientBuffer)graphicBuffer->getNativeBuffer();
+ const EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(ahb);
AutoEglImage autoImage(display, clientBuffer);
if (autoImage.image == EGL_NO_IMAGE_KHR) {
ALOGW("Could not create EGL image, err =%s",
@@ -228,62 +223,67 @@ private:
class VkUploader : public AHBUploader {
private:
- void onInitialize() override {
- std::lock_guard _lock{mLock};
- if (!mUploadThread) {
- mUploadThread = new ThreadBase{};
- }
- if (!mUploadThread->isRunning()) {
- mUploadThread->start("GrallocUploadThread");
- }
-
- mUploadThread->queue().post([this]() {
- std::lock_guard _lock{mVkLock};
- if (!mVulkanManager.hasVkContext()) {
- mVulkanManager.initialize();
- }
- });
- }
void onDestroy() override {
+ std::lock_guard _lock{mVkLock};
mGrContext.reset();
- mVulkanManager.destroy();
+ mVulkanManagerStrong.clear();
}
void onIdle() override {
- mGrContext.reset();
+ onDestroy();
}
- void onBeginUpload() override {
- {
- std::lock_guard _lock{mVkLock};
- if (!mVulkanManager.hasVkContext()) {
- LOG_ALWAYS_FATAL_IF(mGrContext,
- "GrContext exists with no VulkanManager for vulkan uploads");
- mUploadThread->queue().runSync([this]() {
- mVulkanManager.initialize();
- });
- }
- }
- if (!mGrContext) {
- GrContextOptions options;
- mGrContext = mVulkanManager.createContext(options);
- LOG_ALWAYS_FATAL_IF(!mGrContext, "failed to create GrContext for vulkan uploads");
- this->postIdleTimeoutCheck();
- }
- }
+ void onBeginUpload() override {}
bool onUploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format,
- sp<GraphicBuffer> graphicBuffer) override {
- ATRACE_CALL();
+ AHardwareBuffer* ahb) override {
+ bool uploadSucceeded = false;
+ mUploadThread->queue().runSync([this, &uploadSucceeded, bitmap, ahb]() {
+ ATRACE_CALL();
+ std::lock_guard _lock{mVkLock};
+
+ renderthread::VulkanManager* vkManager = getVulkanManager();
+ if (!vkManager->hasVkContext()) {
+ LOG_ALWAYS_FATAL_IF(mGrContext,
+ "GrContext exists with no VulkanManager for vulkan uploads");
+ vkManager->initialize();
+ }
+
+ if (!mGrContext) {
+ GrContextOptions options;
+ mGrContext = vkManager->createContext(options,
+ renderthread::VulkanManager::ContextType::kUploadThread);
+ LOG_ALWAYS_FATAL_IF(!mGrContext, "failed to create GrContext for vulkan uploads");
+ this->postIdleTimeoutCheck();
+ }
+
+ sk_sp<SkImage> image =
+ SkImage::MakeFromAHardwareBufferWithData(mGrContext.get(), bitmap.pixmap(), ahb);
+ mGrContext->submit(true);
+
+ uploadSucceeded = (image.get() != nullptr);
+ });
+ return uploadSucceeded;
+ }
- std::lock_guard _lock{mLock};
+ /* must be called on the upload thread after the vkLock has been acquired */
+ renderthread::VulkanManager* getVulkanManager() {
+ if (!mVulkanManagerStrong) {
+ mVulkanManagerStrong = mVulkanManagerWeak.promote();
- sk_sp<SkImage> image = SkImage::MakeFromAHardwareBufferWithData(mGrContext.get(),
- bitmap.pixmap(), reinterpret_cast<AHardwareBuffer*>(graphicBuffer.get()));
- return (image.get() != nullptr);
+ // create a new manager if we couldn't promote the weak ref
+ if (!mVulkanManagerStrong) {
+ mVulkanManagerStrong = renderthread::VulkanManager::getInstance();
+ mGrContext.reset();
+ mVulkanManagerWeak = mVulkanManagerStrong;
+ }
+ }
+
+ return mVulkanManagerStrong.get();
}
- sk_sp<GrContext> mGrContext;
- renderthread::VulkanManager mVulkanManager;
+ sk_sp<GrDirectContext> mGrContext;
+ sp<renderthread::VulkanManager> mVulkanManagerStrong;
+ wp<renderthread::VulkanManager> mVulkanManagerWeak;
std::mutex mVkLock;
};
@@ -294,13 +294,17 @@ bool HardwareBitmapUploader::hasFP16Support() {
// Gralloc shouldn't let us create a USAGE_HW_TEXTURE if GLES is unable to consume it, so
// we don't need to double-check the GLES version/extension.
std::call_once(sOnce, []() {
- sp<GraphicBuffer> buffer = new GraphicBuffer(1, 1, PIXEL_FORMAT_RGBA_FP16,
- GraphicBuffer::USAGE_HW_TEXTURE |
- GraphicBuffer::USAGE_SW_WRITE_NEVER |
- GraphicBuffer::USAGE_SW_READ_NEVER,
- "tempFp16Buffer");
- status_t error = buffer->initCheck();
- hasFP16Support = !error;
+ AHardwareBuffer_Desc desc = {
+ .width = 1,
+ .height = 1,
+ .layers = 1,
+ .format = AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT,
+ .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER |
+ AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER |
+ AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE,
+ };
+ UniqueAHardwareBuffer buffer = allocateAHardwareBuffer(desc);
+ hasFP16Support = buffer != nullptr;
});
return hasFP16Support;
@@ -314,7 +318,7 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) {
[[fallthrough]];
// ARGB_4444 is upconverted to RGBA_8888
case kARGB_4444_SkColorType:
- formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_8888;
+ formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
formatInfo.format = GL_RGBA;
formatInfo.type = GL_UNSIGNED_BYTE;
formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM;
@@ -323,25 +327,25 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) {
formatInfo.isSupported = HardwareBitmapUploader::hasFP16Support();
if (formatInfo.isSupported) {
formatInfo.type = GL_HALF_FLOAT;
- formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_FP16;
+ formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT;
formatInfo.vkFormat = VK_FORMAT_R16G16B16A16_SFLOAT;
} else {
formatInfo.type = GL_UNSIGNED_BYTE;
- formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_8888;
+ formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM;
}
formatInfo.format = GL_RGBA;
break;
case kRGB_565_SkColorType:
formatInfo.isSupported = true;
- formatInfo.pixelFormat = PIXEL_FORMAT_RGB_565;
+ formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM;
formatInfo.format = GL_RGB;
formatInfo.type = GL_UNSIGNED_SHORT_5_6_5;
formatInfo.vkFormat = VK_FORMAT_R5G6B5_UNORM_PACK16;
break;
case kGray_8_SkColorType:
formatInfo.isSupported = usingGL;
- formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_8888;
+ formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
formatInfo.format = GL_LUMINANCE;
formatInfo.type = GL_UNSIGNED_BYTE;
formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM;
@@ -394,35 +398,33 @@ sk_sp<Bitmap> HardwareBitmapUploader::allocateHardwareBitmap(const SkBitmap& sou
}
SkBitmap bitmap = makeHwCompatible(format, sourceBitmap);
- sp<GraphicBuffer> buffer = new GraphicBuffer(
- static_cast<uint32_t>(bitmap.width()), static_cast<uint32_t>(bitmap.height()),
- format.pixelFormat,
- GraphicBuffer::USAGE_HW_TEXTURE | GraphicBuffer::USAGE_SW_WRITE_NEVER |
- GraphicBuffer::USAGE_SW_READ_NEVER,
- std::string("Bitmap::allocateHardwareBitmap pid [") + std::to_string(getpid()) +
- "]");
-
- status_t error = buffer->initCheck();
- if (error < 0) {
- ALOGW("createGraphicBuffer() failed in GraphicBuffer.create()");
+ AHardwareBuffer_Desc desc = {
+ .width = static_cast<uint32_t>(bitmap.width()),
+ .height = static_cast<uint32_t>(bitmap.height()),
+ .layers = 1,
+ .format = format.bufferFormat,
+ .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER |
+ AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE,
+ };
+ UniqueAHardwareBuffer ahb = allocateAHardwareBuffer(desc);
+ if (!ahb) {
+ ALOGW("allocateHardwareBitmap() failed in AHardwareBuffer_allocate()");
return nullptr;
- }
+ };
createUploader(usingGL);
- if (!sUploader->uploadHardwareBitmap(bitmap, format, buffer)) {
+ if (!sUploader->uploadHardwareBitmap(bitmap, format, ahb.get())) {
return nullptr;
}
- return Bitmap::createFrom(buffer->toAHardwareBuffer(), bitmap.colorType(),
- bitmap.refColorSpace(), bitmap.alphaType(),
- Bitmap::computePalette(bitmap));
+ return Bitmap::createFrom(ahb.get(), bitmap.colorType(), bitmap.refColorSpace(),
+ bitmap.alphaType(), Bitmap::computePalette(bitmap));
}
void HardwareBitmapUploader::initialize() {
bool usingGL = uirenderer::Properties::getRenderPipelineType() ==
uirenderer::RenderPipelineType::SkiaGL;
createUploader(usingGL);
- sUploader->initialize();
}
void HardwareBitmapUploader::terminate() {
diff --git a/libs/hwui/HardwareBitmapUploader.h b/libs/hwui/HardwareBitmapUploader.h
index 72243d23dd35..ad7a95a4fa03 100644
--- a/libs/hwui/HardwareBitmapUploader.h
+++ b/libs/hwui/HardwareBitmapUploader.h
@@ -20,7 +20,7 @@
namespace android::uirenderer {
-class ANDROID_API HardwareBitmapUploader {
+class HardwareBitmapUploader {
public:
static void initialize();
static void terminate();
diff --git a/libs/hwui/Interpolator.h b/libs/hwui/Interpolator.h
index 452988fc8711..131cc3935f10 100644
--- a/libs/hwui/Interpolator.h
+++ b/libs/hwui/Interpolator.h
@@ -37,12 +37,12 @@ protected:
Interpolator() {}
};
-class ANDROID_API AccelerateDecelerateInterpolator : public Interpolator {
+class AccelerateDecelerateInterpolator : public Interpolator {
public:
virtual float interpolate(float input) override;
};
-class ANDROID_API AccelerateInterpolator : public Interpolator {
+class AccelerateInterpolator : public Interpolator {
public:
explicit AccelerateInterpolator(float factor) : mFactor(factor), mDoubleFactor(factor * 2) {}
virtual float interpolate(float input) override;
@@ -52,7 +52,7 @@ private:
const float mDoubleFactor;
};
-class ANDROID_API AnticipateInterpolator : public Interpolator {
+class AnticipateInterpolator : public Interpolator {
public:
explicit AnticipateInterpolator(float tension) : mTension(tension) {}
virtual float interpolate(float input) override;
@@ -61,7 +61,7 @@ private:
const float mTension;
};
-class ANDROID_API AnticipateOvershootInterpolator : public Interpolator {
+class AnticipateOvershootInterpolator : public Interpolator {
public:
explicit AnticipateOvershootInterpolator(float tension) : mTension(tension) {}
virtual float interpolate(float input) override;
@@ -70,12 +70,12 @@ private:
const float mTension;
};
-class ANDROID_API BounceInterpolator : public Interpolator {
+class BounceInterpolator : public Interpolator {
public:
virtual float interpolate(float input) override;
};
-class ANDROID_API CycleInterpolator : public Interpolator {
+class CycleInterpolator : public Interpolator {
public:
explicit CycleInterpolator(float cycles) : mCycles(cycles) {}
virtual float interpolate(float input) override;
@@ -84,7 +84,7 @@ private:
const float mCycles;
};
-class ANDROID_API DecelerateInterpolator : public Interpolator {
+class DecelerateInterpolator : public Interpolator {
public:
explicit DecelerateInterpolator(float factor) : mFactor(factor) {}
virtual float interpolate(float input) override;
@@ -93,12 +93,12 @@ private:
const float mFactor;
};
-class ANDROID_API LinearInterpolator : public Interpolator {
+class LinearInterpolator : public Interpolator {
public:
virtual float interpolate(float input) override { return input; }
};
-class ANDROID_API OvershootInterpolator : public Interpolator {
+class OvershootInterpolator : public Interpolator {
public:
explicit OvershootInterpolator(float tension) : mTension(tension) {}
virtual float interpolate(float input) override;
@@ -107,7 +107,7 @@ private:
const float mTension;
};
-class ANDROID_API PathInterpolator : public Interpolator {
+class PathInterpolator : public Interpolator {
public:
explicit PathInterpolator(std::vector<float>&& x, std::vector<float>&& y) : mX(x), mY(y) {}
virtual float interpolate(float input) override;
@@ -117,7 +117,7 @@ private:
std::vector<float> mY;
};
-class ANDROID_API LUTInterpolator : public Interpolator {
+class LUTInterpolator : public Interpolator {
public:
LUTInterpolator(float* values, size_t size);
~LUTInterpolator();
diff --git a/libs/hwui/Matrix.h b/libs/hwui/Matrix.h
index 0c515a41689d..4c6e1a0a6eee 100644
--- a/libs/hwui/Matrix.h
+++ b/libs/hwui/Matrix.h
@@ -44,7 +44,7 @@ namespace uirenderer {
// Classes
///////////////////////////////////////////////////////////////////////////////
-class ANDROID_API Matrix4 {
+class Matrix4 {
public:
float data[16];
diff --git a/libs/hwui/PathParser.h b/libs/hwui/PathParser.h
index 878bb7c0f137..859697eb3e9b 100644
--- a/libs/hwui/PathParser.h
+++ b/libs/hwui/PathParser.h
@@ -30,17 +30,17 @@ namespace uirenderer {
class PathParser {
public:
- struct ANDROID_API ParseResult {
+ struct ParseResult {
bool failureOccurred = false;
std::string failureMessage;
};
/**
* Parse the string literal and create a Skia Path. Return true on success.
*/
- ANDROID_API static void parseAsciiStringForSkPath(SkPath* outPath, ParseResult* result,
- const char* pathStr, size_t strLength);
- ANDROID_API static void getPathDataFromAsciiString(PathData* outData, ParseResult* result,
- const char* pathStr, size_t strLength);
+ static void parseAsciiStringForSkPath(SkPath* outPath, ParseResult* result,
+ const char* pathStr, size_t strLength);
+ static void getPathDataFromAsciiString(PathData* outData, ParseResult* result,
+ const char* pathStr, size_t strLength);
static void dump(const PathData& data);
static void validateVerbAndPoints(char verb, size_t points, ParseResult* result);
};
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 446e81e65bb8..ba44d056dda3 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -78,6 +78,7 @@ bool Properties::isolatedProcess = false;
int Properties::contextPriority = 0;
int Properties::defaultRenderAhead = -1;
+float Properties::defaultSdrWhitePoint = 200.f;
bool Properties::load() {
bool prevDebugLayersUpdates = debugLayersUpdates;
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index d3ecb54d94f6..85a0f4aa7809 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -213,10 +213,10 @@ public:
static int overrideSpotShadowStrength;
static ProfileType getProfileType();
- ANDROID_API static RenderPipelineType peekRenderPipelineType();
- ANDROID_API static RenderPipelineType getRenderPipelineType();
+ static RenderPipelineType peekRenderPipelineType();
+ static RenderPipelineType getRenderPipelineType();
- ANDROID_API static bool enableHighContrastText;
+ static bool enableHighContrastText;
// Should be used only by test apps
static bool waitForGpuCompletion;
@@ -235,20 +235,22 @@ public:
static bool skpCaptureEnabled;
// For experimentation b/68769804
- ANDROID_API static bool enableRTAnimations;
+ static bool enableRTAnimations;
// Used for testing only to change the render pipeline.
static void overrideRenderPipelineType(RenderPipelineType);
static bool runningInEmulator;
- ANDROID_API static bool debuggingEnabled;
- ANDROID_API static bool isolatedProcess;
+ static bool debuggingEnabled;
+ static bool isolatedProcess;
- ANDROID_API static int contextPriority;
+ static int contextPriority;
static int defaultRenderAhead;
+ static float defaultSdrWhitePoint;
+
private:
static ProfileType sProfileType;
static bool sDisableProfileBars;
diff --git a/libs/hwui/PropertyValuesAnimatorSet.h b/libs/hwui/PropertyValuesAnimatorSet.h
index e4214b22d1cc..c04a0b9b0fe7 100644
--- a/libs/hwui/PropertyValuesAnimatorSet.h
+++ b/libs/hwui/PropertyValuesAnimatorSet.h
@@ -44,7 +44,7 @@ private:
};
// TODO: This class should really be named VectorDrawableAnimator
-class ANDROID_API PropertyValuesAnimatorSet : public BaseRenderNodeAnimator {
+class PropertyValuesAnimatorSet : public BaseRenderNodeAnimator {
public:
friend class PropertyAnimatorSetListener;
PropertyValuesAnimatorSet();
diff --git a/libs/hwui/PropertyValuesHolder.h b/libs/hwui/PropertyValuesHolder.h
index 0a799d3c0b5c..bb26cbe7bc9b 100644
--- a/libs/hwui/PropertyValuesHolder.h
+++ b/libs/hwui/PropertyValuesHolder.h
@@ -28,7 +28,7 @@ namespace uirenderer {
* When a fraction in [0f, 1f] is provided, the holder will calculate an interpolated value based
* on its start and end value, and set the new value on the VectorDrawble's corresponding property.
*/
-class ANDROID_API PropertyValuesHolder {
+class PropertyValuesHolder {
public:
virtual void setFraction(float fraction) = 0;
virtual ~PropertyValuesHolder() {}
@@ -49,19 +49,19 @@ public:
}
};
-class ANDROID_API ColorEvaluator : public Evaluator<SkColor> {
+class ColorEvaluator : public Evaluator<SkColor> {
public:
virtual void evaluate(SkColor* outColor, const SkColor& from, const SkColor& to,
float fraction) const override;
};
-class ANDROID_API PathEvaluator : public Evaluator<PathData> {
+class PathEvaluator : public Evaluator<PathData> {
virtual void evaluate(PathData* out, const PathData& from, const PathData& to,
float fraction) const override;
};
template <typename T>
-class ANDROID_API PropertyValuesHolderImpl : public PropertyValuesHolder {
+class PropertyValuesHolderImpl : public PropertyValuesHolder {
public:
PropertyValuesHolderImpl(const T& startValue, const T& endValue)
: mStartValue(startValue), mEndValue(endValue) {}
@@ -85,7 +85,7 @@ protected:
T mEndValue;
};
-class ANDROID_API GroupPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
+class GroupPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
public:
GroupPropertyValuesHolder(VectorDrawable::Group* ptr, int propertyId, float startValue,
float endValue)
@@ -99,7 +99,7 @@ private:
int mPropertyId;
};
-class ANDROID_API FullPathColorPropertyValuesHolder : public PropertyValuesHolderImpl<SkColor> {
+class FullPathColorPropertyValuesHolder : public PropertyValuesHolderImpl<SkColor> {
public:
FullPathColorPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId,
SkColor startValue, SkColor endValue)
@@ -116,7 +116,7 @@ private:
int mPropertyId;
};
-class ANDROID_API FullPathPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
+class FullPathPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
public:
FullPathPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId, float startValue,
float endValue)
@@ -132,7 +132,7 @@ private:
int mPropertyId;
};
-class ANDROID_API PathDataPropertyValuesHolder : public PropertyValuesHolderImpl<PathData> {
+class PathDataPropertyValuesHolder : public PropertyValuesHolderImpl<PathData> {
public:
PathDataPropertyValuesHolder(VectorDrawable::Path* ptr, PathData* startValue,
PathData* endValue)
@@ -146,7 +146,7 @@ private:
PathData mPathData;
};
-class ANDROID_API RootAlphaPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
+class RootAlphaPropertyValuesHolder : public PropertyValuesHolderImpl<float> {
public:
RootAlphaPropertyValuesHolder(VectorDrawable::Tree* tree, float startValue, float endValue)
: PropertyValuesHolderImpl(startValue, endValue), mTree(tree) {
diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp
index 39900e65cb8a..b71bb07dbc86 100644
--- a/libs/hwui/Readback.cpp
+++ b/libs/hwui/Readback.cpp
@@ -18,7 +18,6 @@
#include <sync/sync.h>
#include <system/window.h>
-#include <ui/GraphicBuffer.h>
#include "DeferredLayerUpdater.h"
#include "Properties.h"
@@ -28,6 +27,7 @@
#include "renderthread/VulkanManager.h"
#include "utils/Color.h"
#include "utils/MathUtils.h"
+#include "utils/NdkUtils.h"
#include "utils/TraceUtils.h"
using namespace android::uirenderer::renderthread;
@@ -54,8 +54,7 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& srcRect,
return CopyResult::SourceEmpty;
}
- std::unique_ptr<AHardwareBuffer, decltype(&AHardwareBuffer_release)> sourceBuffer(
- rawSourceBuffer, AHardwareBuffer_release);
+ UniqueAHardwareBuffer sourceBuffer{rawSourceBuffer};
AHardwareBuffer_Desc description;
AHardwareBuffer_describe(sourceBuffer.get(), &description);
if (description.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT) {
@@ -119,7 +118,7 @@ CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, Matrix4& texTran
}
int imgWidth = image->width();
int imgHeight = image->height();
- sk_sp<GrContext> grContext = sk_ref_sp(mRenderThread.getGrContext());
+ sk_sp<GrDirectContext> grContext = sk_ref_sp(mRenderThread.getGrContext());
if (bitmap->colorType() == kRGBA_F16_SkColorType &&
!grContext->colorTypeSupportedAsSurface(bitmap->colorType())) {
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index dc467c41baed..473dc53dc4bf 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -96,7 +96,7 @@ struct Restore final : Op {
struct SaveLayer final : Op {
static const auto kType = Type::SaveLayer;
SaveLayer(const SkRect* bounds, const SkPaint* paint, const SkImageFilter* backdrop,
- const SkImage* clipMask, const SkMatrix* clipMatrix, SkCanvas::SaveLayerFlags flags) {
+ SkCanvas::SaveLayerFlags flags) {
if (bounds) {
this->bounds = *bounds;
}
@@ -104,19 +104,14 @@ struct SaveLayer final : Op {
this->paint = *paint;
}
this->backdrop = sk_ref_sp(backdrop);
- this->clipMask = sk_ref_sp(clipMask);
- this->clipMatrix = clipMatrix ? *clipMatrix : SkMatrix::I();
this->flags = flags;
}
SkRect bounds = kUnset;
SkPaint paint;
sk_sp<const SkImageFilter> backdrop;
- sk_sp<const SkImage> clipMask;
- SkMatrix clipMatrix;
SkCanvas::SaveLayerFlags flags;
void draw(SkCanvas* c, const SkMatrix&) const {
- c->saveLayer({maybe_unset(bounds), &paint, backdrop.get(), clipMask.get(),
- clipMatrix.isIdentity() ? nullptr : &clipMatrix, flags});
+ c->saveLayer({maybe_unset(bounds), &paint, backdrop.get(), flags});
}
};
struct SaveBehind final : Op {
@@ -132,9 +127,9 @@ struct SaveBehind final : Op {
struct Concat44 final : Op {
static const auto kType = Type::Concat44;
- Concat44(const SkScalar m[16]) { memcpy(colMajor, m, sizeof(colMajor)); }
- SkScalar colMajor[16];
- void draw(SkCanvas* c, const SkMatrix&) const { c->experimental_concat44(colMajor); }
+ Concat44(const SkM44& m) : matrix(m) {}
+ SkM44 matrix;
+ void draw(SkCanvas* c, const SkMatrix&) const { c->concat(matrix); }
};
struct Concat final : Op {
static const auto kType = Type::Concat;
@@ -448,14 +443,13 @@ struct DrawPoints final : Op {
};
struct DrawVertices final : Op {
static const auto kType = Type::DrawVertices;
- DrawVertices(const SkVertices* v, int bc, SkBlendMode m, const SkPaint& p)
- : vertices(sk_ref_sp(const_cast<SkVertices*>(v))), boneCount(bc), mode(m), paint(p) {}
+ DrawVertices(const SkVertices* v, SkBlendMode m, const SkPaint& p)
+ : vertices(sk_ref_sp(const_cast<SkVertices*>(v))), mode(m), paint(p) {}
sk_sp<SkVertices> vertices;
- int boneCount;
SkBlendMode mode;
SkPaint paint;
void draw(SkCanvas* c, const SkMatrix&) const {
- c->drawVertices(vertices, pod<SkVertices::Bone>(this), boneCount, mode, paint);
+ c->drawVertices(vertices, mode, paint);
}
};
struct DrawAtlas final : Op {
@@ -530,6 +524,7 @@ void* DisplayListData::push(size_t pod, Args&&... args) {
// Next greater multiple of SKLITEDL_PAGE.
fReserved = (fUsed + skip + SKLITEDL_PAGE) & ~(SKLITEDL_PAGE - 1);
fBytes.realloc(fReserved);
+ LOG_ALWAYS_FATAL_IF(fBytes.get() == nullptr, "realloc(%zd) failed", fReserved);
}
SkASSERT(fUsed + skip <= fReserved);
auto op = (T*)(fBytes.get() + fUsed);
@@ -565,17 +560,16 @@ void DisplayListData::restore() {
this->push<Restore>(0);
}
void DisplayListData::saveLayer(const SkRect* bounds, const SkPaint* paint,
- const SkImageFilter* backdrop, const SkImage* clipMask,
- const SkMatrix* clipMatrix, SkCanvas::SaveLayerFlags flags) {
- this->push<SaveLayer>(0, bounds, paint, backdrop, clipMask, clipMatrix, flags);
+ const SkImageFilter* backdrop, SkCanvas::SaveLayerFlags flags) {
+ this->push<SaveLayer>(0, bounds, paint, backdrop, flags);
}
void DisplayListData::saveBehind(const SkRect* subset) {
this->push<SaveBehind>(0, subset);
}
-void DisplayListData::concat44(const SkScalar colMajor[16]) {
- this->push<Concat44>(0, colMajor);
+void DisplayListData::concat(const SkM44& m) {
+ this->push<Concat44>(0, m);
}
void DisplayListData::concat(const SkMatrix& matrix) {
this->push<Concat>(0, matrix);
@@ -686,11 +680,8 @@ void DisplayListData::drawPoints(SkCanvas::PointMode mode, size_t count, const S
void* pod = this->push<DrawPoints>(count * sizeof(SkPoint), mode, count, paint);
copy_v(pod, points, count);
}
-void DisplayListData::drawVertices(const SkVertices* vertices, const SkVertices::Bone bones[],
- int boneCount, SkBlendMode mode, const SkPaint& paint) {
- void* pod = this->push<DrawVertices>(boneCount * sizeof(SkVertices::Bone), vertices, boneCount,
- mode, paint);
- copy_v(pod, bones, boneCount);
+void DisplayListData::drawVertices(const SkVertices* vertices, SkBlendMode mode, const SkPaint& paint) {
+ this->push<DrawVertices>(0, vertices, mode, paint);
}
void DisplayListData::drawAtlas(const SkImage* atlas, const SkRSXform xforms[], const SkRect texs[],
const SkColor colors[], int count, SkBlendMode xfermode,
@@ -823,8 +814,7 @@ void RecordingCanvas::willSave() {
fDL->save();
}
SkCanvas::SaveLayerStrategy RecordingCanvas::getSaveLayerStrategy(const SaveLayerRec& rec) {
- fDL->saveLayer(rec.fBounds, rec.fPaint, rec.fBackdrop, rec.fClipMask, rec.fClipMatrix,
- rec.fSaveLayerFlags);
+ fDL->saveLayer(rec.fBounds, rec.fPaint, rec.fBackdrop, rec.fSaveLayerFlags);
return SkCanvas::kNoLayer_SaveLayerStrategy;
}
void RecordingCanvas::willRestore() {
@@ -841,8 +831,8 @@ bool RecordingCanvas::onDoSaveBehind(const SkRect* subset) {
return false;
}
-void RecordingCanvas::didConcat44(const SkScalar colMajor[16]) {
- fDL->concat44(colMajor);
+void RecordingCanvas::didConcat44(const SkM44& m) {
+ fDL->concat(m);
}
void RecordingCanvas::didConcat(const SkMatrix& matrix) {
fDL->concat(matrix);
@@ -929,24 +919,6 @@ void RecordingCanvas::onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScala
fDL->drawTextBlob(blob, x, y, paint);
}
-void RecordingCanvas::onDrawBitmap(const SkBitmap& bm, SkScalar x, SkScalar y,
- const SkPaint* paint) {
- fDL->drawImage(SkImage::MakeFromBitmap(bm), x, y, paint, BitmapPalette::Unknown);
-}
-void RecordingCanvas::onDrawBitmapNine(const SkBitmap& bm, const SkIRect& center, const SkRect& dst,
- const SkPaint* paint) {
- fDL->drawImageNine(SkImage::MakeFromBitmap(bm), center, dst, paint);
-}
-void RecordingCanvas::onDrawBitmapRect(const SkBitmap& bm, const SkRect* src, const SkRect& dst,
- const SkPaint* paint, SrcRectConstraint constraint) {
- fDL->drawImageRect(SkImage::MakeFromBitmap(bm), src, dst, paint, constraint,
- BitmapPalette::Unknown);
-}
-void RecordingCanvas::onDrawBitmapLattice(const SkBitmap& bm, const SkCanvas::Lattice& lattice,
- const SkRect& dst, const SkPaint* paint) {
- fDL->drawImageLattice(SkImage::MakeFromBitmap(bm), lattice, dst, paint, BitmapPalette::Unknown);
-}
-
void RecordingCanvas::drawImage(const sk_sp<SkImage>& image, SkScalar x, SkScalar y,
const SkPaint* paint, BitmapPalette palette) {
fDL->drawImage(image, x, y, paint, palette);
@@ -1007,9 +979,8 @@ void RecordingCanvas::onDrawPoints(SkCanvas::PointMode mode, size_t count, const
fDL->drawPoints(mode, count, pts, paint);
}
void RecordingCanvas::onDrawVerticesObject(const SkVertices* vertices,
- const SkVertices::Bone bones[], int boneCount,
SkBlendMode mode, const SkPaint& paint) {
- fDL->drawVertices(vertices, bones, boneCount, mode, paint);
+ fDL->drawVertices(vertices, mode, paint);
}
void RecordingCanvas::onDrawAtlas(const SkImage* atlas, const SkRSXform xforms[],
const SkRect texs[], const SkColor colors[], int count,
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 7eb1ce3eb18a..63d120c4ca19 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -77,12 +77,11 @@ private:
void flush();
void save();
- void saveLayer(const SkRect*, const SkPaint*, const SkImageFilter*, const SkImage*,
- const SkMatrix*, SkCanvas::SaveLayerFlags);
+ void saveLayer(const SkRect*, const SkPaint*, const SkImageFilter*, SkCanvas::SaveLayerFlags);
void saveBehind(const SkRect*);
void restore();
- void concat44(const SkScalar colMajor[16]);
+ void concat(const SkM44&);
void concat(const SkMatrix&);
void setMatrix(const SkMatrix&);
void scale(SkScalar, SkScalar);
@@ -120,8 +119,7 @@ private:
void drawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode,
const SkPaint&);
void drawPoints(SkCanvas::PointMode, size_t, const SkPoint[], const SkPaint&);
- void drawVertices(const SkVertices*, const SkVertices::Bone bones[], int boneCount, SkBlendMode,
- const SkPaint&);
+ void drawVertices(const SkVertices*, SkBlendMode, const SkPaint&);
void drawAtlas(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int,
SkBlendMode, const SkRect*, const SkPaint*);
void drawShadowRec(const SkPath&, const SkDrawShadowRec&);
@@ -155,7 +153,7 @@ public:
void onFlush() override;
- void didConcat44(const SkScalar[16]) override;
+ void didConcat44(const SkM44&) override;
void didConcat(const SkMatrix&) override;
void didSetMatrix(const SkMatrix&) override;
void didScale(SkScalar, SkScalar) override;
@@ -182,13 +180,6 @@ public:
void onDrawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&) override;
- void onDrawBitmap(const SkBitmap&, SkScalar, SkScalar, const SkPaint*) override;
- void onDrawBitmapLattice(const SkBitmap&, const Lattice&, const SkRect&,
- const SkPaint*) override;
- void onDrawBitmapNine(const SkBitmap&, const SkIRect&, const SkRect&, const SkPaint*) override;
- void onDrawBitmapRect(const SkBitmap&, const SkRect*, const SkRect&, const SkPaint*,
- SrcRectConstraint) override;
-
void drawImage(const sk_sp<SkImage>& image, SkScalar left, SkScalar top, const SkPaint* paint,
BitmapPalette pallete);
@@ -206,8 +197,7 @@ public:
void onDrawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode,
const SkPaint&) override;
void onDrawPoints(PointMode, size_t count, const SkPoint pts[], const SkPaint&) override;
- void onDrawVerticesObject(const SkVertices*, const SkVertices::Bone bones[], int boneCount,
- SkBlendMode, const SkPaint&) override;
+ void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override;
void onDrawAtlas(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int,
SkBlendMode, const SkRect*, const SkPaint*) override;
void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override;
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index c0ec2174bb35..6d5e62e955bb 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -94,17 +94,17 @@ public:
DISPLAY_LIST = 1 << 14,
};
- ANDROID_API RenderNode();
- ANDROID_API virtual ~RenderNode();
+ RenderNode();
+ virtual ~RenderNode();
// See flags defined in DisplayList.java
enum ReplayFlag { kReplayFlag_ClipChildren = 0x1 };
- ANDROID_API void setStagingDisplayList(DisplayList* newData);
+ void setStagingDisplayList(DisplayList* newData);
- ANDROID_API void output();
- ANDROID_API int getUsageSize();
- ANDROID_API int getAllocatedSize();
+ void output();
+ int getUsageSize();
+ int getAllocatedSize();
bool isRenderable() const { return mDisplayList && !mDisplayList->isEmpty(); }
@@ -149,12 +149,12 @@ public:
int getHeight() const { return properties().getHeight(); }
- ANDROID_API virtual void prepareTree(TreeInfo& info);
+ virtual void prepareTree(TreeInfo& info);
void destroyHardwareResources(TreeInfo* info = nullptr);
void destroyLayers();
// UI thread only!
- ANDROID_API void addAnimator(const sp<BaseRenderNodeAnimator>& animator);
+ void addAnimator(const sp<BaseRenderNodeAnimator>& animator);
void removeAnimator(const sp<BaseRenderNodeAnimator>& animator);
// This can only happen during pushStaging()
@@ -179,7 +179,7 @@ public:
// the frameNumber to appropriately batch/synchronize these transactions.
// There is no other filtering/batching to ensure that only the "final"
// state called once per frame.
- class ANDROID_API PositionListener : public VirtualLightRefBase {
+ class PositionListener : public VirtualLightRefBase {
public:
virtual ~PositionListener() {}
// Called when the RenderNode's position changes
@@ -190,14 +190,14 @@ public:
virtual void onPositionLost(RenderNode& node, const TreeInfo* info) = 0;
};
- ANDROID_API void setPositionListener(PositionListener* listener) {
+ void setPositionListener(PositionListener* listener) {
mStagingPositionListener = listener;
mPositionListenerDirty = true;
}
// This is only modified in MODE_FULL, so it can be safely accessed
// on the UI thread.
- ANDROID_API bool hasParents() { return mParentCount; }
+ bool hasParents() { return mParentCount; }
void onRemovedFromTree(TreeInfo* info);
diff --git a/libs/hwui/RenderProperties.cpp b/libs/hwui/RenderProperties.cpp
index ff9cf45cdc73..8fba9cf21df1 100644
--- a/libs/hwui/RenderProperties.cpp
+++ b/libs/hwui/RenderProperties.cpp
@@ -49,6 +49,12 @@ bool LayerProperties::setColorFilter(SkColorFilter* filter) {
return true;
}
+bool LayerProperties::setImageFilter(SkImageFilter* imageFilter) {
+ if(mImageFilter.get() == imageFilter) return false;
+ mImageFilter = sk_ref_sp(imageFilter);
+ return true;
+}
+
bool LayerProperties::setFromPaint(const SkPaint* paint) {
bool changed = false;
changed |= setAlpha(static_cast<uint8_t>(PaintUtils::getAlphaDirect(paint)));
@@ -63,6 +69,7 @@ LayerProperties& LayerProperties::operator=(const LayerProperties& other) {
setAlpha(other.alpha());
setXferMode(other.xferMode());
setColorFilter(other.getColorFilter());
+ setImageFilter(other.getImageFilter());
return *this;
}
diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h
index 24f6035b6708..aeb60e6ce355 100644
--- a/libs/hwui/RenderProperties.h
+++ b/libs/hwui/RenderProperties.h
@@ -27,6 +27,7 @@
#include "utils/PaintUtils.h"
#include <SkBlendMode.h>
+#include <SkImageFilter.h>
#include <SkCamera.h>
#include <SkColor.h>
#include <SkMatrix.h>
@@ -69,7 +70,7 @@ enum ClippingFlags {
CLIP_TO_CLIP_BOUNDS = 0x1 << 1,
};
-class ANDROID_API LayerProperties {
+class LayerProperties {
public:
bool setType(LayerType type) {
if (RP_SET(mType, type)) {
@@ -93,6 +94,10 @@ public:
SkColorFilter* getColorFilter() const { return mColorFilter.get(); }
+ bool setImageFilter(SkImageFilter* imageFilter);
+
+ SkImageFilter* getImageFilter() const { return mImageFilter.get(); }
+
// Sets alpha, xfermode, and colorfilter from an SkPaint
// paint may be NULL, in which case defaults will be set
bool setFromPaint(const SkPaint* paint);
@@ -118,12 +123,13 @@ private:
uint8_t mAlpha;
SkBlendMode mMode;
sk_sp<SkColorFilter> mColorFilter;
+ sk_sp<SkImageFilter> mImageFilter;
};
/*
* Data structure that holds the properties for a RenderNode
*/
-class ANDROID_API RenderProperties {
+class RenderProperties {
public:
RenderProperties();
virtual ~RenderProperties();
@@ -541,6 +547,7 @@ public:
bool promotedToLayer() const {
return mLayerProperties.mType == LayerType::None && fitsOnLayer() &&
(mComputedFields.mNeedLayerForFunctors ||
+ mLayerProperties.mImageFilter != nullptr ||
(!MathUtils::isZero(mPrimitiveFields.mAlpha) && mPrimitiveFields.mAlpha < 1 &&
mPrimitiveFields.mHasOverlappingRendering));
}
diff --git a/libs/hwui/RootRenderNode.h b/libs/hwui/RootRenderNode.h
index 12de4ecac94b..1d3f5a8a51e0 100644
--- a/libs/hwui/RootRenderNode.h
+++ b/libs/hwui/RootRenderNode.h
@@ -27,16 +27,16 @@
namespace android::uirenderer {
-class ANDROID_API RootRenderNode : public RenderNode {
+class RootRenderNode : public RenderNode {
public:
- ANDROID_API explicit RootRenderNode(std::unique_ptr<ErrorHandler> errorHandler)
+ explicit RootRenderNode(std::unique_ptr<ErrorHandler> errorHandler)
: RenderNode(), mErrorHandler(std::move(errorHandler)) {}
- ANDROID_API virtual ~RootRenderNode() {}
+ virtual ~RootRenderNode() {}
virtual void prepareTree(TreeInfo& info) override;
- ANDROID_API void attachAnimatingNode(RenderNode* animatingNode);
+ void attachAnimatingNode(RenderNode* animatingNode);
void attachPendingVectorDrawableAnimators();
@@ -53,9 +53,9 @@ public:
void pushStagingVectorDrawableAnimators(AnimationContext* context);
- ANDROID_API void destroy();
+ void destroy();
- ANDROID_API void addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim);
+ void addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim);
private:
const std::unique_ptr<ErrorHandler> mErrorHandler;
@@ -75,12 +75,11 @@ private:
};
#ifdef __ANDROID__ // Layoutlib does not support Animations
-class ANDROID_API ContextFactoryImpl : public IContextFactory {
+class ContextFactoryImpl : public IContextFactory {
public:
- ANDROID_API explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {}
+ explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {}
- ANDROID_API virtual AnimationContext* createAnimationContext(
- renderthread::TimeLord& clock) override;
+ virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override;
private:
RootRenderNode* mRootNode;
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index 941437998838..1dbce58fb7c9 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -40,6 +40,7 @@
#include <SkShader.h>
#include <SkTemplates.h>
#include <SkTextBlob.h>
+#include <SkVertices.h>
#include <memory>
#include <optional>
@@ -729,8 +730,7 @@ void SkiaCanvas::drawVectorDrawable(VectorDrawableRoot* vectorDrawable) {
// ----------------------------------------------------------------------------
void SkiaCanvas::drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& paint, float x,
- float y, float boundsLeft, float boundsTop, float boundsRight,
- float boundsBottom, float totalAdvance) {
+ float y, float totalAdvance) {
if (count <= 0 || paint.nothingToDraw()) return;
Paint paintCopy(paint);
if (mPaintFilter) {
@@ -842,9 +842,4 @@ void SkiaCanvas::drawRenderNode(uirenderer::RenderNode* renderNode) {
LOG_ALWAYS_FATAL("SkiaCanvas can't directly draw RenderNodes");
}
-void SkiaCanvas::callDrawGLFunction(Functor* functor,
- uirenderer::GlFunctorLifecycleListener* listener) {
- LOG_ALWAYS_FATAL("SkiaCanvas can't directly draw GL Content");
-}
-
} // namespace android
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index 1eb089d8764c..2cb850c83934 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -57,8 +57,8 @@ public:
LOG_ALWAYS_FATAL("SkiaCanvas does not produce a DisplayList");
return nullptr;
}
- virtual void insertReorderBarrier(bool enableReorder) override {
- LOG_ALWAYS_FATAL("SkiaCanvas does not support reordering barriers");
+ virtual void enableZ(bool enableZ) override {
+ LOG_ALWAYS_FATAL("SkiaCanvas does not support enableZ");
}
virtual void setBitmap(const SkBitmap& bitmap) override;
@@ -152,8 +152,6 @@ public:
virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) override;
virtual void drawRenderNode(uirenderer::RenderNode* renderNode) override;
- virtual void callDrawGLFunction(Functor* functor,
- uirenderer::GlFunctorLifecycleListener* listener) override;
virtual void drawPicture(const SkPicture& picture) override;
protected:
@@ -163,8 +161,7 @@ protected:
void drawDrawable(SkDrawable* drawable) { mCanvas->drawDrawable(drawable); }
virtual void drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& paint, float x,
- float y, float boundsLeft, float boundsTop, float boundsRight,
- float boundsBottom, float totalAdvance) override;
+ float y, float totalAdvance) override;
virtual void drawLayoutOnPath(const minikin::Layout& layout, float hOffset, float vOffset,
const Paint& paint, const SkPath& path, size_t start,
size_t end) override;
diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h
index e1b6f2adde74..ac7d41e0d600 100644
--- a/libs/hwui/VectorDrawable.h
+++ b/libs/hwui/VectorDrawable.h
@@ -97,7 +97,7 @@ private:
bool* mStagingDirty;
};
-class ANDROID_API Node {
+class Node {
public:
class Properties {
public:
@@ -127,9 +127,9 @@ protected:
PropertyChangedListener* mPropertyChangedListener = nullptr;
};
-class ANDROID_API Path : public Node {
+class Path : public Node {
public:
- struct ANDROID_API Data {
+ struct Data {
std::vector<char> verbs;
std::vector<size_t> verbSizes;
std::vector<float> points;
@@ -200,7 +200,7 @@ private:
bool mStagingPropertiesDirty = true;
};
-class ANDROID_API FullPath : public Path {
+class FullPath : public Path {
public:
class FullPathProperties : public Properties {
public:
@@ -369,7 +369,7 @@ private:
bool mAntiAlias = true;
};
-class ANDROID_API ClipPath : public Path {
+class ClipPath : public Path {
public:
ClipPath(const ClipPath& path) : Path(path) {}
ClipPath(const char* path, size_t strLength) : Path(path, strLength) {}
@@ -378,7 +378,7 @@ public:
virtual void setAntiAlias(bool aa) {}
};
-class ANDROID_API Group : public Node {
+class Group : public Node {
public:
class GroupProperties : public Properties {
public:
@@ -498,7 +498,7 @@ private:
std::vector<std::unique_ptr<Node> > mChildren;
};
-class ANDROID_API Tree : public VirtualLightRefBase {
+class Tree : public VirtualLightRefBase {
public:
explicit Tree(Group* rootNode) : mRootNode(rootNode) {
mRootNode->setPropertyChangedListener(&mPropertyChangedListener);
diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp
index 4bbf1214bdcf..dca10e29cbb8 100644
--- a/libs/hwui/apex/LayoutlibLoader.cpp
+++ b/libs/hwui/apex/LayoutlibLoader.cpp
@@ -47,6 +47,7 @@ extern int register_android_graphics_MaskFilter(JNIEnv* env);
extern int register_android_graphics_NinePatch(JNIEnv*);
extern int register_android_graphics_PathEffect(JNIEnv* env);
extern int register_android_graphics_Shader(JNIEnv* env);
+extern int register_android_graphics_RenderEffect(JNIEnv* env);
extern int register_android_graphics_Typeface(JNIEnv* env);
namespace android {
@@ -108,6 +109,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = {
{"android.graphics.RecordingCanvas", REG_JNI(register_android_view_DisplayListCanvas)},
// {"android.graphics.Region", REG_JNI(register_android_graphics_Region)},
{"android.graphics.Shader", REG_JNI(register_android_graphics_Shader)},
+ {"android.graphics.RenderEffect", REG_JNI(register_android_graphics_RenderEffect)},
{"android.graphics.Typeface", REG_JNI(register_android_graphics_Typeface)},
{"android.graphics.animation.NativeInterpolatorFactory",
REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory)},
diff --git a/libs/hwui/apex/java/android/graphics/ColorMatrix.java b/libs/hwui/apex/java/android/graphics/ColorMatrix.java
new file mode 100644
index 000000000000..6299b2c47ea1
--- /dev/null
+++ b/libs/hwui/apex/java/android/graphics/ColorMatrix.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2007 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 android.graphics;
+
+import java.util.Arrays;
+
+/**
+ * 4x5 matrix for transforming the color and alpha components of a Bitmap.
+ * The matrix can be passed as single array, and is treated as follows:
+ *
+ * <pre>
+ * [ a, b, c, d, e,
+ * f, g, h, i, j,
+ * k, l, m, n, o,
+ * p, q, r, s, t ]</pre>
+ *
+ * <p>
+ * When applied to a color <code>[R, G, B, A]</code>, the resulting color
+ * is computed as:
+ * </p>
+ *
+ * <pre>
+ * R&rsquo; = a*R + b*G + c*B + d*A + e;
+ * G&rsquo; = f*R + g*G + h*B + i*A + j;
+ * B&rsquo; = k*R + l*G + m*B + n*A + o;
+ * A&rsquo; = p*R + q*G + r*B + s*A + t;</pre>
+ *
+ * <p>
+ * That resulting color <code>[R&rsquo;, G&rsquo;, B&rsquo;, A&rsquo;]</code>
+ * then has each channel clamped to the <code>0</code> to <code>255</code>
+ * range.
+ * </p>
+ *
+ * <p>
+ * The sample ColorMatrix below inverts incoming colors by scaling each
+ * channel by <code>-1</code>, and then shifting the result up by
+ * <code>255</code> to remain in the standard color space.
+ * </p>
+ *
+ * <pre>
+ * [ -1, 0, 0, 0, 255,
+ * 0, -1, 0, 0, 255,
+ * 0, 0, -1, 0, 255,
+ * 0, 0, 0, 1, 0 ]</pre>
+ */
+@SuppressWarnings({ "MismatchedReadAndWriteOfArray", "PointlessArithmeticExpression" })
+public class ColorMatrix {
+ private final float[] mArray = new float[20];
+
+ /**
+ * Create a new colormatrix initialized to identity (as if reset() had
+ * been called).
+ */
+ public ColorMatrix() {
+ reset();
+ }
+
+ /**
+ * Create a new colormatrix initialized with the specified array of values.
+ */
+ public ColorMatrix(float[] src) {
+ System.arraycopy(src, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Create a new colormatrix initialized with the specified colormatrix.
+ */
+ public ColorMatrix(ColorMatrix src) {
+ System.arraycopy(src.mArray, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Return the array of floats representing this colormatrix.
+ */
+ public final float[] getArray() { return mArray; }
+
+ /**
+ * Set this colormatrix to identity:
+ * <pre>
+ * [ 1 0 0 0 0 - red vector
+ * 0 1 0 0 0 - green vector
+ * 0 0 1 0 0 - blue vector
+ * 0 0 0 1 0 ] - alpha vector
+ * </pre>
+ */
+ public void reset() {
+ final float[] a = mArray;
+ Arrays.fill(a, 0);
+ a[0] = a[6] = a[12] = a[18] = 1;
+ }
+
+ /**
+ * Assign the src colormatrix into this matrix, copying all of its values.
+ */
+ public void set(ColorMatrix src) {
+ System.arraycopy(src.mArray, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Assign the array of floats into this matrix, copying all of its values.
+ */
+ public void set(float[] src) {
+ System.arraycopy(src, 0, mArray, 0, 20);
+ }
+
+ /**
+ * Set this colormatrix to scale by the specified values.
+ */
+ public void setScale(float rScale, float gScale, float bScale,
+ float aScale) {
+ final float[] a = mArray;
+
+ for (int i = 19; i > 0; --i) {
+ a[i] = 0;
+ }
+ a[0] = rScale;
+ a[6] = gScale;
+ a[12] = bScale;
+ a[18] = aScale;
+ }
+
+ /**
+ * Set the rotation on a color axis by the specified values.
+ * <p>
+ * <code>axis=0</code> correspond to a rotation around the RED color
+ * <code>axis=1</code> correspond to a rotation around the GREEN color
+ * <code>axis=2</code> correspond to a rotation around the BLUE color
+ * </p>
+ */
+ public void setRotate(int axis, float degrees) {
+ reset();
+ double radians = degrees * Math.PI / 180d;
+ float cosine = (float) Math.cos(radians);
+ float sine = (float) Math.sin(radians);
+ switch (axis) {
+ // Rotation around the red color
+ case 0:
+ mArray[6] = mArray[12] = cosine;
+ mArray[7] = sine;
+ mArray[11] = -sine;
+ break;
+ // Rotation around the green color
+ case 1:
+ mArray[0] = mArray[12] = cosine;
+ mArray[2] = -sine;
+ mArray[10] = sine;
+ break;
+ // Rotation around the blue color
+ case 2:
+ mArray[0] = mArray[6] = cosine;
+ mArray[1] = sine;
+ mArray[5] = -sine;
+ break;
+ default:
+ throw new RuntimeException();
+ }
+ }
+
+ /**
+ * Set this colormatrix to the concatenation of the two specified
+ * colormatrices, such that the resulting colormatrix has the same effect
+ * as applying matB and then applying matA.
+ * <p>
+ * It is legal for either matA or matB to be the same colormatrix as this.
+ * </p>
+ */
+ public void setConcat(ColorMatrix matA, ColorMatrix matB) {
+ float[] tmp;
+ if (matA == this || matB == this) {
+ tmp = new float[20];
+ } else {
+ tmp = mArray;
+ }
+
+ final float[] a = matA.mArray;
+ final float[] b = matB.mArray;
+ int index = 0;
+ for (int j = 0; j < 20; j += 5) {
+ for (int i = 0; i < 4; i++) {
+ tmp[index++] = a[j + 0] * b[i + 0] + a[j + 1] * b[i + 5] +
+ a[j + 2] * b[i + 10] + a[j + 3] * b[i + 15];
+ }
+ tmp[index++] = a[j + 0] * b[4] + a[j + 1] * b[9] +
+ a[j + 2] * b[14] + a[j + 3] * b[19] +
+ a[j + 4];
+ }
+
+ if (tmp != mArray) {
+ System.arraycopy(tmp, 0, mArray, 0, 20);
+ }
+ }
+
+ /**
+ * Concat this colormatrix with the specified prematrix.
+ * <p>
+ * This is logically the same as calling setConcat(this, prematrix);
+ * </p>
+ */
+ public void preConcat(ColorMatrix prematrix) {
+ setConcat(this, prematrix);
+ }
+
+ /**
+ * Concat this colormatrix with the specified postmatrix.
+ * <p>
+ * This is logically the same as calling setConcat(postmatrix, this);
+ * </p>
+ */
+ public void postConcat(ColorMatrix postmatrix) {
+ setConcat(postmatrix, this);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set the matrix to affect the saturation of colors.
+ *
+ * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
+ */
+ public void setSaturation(float sat) {
+ reset();
+ float[] m = mArray;
+
+ final float invSat = 1 - sat;
+ final float R = 0.213f * invSat;
+ final float G = 0.715f * invSat;
+ final float B = 0.072f * invSat;
+
+ m[0] = R + sat; m[1] = G; m[2] = B;
+ m[5] = R; m[6] = G + sat; m[7] = B;
+ m[10] = R; m[11] = G; m[12] = B + sat;
+ }
+
+ /**
+ * Set the matrix to convert RGB to YUV
+ */
+ public void setRGB2YUV() {
+ reset();
+ float[] m = mArray;
+ // these coefficients match those in libjpeg
+ m[0] = 0.299f; m[1] = 0.587f; m[2] = 0.114f;
+ m[5] = -0.16874f; m[6] = -0.33126f; m[7] = 0.5f;
+ m[10] = 0.5f; m[11] = -0.41869f; m[12] = -0.08131f;
+ }
+
+ /**
+ * Set the matrix to convert from YUV to RGB
+ */
+ public void setYUV2RGB() {
+ reset();
+ float[] m = mArray;
+ // these coefficients match those in libjpeg
+ m[2] = 1.402f;
+ m[5] = 1; m[6] = -0.34414f; m[7] = -0.71414f;
+ m[10] = 1; m[11] = 1.772f; m[12] = 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // if (obj == this) return true; -- NaN value would mean matrix != itself
+ if (!(obj instanceof ColorMatrix)) {
+ return false;
+ }
+
+ // we don't use Arrays.equals(), since that considers NaN == NaN
+ final float[] other = ((ColorMatrix) obj).mArray;
+ for (int i = 0; i < 20; i++) {
+ if (other[i] != mArray[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index a114e2f42157..e1f5abd786bf 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -16,14 +16,14 @@
#include "android/graphics/jni_runtime.h"
-#include <android/log.h>
-#include <nativehelper/JNIHelp.h>
-#include <sys/cdefs.h>
-
#include <EGL/egl.h>
#include <GraphicsJNI.h>
#include <Properties.h>
#include <SkGraphics.h>
+#include <android/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <sys/cdefs.h>
+#include <vulkan/vulkan.h>
#undef LOG_TAG
#define LOG_TAG "AndroidGraphicsJNI"
@@ -43,6 +43,7 @@ extern int register_android_graphics_Movie(JNIEnv* env);
extern int register_android_graphics_NinePatch(JNIEnv*);
extern int register_android_graphics_PathEffect(JNIEnv* env);
extern int register_android_graphics_Shader(JNIEnv* env);
+extern int register_android_graphics_RenderEffect(JNIEnv* env);
extern int register_android_graphics_Typeface(JNIEnv* env);
extern int register_android_graphics_YuvImage(JNIEnv* env);
@@ -61,6 +62,7 @@ extern int register_android_graphics_Path(JNIEnv* env);
extern int register_android_graphics_PathMeasure(JNIEnv* env);
extern int register_android_graphics_Picture(JNIEnv*);
extern int register_android_graphics_Region(JNIEnv* env);
+extern int register_android_graphics_TextureLayer(JNIEnv* env);
extern int register_android_graphics_animation_NativeInterpolatorFactory(JNIEnv* env);
extern int register_android_graphics_animation_RenderNodeAnimator(JNIEnv* env);
extern int register_android_graphics_drawable_AnimatedVectorDrawable(JNIEnv* env);
@@ -72,11 +74,11 @@ extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env);
extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env);
extern int register_android_graphics_text_MeasuredText(JNIEnv* env);
extern int register_android_graphics_text_LineBreaker(JNIEnv *env);
+extern int register_android_graphics_text_TextShaper(JNIEnv *env);
extern int register_android_util_PathParser(JNIEnv* env);
extern int register_android_view_DisplayListCanvas(JNIEnv* env);
extern int register_android_view_RenderNode(JNIEnv* env);
-extern int register_android_view_TextureLayer(JNIEnv* env);
extern int register_android_view_ThreadedRenderer(JNIEnv* env);
#ifdef NDEBUG
@@ -123,6 +125,8 @@ static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_graphics_Picture),
REG_JNI(register_android_graphics_Region),
REG_JNI(register_android_graphics_Shader),
+ REG_JNI(register_android_graphics_RenderEffect),
+ REG_JNI(register_android_graphics_TextureLayer),
REG_JNI(register_android_graphics_Typeface),
REG_JNI(register_android_graphics_YuvImage),
REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory),
@@ -136,11 +140,11 @@ static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_graphics_pdf_PdfRenderer),
REG_JNI(register_android_graphics_text_MeasuredText),
REG_JNI(register_android_graphics_text_LineBreaker),
+ REG_JNI(register_android_graphics_text_TextShaper),
REG_JNI(register_android_util_PathParser),
REG_JNI(register_android_view_RenderNode),
REG_JNI(register_android_view_DisplayListCanvas),
- REG_JNI(register_android_view_TextureLayer),
REG_JNI(register_android_view_ThreadedRenderer),
};
@@ -172,6 +176,11 @@ using android::uirenderer::RenderPipelineType;
void zygote_preload_graphics() {
if (Properties::peekRenderPipelineType() == RenderPipelineType::SkiaGL) {
+ // Preload GL driver if HWUI renders with GL backend.
eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ } else {
+ // Preload Vulkan driver if HWUI renders with Vulkan backend.
+ uint32_t apiVersion;
+ vkEnumerateInstanceVersion(&apiVersion);
}
-} \ No newline at end of file
+}
diff --git a/libs/hwui/api/current.txt b/libs/hwui/api/current.txt
new file mode 100644
index 000000000000..c396a2032eed
--- /dev/null
+++ b/libs/hwui/api/current.txt
@@ -0,0 +1,23 @@
+// Signature format: 2.0
+package android.graphics {
+
+ public class ColorMatrix {
+ ctor public ColorMatrix();
+ ctor public ColorMatrix(float[]);
+ ctor public ColorMatrix(android.graphics.ColorMatrix);
+ method public final float[] getArray();
+ method public void postConcat(android.graphics.ColorMatrix);
+ method public void preConcat(android.graphics.ColorMatrix);
+ method public void reset();
+ method public void set(android.graphics.ColorMatrix);
+ method public void set(float[]);
+ method public void setConcat(android.graphics.ColorMatrix, android.graphics.ColorMatrix);
+ method public void setRGB2YUV();
+ method public void setRotate(int, float);
+ method public void setSaturation(float);
+ method public void setScale(float, float, float, float);
+ method public void setYUV2RGB();
+ }
+
+}
+
diff --git a/libs/hwui/api/module-lib-current.txt b/libs/hwui/api/module-lib-current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/libs/hwui/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/hwui/api/module-lib-removed.txt b/libs/hwui/api/module-lib-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/libs/hwui/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/hwui/api/removed.txt b/libs/hwui/api/removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/libs/hwui/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/hwui/api/system-current.txt b/libs/hwui/api/system-current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/libs/hwui/api/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/hwui/api/system-removed.txt b/libs/hwui/api/system-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/libs/hwui/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/hwui/hwui/AnimatedImageDrawable.h b/libs/hwui/hwui/AnimatedImageDrawable.h
index f0aa35acf71b..f81a5a40b44e 100644
--- a/libs/hwui/hwui/AnimatedImageDrawable.h
+++ b/libs/hwui/hwui/AnimatedImageDrawable.h
@@ -44,7 +44,7 @@ public:
* This class can be drawn into Canvas.h and maintains the state needed to drive
* the animation from the RenderThread.
*/
-class ANDROID_API AnimatedImageDrawable : public SkDrawable {
+class AnimatedImageDrawable : public SkDrawable {
public:
// bytesUsed includes the approximate sizes of the SkAnimatedImage and the SkPictures in the
// Snapshots.
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 60ef4371d38d..1a89cfd5d0ad 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -33,7 +33,6 @@
#ifndef _WIN32
#include <binder/IServiceManager.h>
#endif
-#include <ui/PixelFormat.h>
#include <SkCanvas.h>
#include <SkImagePriv.h>
@@ -132,15 +131,8 @@ sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, s
return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}
-void FreePixelRef(void* addr, void* context) {
- auto pixelRef = (SkPixelRef*)context;
- pixelRef->unref();
-}
-
sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, SkPixelRef& pixelRef) {
- pixelRef.ref();
- return sk_sp<Bitmap>(new Bitmap((void*)pixelRef.pixels(), (void*)&pixelRef, FreePixelRef, info,
- pixelRef.rowBytes()));
+ return sk_sp<Bitmap>(new Bitmap(pixelRef, info));
}
@@ -230,14 +222,12 @@ Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBy
mPixelStorage.heap.size = size;
}
-Bitmap::Bitmap(void* address, void* context, FreeFunc freeFunc, const SkImageInfo& info,
- size_t rowBytes)
- : SkPixelRef(info.width(), info.height(), address, rowBytes)
+Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info)
+ : SkPixelRef(info.width(), info.height(), pixelRef.pixels(), pixelRef.rowBytes())
, mInfo(validateAlpha(info))
- , mPixelStorageType(PixelStorageType::External) {
- mPixelStorage.external.address = address;
- mPixelStorage.external.context = context;
- mPixelStorage.external.freeFunc = freeFunc;
+ , mPixelStorageType(PixelStorageType::WrappedPixelRef) {
+ pixelRef.ref();
+ mPixelStorage.wrapped.pixelRef = &pixelRef;
}
Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes)
@@ -266,9 +256,8 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes
Bitmap::~Bitmap() {
switch (mPixelStorageType) {
- case PixelStorageType::External:
- mPixelStorage.external.freeFunc(mPixelStorage.external.address,
- mPixelStorage.external.context);
+ case PixelStorageType::WrappedPixelRef:
+ mPixelStorage.wrapped.pixelRef->unref();
break;
case PixelStorageType::Ashmem:
#ifndef _WIN32 // ashmem not implemented on Windows
@@ -300,19 +289,6 @@ void Bitmap::setHasHardwareMipMap(bool hasMipMap) {
mHasHardwareMipMap = hasMipMap;
}
-void* Bitmap::getStorage() const {
- switch (mPixelStorageType) {
- case PixelStorageType::External:
- return mPixelStorage.external.address;
- case PixelStorageType::Ashmem:
- return mPixelStorage.ashmem.address;
- case PixelStorageType::Heap:
- return mPixelStorage.heap.address;
- case PixelStorageType::Hardware:
- return nullptr;
- }
-}
-
int Bitmap::getAshmemFd() const {
switch (mPixelStorageType) {
case PixelStorageType::Ashmem:
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index b8b59947a57b..6ece7ef9f329 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -32,7 +32,7 @@ class SkWStream;
namespace android {
enum class PixelStorageType {
- External,
+ WrappedPixelRef,
Heap,
Ashmem,
Hardware,
@@ -56,7 +56,7 @@ class PixelStorage;
typedef void (*FreeFunc)(void* addr, void* context);
-class ANDROID_API Bitmap : public SkPixelRef {
+class Bitmap : public SkPixelRef {
public:
/* The allocate factories not only construct the Bitmap object but also allocate the
* backing store whose type is determined by the specific method that is called.
@@ -71,6 +71,7 @@ public:
static sk_sp<Bitmap> allocateHardwareBitmap(const SkBitmap& bitmap);
static sk_sp<Bitmap> allocateHeapBitmap(SkBitmap* bitmap);
static sk_sp<Bitmap> allocateHeapBitmap(const SkImageInfo& info);
+ static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& i, size_t rowBytes);
/* The createFrom factories construct a new Bitmap object by wrapping the already allocated
* memory that is provided as an input param.
@@ -160,11 +161,9 @@ public:
int32_t quality, SkWStream* stream);
private:
static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes);
- static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& i, size_t rowBytes);
Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes);
- Bitmap(void* address, void* context, FreeFunc freeFunc, const SkImageInfo& info,
- size_t rowBytes);
+ Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info);
Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes);
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes,
@@ -178,7 +177,6 @@ private:
#endif
virtual ~Bitmap();
- void* getStorage() const;
SkImageInfo mInfo;
@@ -191,10 +189,8 @@ private:
union {
struct {
- void* address;
- void* context;
- FreeFunc freeFunc;
- } external;
+ SkPixelRef* pixelRef;
+ } wrapped;
struct {
void* address;
int fd;
diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp
index c138a32eacc2..146bf283c58a 100644
--- a/libs/hwui/hwui/Canvas.cpp
+++ b/libs/hwui/hwui/Canvas.cpp
@@ -84,13 +84,12 @@ static void simplifyPaint(int color, Paint* paint) {
class DrawTextFunctor {
public:
DrawTextFunctor(const minikin::Layout& layout, Canvas* canvas, const Paint& paint, float x,
- float y, minikin::MinikinRect& bounds, float totalAdvance)
+ float y, float totalAdvance)
: layout(layout)
, canvas(canvas)
, paint(paint)
, x(x)
, y(y)
- , bounds(bounds)
, totalAdvance(totalAdvance) {}
void operator()(size_t start, size_t end) {
@@ -114,19 +113,16 @@ public:
Paint outlinePaint(paint);
simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint);
outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style);
- canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, bounds.mLeft, bounds.mTop,
- bounds.mRight, bounds.mBottom, totalAdvance);
+ canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
// inner
Paint innerPaint(paint);
simplifyPaint(darken ? SK_ColorBLACK : SK_ColorWHITE, &innerPaint);
innerPaint.setStyle(SkPaint::kFill_Style);
- canvas->drawGlyphs(glyphFunc, glyphCount, innerPaint, x, y, bounds.mLeft, bounds.mTop,
- bounds.mRight, bounds.mBottom, totalAdvance);
+ canvas->drawGlyphs(glyphFunc, glyphCount, innerPaint, x, y, totalAdvance);
} else {
// standard draw path
- canvas->drawGlyphs(glyphFunc, glyphCount, paint, x, y, bounds.mLeft, bounds.mTop,
- bounds.mRight, bounds.mBottom, totalAdvance);
+ canvas->drawGlyphs(glyphFunc, glyphCount, paint, x, y, totalAdvance);
}
}
@@ -136,10 +132,29 @@ private:
const Paint& paint;
float x;
float y;
- minikin::MinikinRect& bounds;
float totalAdvance;
};
+void Canvas::drawGlyphs(const minikin::Font& font, const int* glyphIds, const float* positions,
+ int glyphCount, const Paint& paint) {
+ // Minikin modify skFont for auto-fakebold/auto-fakeitalic.
+ Paint copied(paint);
+
+ auto glyphFunc = [&](uint16_t* outGlyphIds, float* outPositions) {
+ for (uint32_t i = 0; i < glyphCount; ++i) {
+ outGlyphIds[i] = static_cast<uint16_t>(glyphIds[i]);
+ }
+ memcpy(outPositions, positions, sizeof(float) * 2 * glyphCount);
+ };
+
+ const minikin::MinikinFont* minikinFont = font.typeface().get();
+ SkFont* skfont = &copied.getSkFont();
+ MinikinFontSkia::populateSkFont(skfont, minikinFont, minikin::FontFakery());
+
+ // total advance is used for drawing underline. We do not support underlyine by glyph drawing.
+ drawGlyphs(glyphFunc, glyphCount, copied, 0 /* x */, 0 /* y */, 0 /* total Advance */);
+}
+
void Canvas::drawText(const uint16_t* text, int textSize, int start, int count, int contextStart,
int contextCount, float x, float y, minikin::Bidi bidiFlags,
const Paint& origPaint, const Typeface* typeface, minikin::MeasuredText* mt) {
@@ -156,15 +171,12 @@ void Canvas::drawText(const uint16_t* text, int textSize, int start, int count,
x += MinikinUtils::xOffsetForTextAlign(&paint, layout);
- minikin::MinikinRect bounds;
- layout.getBounds(&bounds);
-
// Set align to left for drawing, as we don't want individual
// glyphs centered or right-aligned; the offset above takes
// care of all alignment.
paint.setTextAlign(Paint::kLeft_Align);
- DrawTextFunctor f(layout, this, paint, x, y, bounds, layout.getAdvance());
+ DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance());
MinikinUtils::forFontRun(layout, &paint, f);
}
diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h
index 27dfed305a94..772b7a28ef04 100644
--- a/libs/hwui/hwui/Canvas.h
+++ b/libs/hwui/hwui/Canvas.h
@@ -20,7 +20,6 @@
#include <utils/Functor.h>
#include <androidfw/ResourceTypes.h>
-#include "GlFunctorLifecycleListener.h"
#include "Properties.h"
#include "utils/Macros.h"
@@ -33,6 +32,7 @@ class SkCanvasState;
class SkVertices;
namespace minikin {
+class Font;
class Layout;
class MeasuredText;
enum class Bidi : uint8_t;
@@ -144,7 +144,7 @@ public:
virtual void resetRecording(int width, int height,
uirenderer::RenderNode* renderNode = nullptr) = 0;
virtual uirenderer::DisplayList* finishRecording() = 0;
- virtual void insertReorderBarrier(bool enableReorder) = 0;
+ virtual void enableZ(bool enableZ) = 0;
bool isHighContrastText() const { return uirenderer::Properties::enableHighContrastText; }
@@ -162,8 +162,7 @@ public:
virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) = 0;
virtual void drawRenderNode(uirenderer::RenderNode* renderNode) = 0;
- virtual void callDrawGLFunction(Functor* functor,
- uirenderer::GlFunctorLifecycleListener* listener) = 0;
+
virtual void drawWebViewFunctor(int /*functor*/) {
LOG_ALWAYS_FATAL("Not supported");
}
@@ -257,6 +256,9 @@ public:
*/
virtual void drawVectorDrawable(VectorDrawableRoot* tree) = 0;
+ void drawGlyphs(const minikin::Font& font, const int* glyphIds, const float* positions,
+ int glyphCount, const Paint& paint);
+
/**
* Converts utf16 text to glyphs, calculating position and boundary,
* and delegating the final draw to virtual drawGlyphs method.
@@ -290,8 +292,7 @@ protected:
* totalAdvance: used to define width of text decorations (underlines, strikethroughs).
*/
virtual void drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& paint, float x,
- float y, float boundsLeft, float boundsTop, float boundsRight,
- float boundsBottom, float totalAdvance) = 0;
+ float y,float totalAdvance) = 0;
virtual void drawLayoutOnPath(const minikin::Layout& layout, float hOffset, float vOffset,
const Paint& paint, const SkPath& path, size_t start,
size_t end) = 0;
diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp
index 6a12a203b9f8..0e338f35b8e7 100644
--- a/libs/hwui/hwui/MinikinSkia.cpp
+++ b/libs/hwui/hwui/MinikinSkia.cpp
@@ -33,8 +33,7 @@ namespace android {
MinikinFontSkia::MinikinFontSkia(sk_sp<SkTypeface> typeface, const void* fontData, size_t fontSize,
std::string_view filePath, int ttcIndex,
const std::vector<minikin::FontVariation>& axes)
- : minikin::MinikinFont(typeface->uniqueID())
- , mTypeface(std::move(typeface))
+ : mTypeface(std::move(typeface))
, mFontData(fontData)
, mFontSize(fontSize)
, mTtcIndex(ttcIndex)
@@ -125,22 +124,22 @@ const std::vector<minikin::FontVariation>& MinikinFontSkia::GetAxes() const {
std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation(
const std::vector<minikin::FontVariation>& variations) const {
- SkFontArguments params;
+ SkFontArguments args;
int ttcIndex;
std::unique_ptr<SkStreamAsset> stream(mTypeface->openStream(&ttcIndex));
LOG_ALWAYS_FATAL_IF(stream == nullptr, "openStream failed");
- params.setCollectionIndex(ttcIndex);
- std::vector<SkFontArguments::Axis> skAxes;
- skAxes.resize(variations.size());
+ args.setCollectionIndex(ttcIndex);
+ std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation;
+ skVariation.resize(variations.size());
for (size_t i = 0; i < variations.size(); i++) {
- skAxes[i].fTag = variations[i].axisTag;
- skAxes[i].fStyleValue = SkFloatToScalar(variations[i].value);
+ skVariation[i].axis = variations[i].axisTag;
+ skVariation[i].value = SkFloatToScalar(variations[i].value);
}
- params.setAxes(skAxes.data(), skAxes.size());
+ args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())});
sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
- sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), params));
+ sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), args));
return std::make_shared<MinikinFontSkia>(std::move(face), mFontData, mFontSize, mFilePath,
ttcIndex, variations);
diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h
index 298967689cd9..77a21428f36a 100644
--- a/libs/hwui/hwui/MinikinSkia.h
+++ b/libs/hwui/hwui/MinikinSkia.h
@@ -49,6 +49,8 @@ public:
void GetFontExtent(minikin::MinikinExtent* extent, const minikin::MinikinPaint& paint,
const minikin::FontFakery& fakery) const override;
+ const std::string& GetFontPath() const override { return mFilePath; }
+
SkTypeface* GetSkTypeface() const;
sk_sp<SkTypeface> RefSkTypeface() const;
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 5f6b53ac767f..b8029087cb4f 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -21,6 +21,7 @@
#include <log/log.h>
#include <minikin/MeasuredText.h>
+#include <minikin/Measurement.h>
#include "Paint.h"
#include "SkPathMeasure.h"
#include "Typeface.h"
@@ -69,6 +70,18 @@ minikin::Layout MinikinUtils::doLayout(const Paint* paint, minikin::Bidi bidiFla
}
}
+void MinikinUtils::getBounds(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface,
+ const uint16_t* buf, size_t bufSize, minikin::MinikinRect* out) {
+ minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface);
+
+ const minikin::U16StringPiece textBuf(buf, bufSize);
+ const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit();
+ const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
+
+ minikin::getBounds(textBuf, minikin::Range(0, textBuf.size()), bidiFlags, minikinPaint,
+ startHyphen, endHyphen, out);
+}
+
float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags,
const Typeface* typeface, const uint16_t* buf, size_t start,
size_t count, size_t bufSize, float* advances) {
diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h
index cbf409504675..a15803ad2dca 100644
--- a/libs/hwui/hwui/MinikinUtils.h
+++ b/libs/hwui/hwui/MinikinUtils.h
@@ -39,37 +39,40 @@ namespace android {
class MinikinUtils {
public:
- ANDROID_API static minikin::MinikinPaint prepareMinikinPaint(const Paint* paint,
+ static minikin::MinikinPaint prepareMinikinPaint(const Paint* paint,
const Typeface* typeface);
- ANDROID_API static minikin::Layout doLayout(const Paint* paint, minikin::Bidi bidiFlags,
+ static minikin::Layout doLayout(const Paint* paint, minikin::Bidi bidiFlags,
const Typeface* typeface, const uint16_t* buf,
size_t bufSize, size_t start, size_t count,
size_t contextStart, size_t contextCount,
minikin::MeasuredText* mt);
- ANDROID_API static float measureText(const Paint* paint, minikin::Bidi bidiFlags,
+ static void getBounds(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface,
+ const uint16_t* buf, size_t bufSize, minikin::MinikinRect* out);
+
+ static float measureText(const Paint* paint, minikin::Bidi bidiFlags,
const Typeface* typeface, const uint16_t* buf,
size_t start, size_t count, size_t bufSize,
float* advances);
- ANDROID_API static bool hasVariationSelector(const Typeface* typeface, uint32_t codepoint,
+ static bool hasVariationSelector(const Typeface* typeface, uint32_t codepoint,
uint32_t vs);
- ANDROID_API static float xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout);
+ static float xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout);
- ANDROID_API static float hOffsetForTextAlign(Paint* paint, const minikin::Layout& layout,
+ static float hOffsetForTextAlign(Paint* paint, const minikin::Layout& layout,
const SkPath& path);
// f is a functor of type void f(size_t start, size_t end);
template <typename F>
- ANDROID_API static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) {
+ static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) {
float saveSkewX = paint->getSkFont().getSkewX();
bool savefakeBold = paint->getSkFont().isEmbolden();
const minikin::MinikinFont* curFont = nullptr;
size_t start = 0;
size_t nGlyphs = layout.nGlyphs();
for (size_t i = 0; i < nGlyphs; i++) {
- const minikin::MinikinFont* nextFont = layout.getFont(i);
+ const minikin::MinikinFont* nextFont = layout.getFont(i)->typeface().get();
if (i > 0 && nextFont != curFont) {
SkFont* skfont = &paint->getSkFont();
MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start));
diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h
index 281ecd27d780..e75e9e7c6933 100644
--- a/libs/hwui/hwui/Paint.h
+++ b/libs/hwui/hwui/Paint.h
@@ -32,7 +32,7 @@
namespace android {
-class ANDROID_API Paint : public SkPaint {
+class Paint : public SkPaint {
public:
// Default values for underlined and strikethrough text,
// as defined by Skia in SkTextFormatParams.h.
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index ccc328c702db..03f1d62625f1 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -188,7 +188,7 @@ void Typeface::setRobotoTypefaceForTest() {
std::shared_ptr<minikin::MinikinFont> font = std::make_shared<MinikinFontSkia>(
std::move(typeface), data, st.st_size, kRobotoFont, 0,
std::vector<minikin::FontVariation>());
- std::vector<minikin::Font> fonts;
+ std::vector<std::shared_ptr<minikin::Font>> fonts;
fonts.push_back(minikin::Font::Builder(font).build());
std::shared_ptr<minikin::FontCollection> collection = std::make_shared<minikin::FontCollection>(
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index c0663a9bc699..eb9885a4436a 100755
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -19,12 +19,19 @@
#include <utils/Color.h>
#ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread
+#include <android-base/unique_fd.h>
+#include <android/binder_parcel.h>
+#include <android/binder_parcel_jni.h>
+#include <android/binder_parcel_platform.h>
+#include <android/binder_parcel_utils.h>
#include <private/android/AHardwareBufferHelpers.h>
-#include <binder/Parcel.h>
+#include <cutils/ashmem.h>
#include <dlfcn.h>
#include <renderthread/RenderProxy.h>
+#include <sys/mman.h>
#endif
+#include <inttypes.h>
#include <string.h>
#include <memory>
#include <string>
@@ -567,152 +574,296 @@ static void Bitmap_setHasMipMap(JNIEnv* env, jobject, jlong bitmapHandle,
///////////////////////////////////////////////////////////////////////////////
+// TODO: Move somewhere else
#ifdef __ANDROID__ // Layoutlib does not support parcel
-static struct parcel_offsets_t
-{
- jclass clazz;
- jfieldID mNativePtr;
-} gParcelOffsets;
-
-static Parcel* parcelForJavaObject(JNIEnv* env, jobject obj) {
- if (obj) {
- Parcel* p = (Parcel*)env->GetLongField(obj, gParcelOffsets.mNativePtr);
- if (p != NULL) {
- return p;
+
+class ScopedParcel {
+public:
+ explicit ScopedParcel(JNIEnv* env, jobject parcel) {
+ mParcel = AParcel_fromJavaParcel(env, parcel);
+ }
+
+ ~ScopedParcel() { AParcel_delete(mParcel); }
+
+ int32_t readInt32() {
+ int32_t temp = 0;
+ // TODO: This behavior-matches what android::Parcel does
+ // but this should probably be better
+ if (AParcel_readInt32(mParcel, &temp) != STATUS_OK) {
+ temp = 0;
}
- jniThrowException(env, "java/lang/IllegalStateException", "Parcel has been finalized!");
+ return temp;
+ }
+
+ uint32_t readUint32() {
+ uint32_t temp = 0;
+ // TODO: This behavior-matches what android::Parcel does
+ // but this should probably be better
+ if (AParcel_readUint32(mParcel, &temp) != STATUS_OK) {
+ temp = 0;
+ }
+ return temp;
+ }
+
+ void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); }
+
+ void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); }
+
+ bool allowFds() const { return AParcel_getAllowFds(mParcel); }
+
+ std::optional<sk_sp<SkData>> readData() {
+ struct Data {
+ void* ptr = nullptr;
+ size_t size = 0;
+ } data;
+ auto error = AParcel_readByteArray(mParcel, &data,
+ [](void* arrayData, int32_t length,
+ int8_t** outBuffer) -> bool {
+ Data* data = reinterpret_cast<Data*>(arrayData);
+ if (length > 0) {
+ data->ptr = sk_malloc_canfail(length);
+ if (!data->ptr) {
+ return false;
+ }
+ *outBuffer =
+ reinterpret_cast<int8_t*>(data->ptr);
+ data->size = length;
+ }
+ return true;
+ });
+ if (error != STATUS_OK || data.size <= 0) {
+ sk_free(data.ptr);
+ return std::nullopt;
+ } else {
+ return SkData::MakeFromMalloc(data.ptr, data.size);
+ }
+ }
+
+ void writeData(const std::optional<sk_sp<SkData>>& optData) {
+ if (optData) {
+ const auto& data = *optData;
+ AParcel_writeByteArray(mParcel, reinterpret_cast<const int8_t*>(data->data()),
+ data->size());
+ } else {
+ AParcel_writeByteArray(mParcel, nullptr, -1);
+ }
+ }
+
+ AParcel* get() { return mParcel; }
+
+private:
+ AParcel* mParcel;
+};
+
+enum class BlobType : int32_t {
+ IN_PLACE,
+ ASHMEM,
+};
+
+#define ON_ERROR_RETURN(X) \
+ if ((error = (X)) != STATUS_OK) return error
+
+template <typename T, typename U>
+static binder_status_t readBlob(AParcel* parcel, T inPlaceCallback, U ashmemCallback) {
+ binder_status_t error = STATUS_OK;
+ BlobType type;
+ static_assert(sizeof(BlobType) == sizeof(int32_t));
+ ON_ERROR_RETURN(AParcel_readInt32(parcel, (int32_t*)&type));
+ if (type == BlobType::IN_PLACE) {
+ struct Data {
+ std::unique_ptr<int8_t[]> ptr = nullptr;
+ int32_t size = 0;
+ } data;
+ ON_ERROR_RETURN(
+ AParcel_readByteArray(parcel, &data,
+ [](void* arrayData, int32_t length, int8_t** outBuffer) {
+ Data* data = reinterpret_cast<Data*>(arrayData);
+ if (length > 0) {
+ data->ptr = std::make_unique<int8_t[]>(length);
+ data->size = length;
+ *outBuffer = data->ptr.get();
+ }
+ return data->ptr != nullptr;
+ }));
+ inPlaceCallback(std::move(data.ptr), data.size);
+ return STATUS_OK;
+ } else if (type == BlobType::ASHMEM) {
+ int rawFd = -1;
+ int32_t size = 0;
+ ON_ERROR_RETURN(AParcel_readInt32(parcel, &size));
+ ON_ERROR_RETURN(AParcel_readParcelFileDescriptor(parcel, &rawFd));
+ android::base::unique_fd fd(rawFd);
+ ashmemCallback(std::move(fd), size);
+ return STATUS_OK;
+ } else {
+ // Although the above if/else was "exhaustive" guard against unknown types
+ return STATUS_UNKNOWN_ERROR;
}
- return NULL;
}
-#endif
+
+static constexpr size_t BLOB_INPLACE_LIMIT = 12 * 1024;
+// Fail fast if we can't use ashmem and the size exceeds this limit - the binder transaction
+// wouldn't go through, anyway
+// TODO: Can we get this from somewhere?
+static constexpr size_t BLOB_MAX_INPLACE_LIMIT = 1 * 1024 * 1024;
+static constexpr bool shouldUseAshmem(AParcel* parcel, int32_t size) {
+ return size > BLOB_INPLACE_LIMIT && AParcel_getAllowFds(parcel);
+}
+
+static binder_status_t writeBlobFromFd(AParcel* parcel, int32_t size, int fd) {
+ binder_status_t error = STATUS_OK;
+ ON_ERROR_RETURN(AParcel_writeInt32(parcel, static_cast<int32_t>(BlobType::ASHMEM)));
+ ON_ERROR_RETURN(AParcel_writeInt32(parcel, size));
+ ON_ERROR_RETURN(AParcel_writeParcelFileDescriptor(parcel, fd));
+ return STATUS_OK;
+}
+
+static binder_status_t writeBlob(AParcel* parcel, const int32_t size, const void* data, bool immutable) {
+ if (size <= 0 || data == nullptr) {
+ return STATUS_NOT_ENOUGH_DATA;
+ }
+ binder_status_t error = STATUS_OK;
+ if (shouldUseAshmem(parcel, size)) {
+ // Create new ashmem region with read/write priv
+ base::unique_fd fd(ashmem_create_region("bitmap", size));
+ if (fd.get() < 0) {
+ return STATUS_NO_MEMORY;
+ }
+
+ {
+ void* dest = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
+ if (dest == MAP_FAILED) {
+ return STATUS_NO_MEMORY;
+ }
+ memcpy(dest, data, size);
+ munmap(dest, size);
+ }
+
+ if (immutable && ashmem_set_prot_region(fd.get(), PROT_READ) < 0) {
+ return STATUS_UNKNOWN_ERROR;
+ }
+ // Workaround b/149851140 in AParcel_writeParcelFileDescriptor
+ int rawFd = fd.release();
+ error = writeBlobFromFd(parcel, size, rawFd);
+ close(rawFd);
+ return error;
+ } else {
+ if (size > BLOB_MAX_INPLACE_LIMIT) {
+ return STATUS_FAILED_TRANSACTION;
+ }
+ ON_ERROR_RETURN(AParcel_writeInt32(parcel, static_cast<int32_t>(BlobType::IN_PLACE)));
+ ON_ERROR_RETURN(AParcel_writeByteArray(parcel, static_cast<const int8_t*>(data), size));
+ return STATUS_OK;
+ }
+}
+
+#undef ON_ERROR_RETURN
+
+#endif // __ANDROID__ // Layoutlib does not support parcel
// This is the maximum possible size because the SkColorSpace must be
// representable (and therefore serializable) using a matrix and numerical
// transfer function. If we allow more color space representations in the
// framework, we may need to update this maximum size.
-static constexpr uint32_t kMaxColorSpaceSerializedBytes = 80;
+static constexpr size_t kMaxColorSpaceSerializedBytes = 80;
+
+static constexpr auto RuntimeException = "java/lang/RuntimeException";
+
+static bool validateImageInfo(const SkImageInfo& info, int32_t rowBytes) {
+ // TODO: Can we avoid making a SkBitmap for this?
+ return SkBitmap().setInfo(info, rowBytes);
+}
static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
#ifdef __ANDROID__ // Layoutlib does not support parcel
if (parcel == NULL) {
- SkDebugf("-------- unparcel parcel is NULL\n");
+ jniThrowNullPointerException(env, "parcel cannot be null");
return NULL;
}
- android::Parcel* p = parcelForJavaObject(env, parcel);
+ ScopedParcel p(env, parcel);
- const bool isMutable = p->readInt32() != 0;
- const SkColorType colorType = (SkColorType)p->readInt32();
- const SkAlphaType alphaType = (SkAlphaType)p->readInt32();
- const uint32_t colorSpaceSize = p->readUint32();
+ const bool isMutable = p.readInt32();
+ const SkColorType colorType = static_cast<SkColorType>(p.readInt32());
+ const SkAlphaType alphaType = static_cast<SkAlphaType>(p.readInt32());
sk_sp<SkColorSpace> colorSpace;
- if (colorSpaceSize > 0) {
- if (colorSpaceSize > kMaxColorSpaceSerializedBytes) {
+ const auto optColorSpaceData = p.readData();
+ if (optColorSpaceData) {
+ const auto& colorSpaceData = *optColorSpaceData;
+ if (colorSpaceData->size() > kMaxColorSpaceSerializedBytes) {
ALOGD("Bitmap_createFromParcel: Serialized SkColorSpace is larger than expected: "
- "%d bytes\n", colorSpaceSize);
+ "%zu bytes (max: %zu)\n",
+ colorSpaceData->size(), kMaxColorSpaceSerializedBytes);
}
- const void* data = p->readInplace(colorSpaceSize);
- if (data) {
- colorSpace = SkColorSpace::Deserialize(data, colorSpaceSize);
- } else {
- ALOGD("Bitmap_createFromParcel: Unable to read serialized SkColorSpace data\n");
- }
+ colorSpace = SkColorSpace::Deserialize(colorSpaceData->data(), colorSpaceData->size());
}
- const int width = p->readInt32();
- const int height = p->readInt32();
- const int rowBytes = p->readInt32();
- const int density = p->readInt32();
+ const int32_t width = p.readInt32();
+ const int32_t height = p.readInt32();
+ const int32_t rowBytes = p.readInt32();
+ const int32_t density = p.readInt32();
if (kN32_SkColorType != colorType &&
kRGBA_F16_SkColorType != colorType &&
kRGB_565_SkColorType != colorType &&
kARGB_4444_SkColorType != colorType &&
kAlpha_8_SkColorType != colorType) {
- SkDebugf("Bitmap_createFromParcel unknown colortype: %d\n", colorType);
+ jniThrowExceptionFmt(env, RuntimeException,
+ "Bitmap_createFromParcel unknown colortype: %d\n", colorType);
return NULL;
}
- std::unique_ptr<SkBitmap> bitmap(new SkBitmap);
- if (!bitmap->setInfo(SkImageInfo::Make(width, height, colorType, alphaType, colorSpace),
- rowBytes)) {
+ auto imageInfo = SkImageInfo::Make(width, height, colorType, alphaType, colorSpace);
+ size_t allocationSize = 0;
+ if (!validateImageInfo(imageInfo, rowBytes)) {
+ jniThrowRuntimeException(env, "Received bad SkImageInfo");
return NULL;
}
-
- // Read the bitmap blob.
- size_t size = bitmap->computeByteSize();
- android::Parcel::ReadableBlob blob;
- android::status_t status = p->readBlob(size, &blob);
- if (status) {
- doThrowRE(env, "Could not read bitmap blob.");
+ if (!Bitmap::computeAllocationSize(rowBytes, height, &allocationSize)) {
+ jniThrowExceptionFmt(env, RuntimeException,
+ "Received bad bitmap size: width=%d, height=%d, rowBytes=%d", width,
+ height, rowBytes);
return NULL;
}
-
- // Map the bitmap in place from the ashmem region if possible otherwise copy.
sk_sp<Bitmap> nativeBitmap;
- // If the blob is mutable we have ownership of the region and can always use it
- // If the blob is immutable _and_ we're immutable, we can then still use it
- if (blob.fd() >= 0 && (blob.isMutable() || !isMutable)) {
-#if DEBUG_PARCEL
- ALOGD("Bitmap.createFromParcel: mapped contents of bitmap from %s blob "
- "(fds %s)",
- blob.isMutable() ? "mutable" : "immutable",
- p->allowFds() ? "allowed" : "forbidden");
-#endif
- // Dup the file descriptor so we can keep a reference to it after the Parcel
- // is disposed.
- int dupFd = fcntl(blob.fd(), F_DUPFD_CLOEXEC, 0);
- if (dupFd < 0) {
- ALOGE("Error allocating dup fd. Error:%d", errno);
- blob.release();
- doThrowRE(env, "Could not allocate dup blob fd.");
- return NULL;
- }
-
- // Map the pixels in place and take ownership of the ashmem region. We must also respect the
- // rowBytes value already set on the bitmap instead of attempting to compute our own.
- nativeBitmap = Bitmap::createFrom(bitmap->info(), bitmap->rowBytes(), dupFd,
- const_cast<void*>(blob.data()), size, !isMutable);
- if (!nativeBitmap) {
- close(dupFd);
- blob.release();
- doThrowRE(env, "Could not allocate ashmem pixel ref.");
- return NULL;
- }
-
- // Clear the blob handle, don't release it.
- blob.clear();
- } else {
-#if DEBUG_PARCEL
- if (blob.fd() >= 0) {
- ALOGD("Bitmap.createFromParcel: copied contents of mutable bitmap "
- "from immutable blob (fds %s)",
- p->allowFds() ? "allowed" : "forbidden");
- } else {
- ALOGD("Bitmap.createFromParcel: copied contents from %s blob "
- "(fds %s)",
- blob.isMutable() ? "mutable" : "immutable",
- p->allowFds() ? "allowed" : "forbidden");
- }
-#endif
-
- // Copy the pixels into a new buffer.
- nativeBitmap = Bitmap::allocateHeapBitmap(bitmap.get());
- if (!nativeBitmap) {
- blob.release();
- doThrowRE(env, "Could not allocate java pixel ref.");
- return NULL;
- }
- memcpy(bitmap->getPixels(), blob.data(), size);
-
- // Release the blob handle.
- blob.release();
+ binder_status_t error = readBlob(
+ p.get(),
+ // In place callback
+ [&](std::unique_ptr<int8_t[]> buffer, int32_t size) {
+ nativeBitmap = Bitmap::allocateHeapBitmap(allocationSize, imageInfo, rowBytes);
+ if (nativeBitmap) {
+ memcpy(nativeBitmap->pixels(), buffer.get(), size);
+ }
+ },
+ // Ashmem callback
+ [&](android::base::unique_fd fd, int32_t size) {
+ int flags = PROT_READ;
+ if (isMutable) {
+ flags |= PROT_WRITE;
+ }
+ void* addr = mmap(nullptr, size, flags, MAP_SHARED, fd.get(), 0);
+ if (addr == MAP_FAILED) {
+ const int err = errno;
+ ALOGW("mmap failed, error %d (%s)", err, strerror(err));
+ return;
+ }
+ nativeBitmap =
+ Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable);
+ });
+ if (error != STATUS_OK) {
+ // TODO: Stringify the error, see signalExceptionForError in android_util_Binder.cpp
+ jniThrowExceptionFmt(env, RuntimeException, "Failed to read from Parcel, error=%d", error);
+ return nullptr;
+ }
+ if (!nativeBitmap) {
+ jniThrowRuntimeException(env, "Could not allocate java pixel ref.");
+ return nullptr;
}
- return createBitmap(env, nativeBitmap.release(),
- getPremulBitmapCreateFlags(isMutable), NULL, NULL, density);
+ return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr,
+ nullptr, density);
#else
- doThrowRE(env, "Cannot use parcels outside of Android");
+ jniThrowRuntimeException(env, "Cannot use parcels outside of Android");
return NULL;
#endif
}
@@ -725,48 +876,38 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
return JNI_FALSE;
}
- android::Parcel* p = parcelForJavaObject(env, parcel);
+ ScopedParcel p(env, parcel);
SkBitmap bitmap;
auto bitmapWrapper = reinterpret_cast<BitmapWrapper*>(bitmapHandle);
bitmapWrapper->getSkBitmap(&bitmap);
- p->writeInt32(!bitmap.isImmutable());
- p->writeInt32(bitmap.colorType());
- p->writeInt32(bitmap.alphaType());
+ p.writeInt32(!bitmap.isImmutable());
+ p.writeInt32(bitmap.colorType());
+ p.writeInt32(bitmap.alphaType());
SkColorSpace* colorSpace = bitmap.colorSpace();
if (colorSpace != nullptr) {
- sk_sp<SkData> data = colorSpace->serialize();
- size_t size = data->size();
- p->writeUint32(size);
- if (size > 0) {
- if (size > kMaxColorSpaceSerializedBytes) {
- ALOGD("Bitmap_writeToParcel: Serialized SkColorSpace is larger than expected: "
- "%zu bytes\n", size);
- }
-
- p->write(data->data(), size);
- }
+ p.writeData(colorSpace->serialize());
} else {
- p->writeUint32(0);
+ p.writeData(std::nullopt);
}
- p->writeInt32(bitmap.width());
- p->writeInt32(bitmap.height());
- p->writeInt32(bitmap.rowBytes());
- p->writeInt32(density);
+ p.writeInt32(bitmap.width());
+ p.writeInt32(bitmap.height());
+ p.writeInt32(bitmap.rowBytes());
+ p.writeInt32(density);
// Transfer the underlying ashmem region if we have one and it's immutable.
- android::status_t status;
+ binder_status_t status;
int fd = bitmapWrapper->bitmap().getAshmemFd();
- if (fd >= 0 && bitmap.isImmutable() && p->allowFds()) {
+ if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) {
#if DEBUG_PARCEL
ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as "
- "immutable blob (fds %s)",
- p->allowFds() ? "allowed" : "forbidden");
+ "immutable blob (fds %s)",
+ p.allowFds() ? "allowed" : "forbidden");
#endif
- status = p->writeDupImmutableBlobFileDescriptor(fd);
- if (status) {
+ status = writeBlobFromFd(p.get(), bitmapWrapper->bitmap().getAllocationByteCount(), fd);
+ if (status != STATUS_OK) {
doThrowRE(env, "Could not write bitmap blob file descriptor.");
return JNI_FALSE;
}
@@ -776,26 +917,15 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
// Copy the bitmap to a new blob.
#if DEBUG_PARCEL
ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)",
- p->allowFds() ? "allowed" : "forbidden");
+ p.allowFds() ? "allowed" : "forbidden");
#endif
- const bool mutableCopy = !bitmap.isImmutable();
size_t size = bitmap.computeByteSize();
- android::Parcel::WritableBlob blob;
- status = p->writeBlob(size, mutableCopy, &blob);
+ status = writeBlob(p.get(), size, bitmap.getPixels(), bitmap.isImmutable());
if (status) {
doThrowRE(env, "Could not copy bitmap to parcel blob.");
return JNI_FALSE;
}
-
- const void* pSrc = bitmap.getPixels();
- if (pSrc == NULL) {
- memset(blob.data(), 0, size);
- } else {
- memcpy(blob.data(), pSrc, size);
- }
-
- blob.release();
return JNI_TRUE;
#else
doThrowRE(env, "Cannot use parcels outside of Android");
@@ -1074,13 +1204,16 @@ static jobject Bitmap_wrapHardwareBufferBitmap(JNIEnv* env, jobject, jobject har
static jobject Bitmap_getHardwareBuffer(JNIEnv* env, jobject, jlong bitmapPtr) {
#ifdef __ANDROID__ // Layoutlib does not support graphic buffer
LocalScopedBitmap bitmapHandle(bitmapPtr);
- LOG_ALWAYS_FATAL_IF(!bitmapHandle->isHardware(),
+ if (!bitmapHandle->isHardware()) {
+ jniThrowException(env, "java/lang/IllegalStateException",
"Hardware config is only supported config in Bitmap_getHardwareBuffer");
+ return nullptr;
+ }
Bitmap& bitmap = bitmapHandle->bitmap();
return AHardwareBuffer_toHardwareBuffer(env, bitmap.hardwareBuffer());
#else
- return NULL;
+ return nullptr;
#endif
}
@@ -1091,6 +1224,14 @@ static jboolean Bitmap_isImmutable(CRITICAL_JNI_PARAMS_COMMA jlong bitmapHandle)
return bitmapHolder->bitmap().isImmutable() ? JNI_TRUE : JNI_FALSE;
}
+static jboolean Bitmap_isBackedByAshmem(CRITICAL_JNI_PARAMS_COMMA jlong bitmapHandle) {
+ LocalScopedBitmap bitmapHolder(bitmapHandle);
+ if (!bitmapHolder.valid()) return JNI_FALSE;
+
+ return bitmapHolder->bitmap().pixelStorageType() == PixelStorageType::Ashmem ? JNI_TRUE
+ : JNI_FALSE;
+}
+
static void Bitmap_setImmutable(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmapHolder(bitmapHandle);
if (!bitmapHolder.valid()) return;
@@ -1157,12 +1298,11 @@ static const JNINativeMethod gBitmapMethods[] = {
{ "nativeSetImmutable", "(J)V", (void*)Bitmap_setImmutable},
// ------------ @CriticalNative ----------------
- { "nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable}
+ { "nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable},
+ { "nativeIsBackedByAshmem", "(J)Z", (void*)Bitmap_isBackedByAshmem}
};
-const char* const kParcelPathName = "android/os/Parcel";
-
int register_android_graphics_Bitmap(JNIEnv* env)
{
gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap"));
@@ -1180,9 +1320,6 @@ int register_android_graphics_Bitmap(JNIEnv* env)
AHardwareBuffer_toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer");
LOG_ALWAYS_FATAL_IF(AHardwareBuffer_toHardwareBuffer == nullptr,
" Failed to find required symbol AHardwareBuffer_toHardwareBuffer!");
-
- gParcelOffsets.clazz = MakeGlobalRefOrDie(env, FindClassOrDie(env, kParcelPathName));
- gParcelOffsets.mNativePtr = GetFieldIDOrDie(env, gParcelOffsets.clazz, "mNativePtr", "J");
#endif
return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods,
NELEM(gBitmapMethods));
diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp
index e8e89d81bdb7..7d2583a2ac01 100644
--- a/libs/hwui/jni/BitmapFactory.cpp
+++ b/libs/hwui/jni/BitmapFactory.cpp
@@ -3,12 +3,11 @@
#include "BitmapFactory.h"
#include "CreateJavaOutputStreamAdaptor.h"
+#include "FrontBufferedStream.h"
#include "GraphicsJNI.h"
#include "MimeType.h"
#include "NinePatchPeeker.h"
#include "SkAndroidCodec.h"
-#include "SkBRDAllocator.h"
-#include "SkFrontBufferedStream.h"
#include "SkMath.h"
#include "SkPixelRef.h"
#include "SkStream.h"
@@ -510,8 +509,8 @@ static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteA
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));
if (stream.get()) {
- std::unique_ptr<SkStreamRewindable> bufferedStream(
- SkFrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded()));
+ std::unique_ptr<SkStreamRewindable> bufferedStream(skia::FrontBufferedStream::Make(
+ std::move(stream), SkCodec::MinBufferedBytesNeeded()));
SkASSERT(bufferedStream.get() != NULL);
bitmap = doDecode(env, std::move(bufferedStream), padding, options, inBitmapHandle,
colorSpaceHandle);
@@ -565,8 +564,8 @@ static jobject nativeDecodeFileDescriptor(JNIEnv* env, jobject clazz, jobject fi
// Use a buffered stream. Although an SkFILEStream can be rewound, this
// ensures that SkImageDecoder::Factory never rewinds beyond the
// current position of the file descriptor.
- std::unique_ptr<SkStreamRewindable> stream(SkFrontBufferedStream::Make(std::move(fileStream),
- SkCodec::MinBufferedBytesNeeded()));
+ std::unique_ptr<SkStreamRewindable> stream(skia::FrontBufferedStream::Make(
+ std::move(fileStream), SkCodec::MinBufferedBytesNeeded()));
return doDecode(env, std::move(stream), padding, bitmapFactoryOptions, inBitmapHandle,
colorSpaceHandle);
diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp
index 712351382d97..4cc05ef6f13b 100644
--- a/libs/hwui/jni/BitmapRegionDecoder.cpp
+++ b/libs/hwui/jni/BitmapRegionDecoder.cpp
@@ -22,8 +22,8 @@
#include "GraphicsJNI.h"
#include "Utils.h"
+#include "BitmapRegionDecoder.h"
#include "SkBitmap.h"
-#include "SkBitmapRegionDecoder.h"
#include "SkCodec.h"
#include "SkData.h"
#include "SkStream.h"
@@ -36,10 +36,8 @@
using namespace android;
-static jobject createBitmapRegionDecoder(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream) {
- std::unique_ptr<SkBitmapRegionDecoder> brd(
- SkBitmapRegionDecoder::Create(stream.release(),
- SkBitmapRegionDecoder::kAndroidCodec_Strategy));
+static jobject createBitmapRegionDecoder(JNIEnv* env, sk_sp<SkData> data) {
+ auto brd = skia::BitmapRegionDecoder::Make(std::move(data));
if (!brd) {
doThrowIOE(env, "Image format not supported");
return nullObjectReturn("CreateBitmapRegionDecoder returned null");
@@ -49,21 +47,13 @@ static jobject createBitmapRegionDecoder(JNIEnv* env, std::unique_ptr<SkStreamRe
}
static jobject nativeNewInstanceFromByteArray(JNIEnv* env, jobject, jbyteArray byteArray,
- jint offset, jint length, jboolean isShareable) {
- /* If isShareable we could decide to just wrap the java array and
- share it, but that means adding a globalref to the java array object
- For now we just always copy the array's data if isShareable.
- */
+ jint offset, jint length) {
AutoJavaByteArray ar(env, byteArray);
- std::unique_ptr<SkMemoryStream> stream(new SkMemoryStream(ar.ptr() + offset, length, true));
-
- // the decoder owns the stream.
- jobject brd = createBitmapRegionDecoder(env, std::move(stream));
- return brd;
+ return createBitmapRegionDecoder(env, SkData::MakeWithCopy(ar.ptr() + offset, length));
}
static jobject nativeNewInstanceFromFileDescriptor(JNIEnv* env, jobject clazz,
- jobject fileDescriptor, jboolean isShareable) {
+ jobject fileDescriptor) {
NPE_CHECK_RETURN_ZERO(env, fileDescriptor);
jint descriptor = jniGetFDFromFileDescriptor(env, fileDescriptor);
@@ -74,41 +64,28 @@ static jobject nativeNewInstanceFromFileDescriptor(JNIEnv* env, jobject clazz,
return nullObjectReturn("fstat return -1");
}
- sk_sp<SkData> data(SkData::MakeFromFD(descriptor));
- std::unique_ptr<SkMemoryStream> stream(new SkMemoryStream(std::move(data)));
-
- // the decoder owns the stream.
- jobject brd = createBitmapRegionDecoder(env, std::move(stream));
- return brd;
+ return createBitmapRegionDecoder(env, SkData::MakeFromFD(descriptor));
}
-static jobject nativeNewInstanceFromStream(JNIEnv* env, jobject clazz,
- jobject is, // InputStream
- jbyteArray storage, // byte[]
- jboolean isShareable) {
- jobject brd = NULL;
- // for now we don't allow shareable with java inputstreams
- std::unique_ptr<SkStreamRewindable> stream(CopyJavaInputStream(env, is, storage));
-
- if (stream) {
- // the decoder owns the stream.
- brd = createBitmapRegionDecoder(env, std::move(stream));
+static jobject nativeNewInstanceFromStream(JNIEnv* env, jobject clazz, jobject is, // InputStream
+ jbyteArray storage) { // byte[]
+ jobject brd = nullptr;
+ sk_sp<SkData> data = CopyJavaInputStream(env, is, storage);
+
+ if (data) {
+ brd = createBitmapRegionDecoder(env, std::move(data));
}
return brd;
}
-static jobject nativeNewInstanceFromAsset(JNIEnv* env, jobject clazz,
- jlong native_asset, // Asset
- jboolean isShareable) {
+static jobject nativeNewInstanceFromAsset(JNIEnv* env, jobject clazz, jlong native_asset) {
Asset* asset = reinterpret_cast<Asset*>(native_asset);
- std::unique_ptr<SkMemoryStream> stream(CopyAssetToStream(asset));
- if (NULL == stream) {
- return NULL;
+ sk_sp<SkData> data = CopyAssetToData(asset);
+ if (!data) {
+ return nullptr;
}
- // the decoder owns the stream.
- jobject brd = createBitmapRegionDecoder(env, std::move(stream));
- return brd;
+ return createBitmapRegionDecoder(env, data);
}
/*
@@ -158,7 +135,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
recycledBytes = recycledBitmap->getAllocationByteCount();
}
- SkBitmapRegionDecoder* brd = reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
SkColorType decodeColorType = brd->computeOutputColorType(colorType);
if (decodeColorType == kRGBA_F16_SkColorType && isHardware &&
!uirenderer::HardwareBitmapUploader::hasFP16Support()) {
@@ -166,7 +143,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
}
// Set up the pixel allocator
- SkBRDAllocator* allocator = nullptr;
+ skia::BRDAllocator* allocator = nullptr;
RecyclingClippingPixelAllocator recycleAlloc(recycledBitmap, recycledBytes);
HeapAllocator heapAlloc;
if (javaBitmap) {
@@ -230,20 +207,17 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
}
static jint nativeGetHeight(JNIEnv* env, jobject, jlong brdHandle) {
- SkBitmapRegionDecoder* brd =
- reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
return static_cast<jint>(brd->height());
}
static jint nativeGetWidth(JNIEnv* env, jobject, jlong brdHandle) {
- SkBitmapRegionDecoder* brd =
- reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
return static_cast<jint>(brd->width());
}
static void nativeClean(JNIEnv* env, jobject, jlong brdHandle) {
- SkBitmapRegionDecoder* brd =
- reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
delete brd;
}
@@ -261,22 +235,22 @@ static const JNINativeMethod gBitmapRegionDecoderMethods[] = {
{ "nativeClean", "(J)V", (void*)nativeClean},
{ "nativeNewInstance",
- "([BIIZ)Landroid/graphics/BitmapRegionDecoder;",
+ "([BII)Landroid/graphics/BitmapRegionDecoder;",
(void*)nativeNewInstanceFromByteArray
},
{ "nativeNewInstance",
- "(Ljava/io/InputStream;[BZ)Landroid/graphics/BitmapRegionDecoder;",
+ "(Ljava/io/InputStream;[B)Landroid/graphics/BitmapRegionDecoder;",
(void*)nativeNewInstanceFromStream
},
{ "nativeNewInstance",
- "(Ljava/io/FileDescriptor;Z)Landroid/graphics/BitmapRegionDecoder;",
+ "(Ljava/io/FileDescriptor;)Landroid/graphics/BitmapRegionDecoder;",
(void*)nativeNewInstanceFromFileDescriptor
},
{ "nativeNewInstance",
- "(JZ)Landroid/graphics/BitmapRegionDecoder;",
+ "(J)Landroid/graphics/BitmapRegionDecoder;",
(void*)nativeNewInstanceFromAsset
},
};
diff --git a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp
index f1c6b29204b2..785a5dc995ab 100644
--- a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp
+++ b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp
@@ -177,8 +177,12 @@ SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray s
return JavaInputStreamAdaptor::Create(env, stream, storage, swallowExceptions);
}
-static SkMemoryStream* adaptor_to_mem_stream(SkStream* stream) {
- SkASSERT(stream != NULL);
+sk_sp<SkData> CopyJavaInputStream(JNIEnv* env, jobject inputStream, jbyteArray storage) {
+ std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, inputStream, storage));
+ if (!stream) {
+ return nullptr;
+ }
+
size_t bufferSize = 4096;
size_t streamLen = 0;
size_t len;
@@ -194,18 +198,7 @@ static SkMemoryStream* adaptor_to_mem_stream(SkStream* stream) {
}
data = (char*)sk_realloc_throw(data, streamLen);
- SkMemoryStream* streamMem = new SkMemoryStream();
- streamMem->setMemoryOwned(data, streamLen);
- return streamMem;
-}
-
-SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream,
- jbyteArray storage) {
- std::unique_ptr<SkStream> adaptor(CreateJavaInputStreamAdaptor(env, stream, storage));
- if (NULL == adaptor.get()) {
- return NULL;
- }
- return adaptor_to_mem_stream(adaptor.get());
+ return SkData::MakeFromMalloc(data, streamLen);
}
///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h
index 849418da01a1..bae40f1e8d2f 100644
--- a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h
+++ b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h
@@ -2,6 +2,7 @@
#define _ANDROID_GRAPHICS_CREATE_JAVA_OUTPUT_STREAM_ADAPTOR_H_
#include "jni.h"
+#include "SkData.h"
class SkMemoryStream;
class SkStream;
@@ -27,15 +28,14 @@ SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray s
bool swallowExceptions = true);
/**
- * Copy a Java InputStream. The result will be rewindable.
+ * Copy a Java InputStream to an SkData.
* @param env JNIEnv object.
* @param stream Pointer to Java InputStream.
* @param storage Java byte array for retrieving data from the
* Java InputStream.
- * @return SkStreamRewindable The data in stream will be copied
- * to a new SkStreamRewindable.
+ * @return SkData containing the stream's data.
*/
-SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream, jbyteArray storage);
+sk_sp<SkData> CopyJavaInputStream(JNIEnv* env, jobject stream, jbyteArray storage);
SkWStream* CreateJavaOutputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage);
diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp
index a2fef1e19328..2e85840cad99 100644
--- a/libs/hwui/jni/FontFamily.cpp
+++ b/libs/hwui/jni/FontFamily.cpp
@@ -42,7 +42,7 @@ struct NativeFamilyBuilder {
: langId(langId), variant(static_cast<minikin::FamilyVariant>(variant)) {}
uint32_t langId;
minikin::FamilyVariant variant;
- std::vector<minikin::Font> fonts;
+ std::vector<std::shared_ptr<minikin::Font>> fonts;
std::vector<minikin::FontVariation> axes;
};
@@ -104,21 +104,21 @@ static jlong FontFamily_getFamilyReleaseFunc(CRITICAL_JNI_PARAMS) {
static bool addSkTypeface(NativeFamilyBuilder* builder, sk_sp<SkData>&& data, int ttcIndex,
jint weight, jint italic) {
- FatVector<SkFontArguments::Axis, 2> skiaAxes;
+ FatVector<SkFontArguments::VariationPosition::Coordinate, 2> skVariation;
for (const auto& axis : builder->axes) {
- skiaAxes.emplace_back(SkFontArguments::Axis{axis.axisTag, axis.value});
+ skVariation.push_back({axis.axisTag, axis.value});
}
const size_t fontSize = data->size();
const void* fontPtr = data->data();
std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(std::move(data)));
- SkFontArguments params;
- params.setCollectionIndex(ttcIndex);
- params.setAxes(skiaAxes.data(), skiaAxes.size());
+ SkFontArguments args;
+ args.setCollectionIndex(ttcIndex);
+ args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())});
sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
- sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), params));
+ sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args));
if (face == NULL) {
ALOGE("addFont failed to create font, invalid request");
builder->axes.clear();
diff --git a/libs/hwui/jni/FontUtils.h b/libs/hwui/jni/FontUtils.h
index b36b4e60e33a..ba4e56e4c7f7 100644
--- a/libs/hwui/jni/FontUtils.h
+++ b/libs/hwui/jni/FontUtils.h
@@ -19,6 +19,7 @@
#include <jni.h>
#include <memory>
+#include <utility>
#include <minikin/Font.h>
@@ -34,8 +35,8 @@ struct FontFamilyWrapper {
};
struct FontWrapper {
- FontWrapper(minikin::Font&& font) : font(std::move(font)) {}
- minikin::Font font;
+ explicit FontWrapper(std::shared_ptr<minikin::Font>&& font) : font(font) {}
+ std::shared_ptr<minikin::Font> font;
};
// Utility wrapper for java.util.List
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index f76ecb4c9c8a..77f46beb2100 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -9,6 +9,7 @@
#include "GraphicsJNI.h"
#include "SkCanvas.h"
+#include "SkFontMetrics.h"
#include "SkMath.h"
#include "SkRegion.h"
#include <cutils/ashmem.h>
@@ -228,6 +229,20 @@ static jfieldID gColorSpace_Named_LinearExtendedSRGBFieldID;
static jclass gTransferParameters_class;
static jmethodID gTransferParameters_constructorMethodID;
+static jclass gFontMetrics_class;
+static jfieldID gFontMetrics_top;
+static jfieldID gFontMetrics_ascent;
+static jfieldID gFontMetrics_descent;
+static jfieldID gFontMetrics_bottom;
+static jfieldID gFontMetrics_leading;
+
+static jclass gFontMetricsInt_class;
+static jfieldID gFontMetricsInt_top;
+static jfieldID gFontMetricsInt_ascent;
+static jfieldID gFontMetricsInt_descent;
+static jfieldID gFontMetricsInt_bottom;
+static jfieldID gFontMetricsInt_leading;
+
///////////////////////////////////////////////////////////////////////////////
void GraphicsJNI::get_jrect(JNIEnv* env, jobject obj, int* L, int* T, int* R, int* B)
@@ -468,9 +483,35 @@ SkRegion* GraphicsJNI::getNativeRegion(JNIEnv* env, jobject region)
return r;
}
+void GraphicsJNI::set_metrics(JNIEnv* env, jobject metrics, const SkFontMetrics& skmetrics) {
+ if (metrics == nullptr) return;
+ SkASSERT(env->IsInstanceOf(metrics, gFontMetrics_class));
+ env->SetFloatField(metrics, gFontMetrics_top, SkScalarToFloat(skmetrics.fTop));
+ env->SetFloatField(metrics, gFontMetrics_ascent, SkScalarToFloat(skmetrics.fAscent));
+ env->SetFloatField(metrics, gFontMetrics_descent, SkScalarToFloat(skmetrics.fDescent));
+ env->SetFloatField(metrics, gFontMetrics_bottom, SkScalarToFloat(skmetrics.fBottom));
+ env->SetFloatField(metrics, gFontMetrics_leading, SkScalarToFloat(skmetrics.fLeading));
+}
+
+int GraphicsJNI::set_metrics_int(JNIEnv* env, jobject metrics, const SkFontMetrics& skmetrics) {
+ int ascent = SkScalarRoundToInt(skmetrics.fAscent);
+ int descent = SkScalarRoundToInt(skmetrics.fDescent);
+ int leading = SkScalarRoundToInt(skmetrics.fLeading);
+
+ if (metrics) {
+ SkASSERT(env->IsInstanceOf(metrics, gFontMetricsInt_class));
+ env->SetIntField(metrics, gFontMetricsInt_top, SkScalarFloorToInt(skmetrics.fTop));
+ env->SetIntField(metrics, gFontMetricsInt_ascent, ascent);
+ env->SetIntField(metrics, gFontMetricsInt_descent, descent);
+ env->SetIntField(metrics, gFontMetricsInt_bottom, SkScalarCeilToInt(skmetrics.fBottom));
+ env->SetIntField(metrics, gFontMetricsInt_leading, leading);
+ }
+ return descent - ascent + leading;
+}
+
///////////////////////////////////////////////////////////////////////////////////////////
-jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, SkBitmapRegionDecoder* bitmap)
+jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, skia::BitmapRegionDecoder* bitmap)
{
ALOG_ASSERT(bitmap != NULL);
@@ -764,5 +805,23 @@ int register_android_graphics_Graphics(JNIEnv* env)
gTransferParameters_constructorMethodID = GetMethodIDOrDie(env, gTransferParameters_class,
"<init>", "(DDDDDDD)V");
+ gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics");
+ gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class);
+
+ gFontMetrics_top = GetFieldIDOrDie(env, gFontMetrics_class, "top", "F");
+ gFontMetrics_ascent = GetFieldIDOrDie(env, gFontMetrics_class, "ascent", "F");
+ gFontMetrics_descent = GetFieldIDOrDie(env, gFontMetrics_class, "descent", "F");
+ gFontMetrics_bottom = GetFieldIDOrDie(env, gFontMetrics_class, "bottom", "F");
+ gFontMetrics_leading = GetFieldIDOrDie(env, gFontMetrics_class, "leading", "F");
+
+ gFontMetricsInt_class = FindClassOrDie(env, "android/graphics/Paint$FontMetricsInt");
+ gFontMetricsInt_class = MakeGlobalRefOrDie(env, gFontMetricsInt_class);
+
+ gFontMetricsInt_top = GetFieldIDOrDie(env, gFontMetricsInt_class, "top", "I");
+ gFontMetricsInt_ascent = GetFieldIDOrDie(env, gFontMetricsInt_class, "ascent", "I");
+ gFontMetricsInt_descent = GetFieldIDOrDie(env, gFontMetricsInt_class, "descent", "I");
+ gFontMetricsInt_bottom = GetFieldIDOrDie(env, gFontMetricsInt_class, "bottom", "I");
+ gFontMetricsInt_leading = GetFieldIDOrDie(env, gFontMetricsInt_class, "leading", "I");
+
return 0;
}
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index b58a740a4c27..541d5a53de07 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -4,8 +4,8 @@
#include <cutils/compiler.h>
#include "Bitmap.h"
+#include "BRDAllocator.h"
#include "SkBitmap.h"
-#include "SkBRDAllocator.h"
#include "SkCodec.h"
#include "SkPixelRef.h"
#include "SkMallocPixelRef.h"
@@ -17,10 +17,13 @@
#include "graphics_jni_helpers.h"
-class SkBitmapRegionDecoder;
class SkCanvas;
+struct SkFontMetrics;
namespace android {
+namespace skia {
+ class BitmapRegionDecoder;
+}
class Paint;
struct Typeface;
}
@@ -83,6 +86,17 @@ public:
bool* isHardware);
static SkRegion* getNativeRegion(JNIEnv*, jobject region);
+ /**
+ * Set SkFontMetrics to Java Paint.FontMetrics.
+ * Do nothing if metrics is nullptr.
+ */
+ static void set_metrics(JNIEnv*, jobject metrics, const SkFontMetrics& skmetrics);
+ /**
+ * Set SkFontMetrics to Java Paint.FontMetricsInt and return recommended interline space.
+ * Do nothing if metrics is nullptr.
+ */
+ static int set_metrics_int(JNIEnv*, jobject metrics, const SkFontMetrics& skmetrics);
+
/*
* LegacyBitmapConfig is the old enum in Skia that matched the enum int values
* in Bitmap.Config. Skia no longer supports this config, but has replaced it
@@ -103,7 +117,8 @@ public:
static jobject createRegion(JNIEnv* env, SkRegion* region);
- static jobject createBitmapRegionDecoder(JNIEnv* env, SkBitmapRegionDecoder* bitmap);
+ static jobject createBitmapRegionDecoder(JNIEnv* env,
+ android::skia::BitmapRegionDecoder* bitmap);
/**
* Given a bitmap we natively allocate a memory block to store the contents
@@ -154,7 +169,7 @@ private:
static JavaVM* mJavaVM;
};
-class HeapAllocator : public SkBRDAllocator {
+class HeapAllocator : public android::skia::BRDAllocator {
public:
HeapAllocator() { };
~HeapAllocator() { };
@@ -181,7 +196,7 @@ private:
* the decoded output to fit in the recycled bitmap if necessary.
* This allocator implements that behavior.
*
- * Skia's SkBitmapRegionDecoder expects the memory that
+ * Skia's BitmapRegionDecoder expects the memory that
* is allocated to be large enough to decode the entire region
* that is requested. It will decode directly into the memory
* that is provided.
@@ -200,7 +215,7 @@ private:
* reuse it again, given that it still may be in use from our
* first allocation.
*/
-class RecyclingClippingPixelAllocator : public SkBRDAllocator {
+class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator {
public:
RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp
index c8c3d3d5b078..da91d46b0738 100644
--- a/libs/hwui/jni/ImageDecoder.cpp
+++ b/libs/hwui/jni/ImageDecoder.cpp
@@ -27,9 +27,9 @@
#include <hwui/ImageDecoder.h>
#include <HardwareBitmapUploader.h>
+#include <FrontBufferedStream.h>
#include <SkAndroidCodec.h>
#include <SkEncodedImageFormat.h>
-#include <SkFrontBufferedStream.h>
#include <SkStream.h>
#include <androidfw/Asset.h>
@@ -194,8 +194,7 @@ static jobject ImageDecoder_nCreateInputStream(JNIEnv* env, jobject /*clazz*/,
}
std::unique_ptr<SkStream> bufferedStream(
- SkFrontBufferedStream::Make(std::move(stream),
- SkCodec::MinBufferedBytesNeeded()));
+ skia::FrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded()));
return native_create(env, std::move(bufferedStream), source, preferAnimation);
}
diff --git a/libs/hwui/jni/Movie.cpp b/libs/hwui/jni/Movie.cpp
index ede0ca8cda5b..bb8c99a73edf 100644
--- a/libs/hwui/jni/Movie.cpp
+++ b/libs/hwui/jni/Movie.cpp
@@ -1,7 +1,7 @@
#include "CreateJavaOutputStreamAdaptor.h"
+#include "FrontBufferedStream.h"
#include "GraphicsJNI.h"
#include <nativehelper/ScopedLocalRef.h>
-#include "SkFrontBufferedStream.h"
#include "Movie.h"
#include "SkStream.h"
#include "SkUtils.h"
@@ -100,10 +100,8 @@ static jobject movie_decodeStream(JNIEnv* env, jobject clazz, jobject istream) {
// Need to buffer enough input to be able to rewind as much as might be read by a decoder
// trying to determine the stream's format. The only decoder for movies is GIF, which
// will only read 6.
- // FIXME: Get this number from SkImageDecoder
- // bufferedStream takes ownership of strm
- std::unique_ptr<SkStreamRewindable> bufferedStream(SkFrontBufferedStream::Make(
- std::unique_ptr<SkStream>(strm), 6));
+ std::unique_ptr<SkStreamRewindable> bufferedStream(
+ android::skia::FrontBufferedStream::Make(std::unique_ptr<SkStream>(strm), 6));
SkASSERT(bufferedStream.get() != NULL);
Movie* moov = Movie::DecodeStream(bufferedStream.get());
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index df8635a8fe5a..3c86b28262b0 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -56,20 +56,6 @@
namespace android {
-struct JMetricsID {
- jfieldID top;
- jfieldID ascent;
- jfieldID descent;
- jfieldID bottom;
- jfieldID leading;
-};
-
-static jclass gFontMetrics_class;
-static JMetricsID gFontMetrics_fieldID;
-
-static jclass gFontMetricsInt_class;
-static JMetricsID gFontMetricsInt_fieldID;
-
static void getPosTextPath(const SkFont& font, const uint16_t glyphs[], int count,
const SkPoint pos[], SkPath* dst) {
dst->reset();
@@ -353,18 +339,13 @@ namespace PaintGlue {
}
static void doTextBounds(JNIEnv* env, const jchar* text, int count, jobject bounds,
- const Paint& paint, const Typeface* typeface, jint bidiFlags) {
+ const Paint& paint, const Typeface* typeface, jint bidiFlagsInt) {
SkRect r;
SkIRect ir;
- minikin::Layout layout = MinikinUtils::doLayout(&paint,
- static_cast<minikin::Bidi>(bidiFlags), typeface,
- text, count, // text buffer
- 0, count, // draw range
- 0, count, // context range
- nullptr);
minikin::MinikinRect rect;
- layout.getBounds(&rect);
+ minikin::Bidi bidiFlags = static_cast<minikin::Bidi>(bidiFlagsInt);
+ MinikinUtils::getBounds(&paint, bidiFlags, typeface, text, count, &rect);
r.fLeft = rect.mLeft;
r.fTop = rect.mTop;
r.fRight = rect.mRight;
@@ -615,35 +596,14 @@ namespace PaintGlue {
static jfloat getFontMetrics(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) {
SkFontMetrics metrics;
SkScalar spacing = getMetricsInternal(paintHandle, &metrics);
-
- if (metricsObj) {
- SkASSERT(env->IsInstanceOf(metricsObj, gFontMetrics_class));
- env->SetFloatField(metricsObj, gFontMetrics_fieldID.top, SkScalarToFloat(metrics.fTop));
- env->SetFloatField(metricsObj, gFontMetrics_fieldID.ascent, SkScalarToFloat(metrics.fAscent));
- env->SetFloatField(metricsObj, gFontMetrics_fieldID.descent, SkScalarToFloat(metrics.fDescent));
- env->SetFloatField(metricsObj, gFontMetrics_fieldID.bottom, SkScalarToFloat(metrics.fBottom));
- env->SetFloatField(metricsObj, gFontMetrics_fieldID.leading, SkScalarToFloat(metrics.fLeading));
- }
+ GraphicsJNI::set_metrics(env, metricsObj, metrics);
return SkScalarToFloat(spacing);
}
static jint getFontMetricsInt(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) {
SkFontMetrics metrics;
-
getMetricsInternal(paintHandle, &metrics);
- int ascent = SkScalarRoundToInt(metrics.fAscent);
- int descent = SkScalarRoundToInt(metrics.fDescent);
- int leading = SkScalarRoundToInt(metrics.fLeading);
-
- if (metricsObj) {
- SkASSERT(env->IsInstanceOf(metricsObj, gFontMetricsInt_class));
- env->SetIntField(metricsObj, gFontMetricsInt_fieldID.top, SkScalarFloorToInt(metrics.fTop));
- env->SetIntField(metricsObj, gFontMetricsInt_fieldID.ascent, ascent);
- env->SetIntField(metricsObj, gFontMetricsInt_fieldID.descent, descent);
- env->SetIntField(metricsObj, gFontMetricsInt_fieldID.bottom, SkScalarCeilToInt(metrics.fBottom));
- env->SetIntField(metricsObj, gFontMetricsInt_fieldID.leading, leading);
- }
- return descent - ascent + leading;
+ return GraphicsJNI::set_metrics_int(env, metricsObj, metrics);
}
@@ -1135,24 +1095,6 @@ static const JNINativeMethod methods[] = {
};
int register_android_graphics_Paint(JNIEnv* env) {
- gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics");
- gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class);
-
- gFontMetrics_fieldID.top = GetFieldIDOrDie(env, gFontMetrics_class, "top", "F");
- gFontMetrics_fieldID.ascent = GetFieldIDOrDie(env, gFontMetrics_class, "ascent", "F");
- gFontMetrics_fieldID.descent = GetFieldIDOrDie(env, gFontMetrics_class, "descent", "F");
- gFontMetrics_fieldID.bottom = GetFieldIDOrDie(env, gFontMetrics_class, "bottom", "F");
- gFontMetrics_fieldID.leading = GetFieldIDOrDie(env, gFontMetrics_class, "leading", "F");
-
- gFontMetricsInt_class = FindClassOrDie(env, "android/graphics/Paint$FontMetricsInt");
- gFontMetricsInt_class = MakeGlobalRefOrDie(env, gFontMetricsInt_class);
-
- gFontMetricsInt_fieldID.top = GetFieldIDOrDie(env, gFontMetricsInt_class, "top", "I");
- gFontMetricsInt_fieldID.ascent = GetFieldIDOrDie(env, gFontMetricsInt_class, "ascent", "I");
- gFontMetricsInt_fieldID.descent = GetFieldIDOrDie(env, gFontMetricsInt_class, "descent", "I");
- gFontMetricsInt_fieldID.bottom = GetFieldIDOrDie(env, gFontMetricsInt_class, "bottom", "I");
- gFontMetricsInt_fieldID.leading = GetFieldIDOrDie(env, gFontMetricsInt_class, "leading", "I");
-
return RegisterMethodsOrDie(env, "android/graphics/Paint", methods, NELEM(methods));
}
diff --git a/libs/hwui/jni/Picture.cpp b/libs/hwui/jni/Picture.cpp
index d1b952130e88..8e4203c0b115 100644
--- a/libs/hwui/jni/Picture.cpp
+++ b/libs/hwui/jni/Picture.cpp
@@ -111,7 +111,7 @@ sk_sp<SkPicture> Picture::makePartialCopy() const {
SkPictureRecorder reRecorder;
- SkCanvas* canvas = reRecorder.beginRecording(mWidth, mHeight, NULL, 0);
+ SkCanvas* canvas = reRecorder.beginRecording(mWidth, mHeight);
mRecorder->partialReplay(canvas);
return reRecorder.finishRecordingAsPicture();
}
diff --git a/libs/hwui/jni/RenderEffect.cpp b/libs/hwui/jni/RenderEffect.cpp
new file mode 100644
index 000000000000..0ebd0ca720d8
--- /dev/null
+++ b/libs/hwui/jni/RenderEffect.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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 "Bitmap.h"
+#include "GraphicsJNI.h"
+#include "SkImageFilter.h"
+#include "SkImageFilters.h"
+#include "graphics_jni_helpers.h"
+#include "utils/Blur.h"
+#include <utils/Log.h>
+
+using namespace android::uirenderer;
+
+static jlong createOffsetEffect(
+ JNIEnv* env,
+ jobject,
+ jfloat offsetX,
+ jfloat offsetY,
+ jlong inputFilterHandle
+) {
+ auto* inputFilter = reinterpret_cast<const SkImageFilter*>(inputFilterHandle);
+ sk_sp<SkImageFilter> offset = SkImageFilters::Offset(offsetX, offsetY, sk_ref_sp(inputFilter));
+ return reinterpret_cast<jlong>(offset.release());
+}
+
+static jlong createBlurEffect(JNIEnv* env , jobject, jfloat radiusX,
+ jfloat radiusY, jlong inputFilterHandle, jint edgeTreatment) {
+ auto* inputImageFilter = reinterpret_cast<SkImageFilter*>(inputFilterHandle);
+ sk_sp<SkImageFilter> blurFilter =
+ SkImageFilters::Blur(
+ Blur::convertRadiusToSigma(radiusX),
+ Blur::convertRadiusToSigma(radiusY),
+ static_cast<SkTileMode>(edgeTreatment),
+ sk_ref_sp(inputImageFilter),
+ nullptr);
+ return reinterpret_cast<jlong>(blurFilter.release());
+}
+
+static void RenderEffect_safeUnref(SkImageFilter* filter) {
+ SkSafeUnref(filter);
+}
+
+static jlong getRenderEffectFinalizer(JNIEnv*, jobject) {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&RenderEffect_safeUnref));
+}
+
+static const JNINativeMethod gRenderEffectMethods[] = {
+ {"nativeGetFinalizer", "()J", (void*)getRenderEffectFinalizer},
+ {"nativeCreateOffsetEffect", "(FFJ)J", (void*)createOffsetEffect},
+ {"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect}
+};
+
+int register_android_graphics_RenderEffect(JNIEnv* env) {
+ android::RegisterMethodsOrDie(env, "android/graphics/RenderEffect",
+ gRenderEffectMethods, NELEM(gRenderEffectMethods));
+ return 0;
+} \ No newline at end of file
diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp
index 0f6837640524..45795ff255aa 100644
--- a/libs/hwui/jni/Shader.cpp
+++ b/libs/hwui/jni/Shader.cpp
@@ -133,11 +133,25 @@ static jlong LinearGradient_create(JNIEnv* env, jobject, jlong matrixPtr,
///////////////////////////////////////////////////////////////////////////////////////////////
-static jlong RadialGradient_create(JNIEnv* env, jobject, jlong matrixPtr, jfloat x, jfloat y,
- jfloat radius, jlongArray colorArray, jfloatArray posArray, jint tileMode,
+static jlong RadialGradient_create(JNIEnv* env,
+ jobject,
+ jlong matrixPtr,
+ jfloat startX,
+ jfloat startY,
+ jfloat startRadius,
+ jfloat endX,
+ jfloat endY,
+ jfloat endRadius,
+ jlongArray colorArray,
+ jfloatArray posArray,
+ jint tileMode,
jlong colorSpaceHandle) {
- SkPoint center;
- center.set(x, y);
+
+ SkPoint start;
+ start.set(startX, startY);
+
+ SkPoint end;
+ end.set(endX, endY);
std::vector<SkColor4f> colors = convertColorLongs(env, colorArray);
@@ -148,11 +162,17 @@ static jlong RadialGradient_create(JNIEnv* env, jobject, jlong matrixPtr, jfloat
#error Need to convert float array to SkScalar array before calling the following function.
#endif
- sk_sp<SkShader> shader = SkGradientShader::MakeRadial(center, radius, &colors[0],
- GraphicsJNI::getNativeColorSpace(colorSpaceHandle), pos, colors.size(),
- static_cast<SkTileMode>(tileMode), sGradientShaderFlags, nullptr);
+ auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpaceHandle);
+ auto skTileMode = static_cast<SkTileMode>(tileMode);
+ sk_sp<SkShader> shader = SkGradientShader::MakeTwoPointConical(start, startRadius, end,
+ endRadius, &colors[0], std::move(colorSpace), pos, colors.size(), skTileMode,
+ sGradientShaderFlags, nullptr);
ThrowIAE_IfNull(env, shader);
+ // Explicitly create a new shader with the specified matrix to match existing behavior.
+ // Passing in the matrix in the instantiation above can throw exceptions for non-invertible
+ // matrices. However, makeWithLocalMatrix will still allow for the shader to be created
+ // and skia handles null-shaders internally (i.e. is ignored)
const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr);
if (matrix) {
shader = shader->makeWithLocalMatrix(*matrix);
@@ -211,14 +231,26 @@ static jlong ComposeShader_create(JNIEnv* env, jobject o, jlong matrixPtr,
///////////////////////////////////////////////////////////////////////////////////////////////
static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderFactory, jlong matrixPtr,
- jbyteArray inputs, jlong colorSpaceHandle, jboolean isOpaque) {
+ jbyteArray inputs, jlongArray inputShaders, jlong colorSpaceHandle, jboolean isOpaque) {
SkRuntimeEffect* effect = reinterpret_cast<SkRuntimeEffect*>(shaderFactory);
AutoJavaByteArray arInputs(env, inputs);
+ std::vector<sk_sp<SkShader>> shaderVector;
+ if (inputShaders) {
+ jsize shaderCount = env->GetArrayLength(inputShaders);
+ shaderVector.resize(shaderCount);
+ jlong* arrayPtr = env->GetLongArrayElements(inputShaders, NULL);
+ for (int i = 0; i < shaderCount; i++) {
+ shaderVector[i] = sk_ref_sp(reinterpret_cast<SkShader*>(arrayPtr[i]));
+ }
+ env->ReleaseLongArrayElements(inputShaders, arrayPtr, 0);
+ }
+
sk_sp<SkData> fData;
fData = SkData::MakeWithCopy(arInputs.ptr(), arInputs.length());
const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr);
- sk_sp<SkShader> shader = effect->makeShader(fData, nullptr, 0, matrix, isOpaque == JNI_TRUE);
+ sk_sp<SkShader> shader = effect->makeShader(fData, shaderVector.data(), shaderVector.size(),
+ matrix, isOpaque == JNI_TRUE);
ThrowIAE_IfNull(env, shader);
return reinterpret_cast<jlong>(shader.release());
@@ -228,9 +260,12 @@ static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderFactory, jlo
static jlong RuntimeShader_createShaderFactory(JNIEnv* env, jobject, jstring sksl) {
ScopedUtfChars strSksl(env, sksl);
- sk_sp<SkRuntimeEffect> effect = std::get<0>(SkRuntimeEffect::Make(SkString(strSksl.c_str())));
- ThrowIAE_IfNull(env, effect);
-
+ auto result = SkRuntimeEffect::Make(SkString(strSksl.c_str()));
+ sk_sp<SkRuntimeEffect> effect = std::get<0>(result);
+ if (!effect) {
+ const auto& err = std::get<1>(result);
+ doThrowIAE(env, err.c_str());
+ }
return reinterpret_cast<jlong>(effect.release());
}
@@ -264,7 +299,7 @@ static const JNINativeMethod gLinearGradientMethods[] = {
};
static const JNINativeMethod gRadialGradientMethods[] = {
- { "nativeCreate", "(JFFF[J[FIJ)J", (void*)RadialGradient_create },
+ { "nativeCreate", "(JFFFFFF[J[FIJ)J", (void*)RadialGradient_create },
};
static const JNINativeMethod gSweepGradientMethods[] = {
@@ -277,7 +312,7 @@ static const JNINativeMethod gComposeShaderMethods[] = {
static const JNINativeMethod gRuntimeShaderMethods[] = {
{ "nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer },
- { "nativeCreate", "(JJ[BJZ)J", (void*)RuntimeShader_create },
+ { "nativeCreate", "(JJ[B[JJZ)J", (void*)RuntimeShader_create },
{ "nativeCreateShaderFactory", "(Ljava/lang/String;)J",
(void*)RuntimeShader_createShaderFactory },
};
diff --git a/libs/hwui/jni/Utils.cpp b/libs/hwui/jni/Utils.cpp
index 34fd6687d52c..ac2f5b77d23a 100644
--- a/libs/hwui/jni/Utils.cpp
+++ b/libs/hwui/jni/Utils.cpp
@@ -114,7 +114,7 @@ size_t AssetStreamAdaptor::read(void* buffer, size_t size) {
return amount;
}
-SkMemoryStream* android::CopyAssetToStream(Asset* asset) {
+sk_sp<SkData> android::CopyAssetToData(Asset* asset) {
if (NULL == asset) {
return NULL;
}
@@ -138,7 +138,7 @@ SkMemoryStream* android::CopyAssetToStream(Asset* asset) {
return NULL;
}
- return new SkMemoryStream(std::move(data));
+ return data;
}
jobject android::nullObjectReturn(const char msg[]) {
diff --git a/libs/hwui/jni/Utils.h b/libs/hwui/jni/Utils.h
index f628cc3c85ed..6cdf44d85a5a 100644
--- a/libs/hwui/jni/Utils.h
+++ b/libs/hwui/jni/Utils.h
@@ -46,12 +46,11 @@ private:
};
/**
- * Make a deep copy of the asset, and return it as a stream, or NULL if there
+ * Make a deep copy of the asset, and return it as an SkData, or NULL if there
* was an error.
- * FIXME: If we could "ref/reopen" the asset, we may not need to copy it here.
*/
-SkMemoryStream* CopyAssetToStream(Asset*);
+sk_sp<SkData> CopyAssetToData(Asset*);
/** Restore the file descriptor's offset in our destructor
*/
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index b6c6cd0b5c1c..c04340c36511 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -30,6 +30,7 @@
#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/ScopedStringChars.h>
+#include "FontUtils.h"
#include "Bitmap.h"
#include "SkGraphics.h"
#include "SkRegion.h"
@@ -540,6 +541,21 @@ static void drawBitmapMesh(JNIEnv* env, jobject, jlong canvasHandle, jlong bitma
colorA.ptr() + colorIndex, paint);
}
+static void drawGlyphs(JNIEnv* env, jobject, jlong canvasHandle, jintArray glyphIds,
+ jfloatArray positions, jint glyphOffset, jint positionOffset,
+ jint glyphCount, jlong fontHandle, jlong paintHandle) {
+ Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+ FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle);
+ AutoJavaIntArray glyphIdArray(env, glyphIds);
+ AutoJavaFloatArray positionArray(env, positions);
+ get_canvas(canvasHandle)->drawGlyphs(
+ *font->font.get(),
+ glyphIdArray.ptr() + glyphOffset,
+ positionArray.ptr() + positionOffset,
+ glyphCount,
+ *paint);
+}
+
static void drawTextChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray charArray,
jint index, jint count, jfloat x, jfloat y, jint bidiFlags,
jlong paintHandle) {
@@ -719,6 +735,7 @@ static const JNINativeMethod gDrawMethods[] = {
{"nDrawBitmap","(JJFFJIII)V", (void*) CanvasJNI::drawBitmap},
{"nDrawBitmap","(JJFFFFFFFFJII)V", (void*) CanvasJNI::drawBitmapRect},
{"nDrawBitmap", "(J[IIIFFIIZJ)V", (void*)CanvasJNI::drawBitmapArray},
+ {"nDrawGlyphs", "(J[I[FIIIJJ)V", (void*)CanvasJNI::drawGlyphs},
{"nDrawText","(J[CIIFFIJ)V", (void*) CanvasJNI::drawTextChars},
{"nDrawText","(JLjava/lang/String;IIFFIJ)V", (void*) CanvasJNI::drawTextString},
{"nDrawTextRun","(J[CIIIIFFZJJ)V", (void*) CanvasJNI::drawTextRunChars},
diff --git a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
index 54822f1f07e2..7c1422de0984 100644
--- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
+++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
@@ -67,39 +67,7 @@ private:
JavaVM* mVm;
jobject mRunnable;
};
-
-class GlFunctorReleasedCallbackBridge : public GlFunctorLifecycleListener {
-public:
- GlFunctorReleasedCallbackBridge(JNIEnv* env, jobject javaCallback) {
- mLooper = Looper::getForThread();
- mMessage = new InvokeRunnableMessage(env, javaCallback);
- }
-
- virtual void onGlFunctorReleased(Functor* functor) override {
- mLooper->sendMessage(mMessage, 0);
- }
-
-private:
- sp<Looper> mLooper;
- sp<InvokeRunnableMessage> mMessage;
-};
-#endif
-
-// ---------------- @FastNative -----------------------------
-
-static void android_view_DisplayListCanvas_callDrawGLFunction(JNIEnv* env, jobject clazz,
- jlong canvasPtr, jlong functorPtr, jobject releasedCallback) {
-#ifdef __ANDROID__ // Layoutlib does not support GL
- Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr);
- Functor* functor = reinterpret_cast<Functor*>(functorPtr);
- sp<GlFunctorReleasedCallbackBridge> bridge;
- if (releasedCallback) {
- bridge = new GlFunctorReleasedCallbackBridge(env, releasedCallback);
- }
- canvas->callDrawGLFunction(functor, bridge.get());
#endif
-}
-
// ---------------- @CriticalNative -------------------------
@@ -124,10 +92,10 @@ static jint android_view_DisplayListCanvas_getMaxTextureSize(CRITICAL_JNI_PARAMS
#endif
}
-static void android_view_DisplayListCanvas_insertReorderBarrier(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr,
+static void android_view_DisplayListCanvas_enableZ(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr,
jboolean reorderEnable) {
Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr);
- canvas->insertReorderBarrier(reorderEnable);
+ canvas->enableZ(reorderEnable);
}
static jlong android_view_DisplayListCanvas_finishRecording(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr) {
@@ -183,18 +151,12 @@ static void android_view_DisplayListCanvas_drawWebViewFunctor(CRITICAL_JNI_PARAM
const char* const kClassPathName = "android/graphics/RecordingCanvas";
static JNINativeMethod gMethods[] = {
-
- // ------------ @FastNative ------------------
-
- { "nCallDrawGLFunction", "(JJLjava/lang/Runnable;)V",
- (void*) android_view_DisplayListCanvas_callDrawGLFunction },
-
// ------------ @CriticalNative --------------
{ "nCreateDisplayListCanvas", "(JII)J", (void*) android_view_DisplayListCanvas_createDisplayListCanvas },
{ "nResetDisplayListCanvas", "(JJII)V", (void*) android_view_DisplayListCanvas_resetDisplayListCanvas },
{ "nGetMaximumTextureWidth", "()I", (void*) android_view_DisplayListCanvas_getMaxTextureSize },
{ "nGetMaximumTextureHeight", "()I", (void*) android_view_DisplayListCanvas_getMaxTextureSize },
- { "nInsertReorderBarrier", "(JZ)V", (void*) android_view_DisplayListCanvas_insertReorderBarrier },
+ { "nEnableZ", "(JZ)V", (void*) android_view_DisplayListCanvas_enableZ },
{ "nFinishRecording", "(J)J", (void*) android_view_DisplayListCanvas_finishRecording },
{ "nDrawRenderNode", "(JJ)V", (void*) android_view_DisplayListCanvas_drawRenderNode },
{ "nDrawTextureLayer", "(JJ)V", (void*) android_view_DisplayListCanvas_drawTextureLayer },
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index 9815e85db880..a146b64e29cc 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -143,11 +143,10 @@ static jlong android_view_ThreadedRenderer_createRootRenderNode(JNIEnv* env, job
}
static jlong android_view_ThreadedRenderer_createProxy(JNIEnv* env, jobject clazz,
- jboolean translucent, jboolean isWideGamut, jlong rootRenderNodePtr) {
+ jboolean translucent, jlong rootRenderNodePtr) {
RootRenderNode* rootRenderNode = reinterpret_cast<RootRenderNode*>(rootRenderNodePtr);
ContextFactoryImpl factory(rootRenderNode);
RenderProxy* proxy = new RenderProxy(translucent, rootRenderNode, &factory);
- proxy->setWideGamut(isWideGamut);
return (jlong) proxy;
}
@@ -185,7 +184,9 @@ static void android_view_ThreadedRenderer_setSurface(JNIEnv* env, jobject clazz,
proxy->setSwapBehavior(SwapBehavior::kSwap_discardBuffer);
}
proxy->setSurface(window, enableTimeout);
- ANativeWindow_release(window);
+ if (window) {
+ ANativeWindow_release(window);
+ }
}
static jboolean android_view_ThreadedRenderer_pause(JNIEnv* env, jobject clazz,
@@ -218,10 +219,15 @@ static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz,
proxy->setOpaque(opaque);
}
-static void android_view_ThreadedRenderer_setWideGamut(JNIEnv* env, jobject clazz,
- jlong proxyPtr, jboolean wideGamut) {
+static void android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz,
+ jlong proxyPtr, jint colorMode) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
- proxy->setWideGamut(wideGamut);
+ proxy->setColorMode(static_cast<ColorMode>(colorMode));
+}
+
+static void android_view_ThreadedRenderer_setSdrWhitePoint(JNIEnv* env, jobject clazz,
+ jlong proxyPtr, jfloat sdrWhitePoint) {
+ Properties::defaultSdrWhitePoint = sdrWhitePoint;
}
static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz,
@@ -256,12 +262,6 @@ static void android_view_ThreadedRenderer_registerVectorDrawableAnimator(JNIEnv*
rootRenderNode->addVectorDrawableAnimator(animator);
}
-static void android_view_ThreadedRenderer_invokeFunctor(JNIEnv* env, jobject clazz,
- jlong functorPtr, jboolean waitForCompletion) {
- Functor* functor = reinterpret_cast<Functor*>(functorPtr);
- RenderProxy::invokeFunctor(functor, waitForCompletion);
-}
-
static jlong android_view_ThreadedRenderer_createTextureLayer(JNIEnv* env, jobject clazz,
jlong proxyPtr) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
@@ -514,7 +514,8 @@ static jobject android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode(
proxy.setLightGeometry((Vector3){0, 0, 0}, 0);
nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC);
UiFrameInfoBuilder(proxy.frameInfo())
- .setVsync(vsync, vsync)
+ .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID,
+ std::numeric_limits<int64_t>::max())
.addFlag(FrameInfoFlags::SurfaceCanvas);
proxy.syncAndDrawFrame();
}
@@ -593,6 +594,28 @@ static void android_view_ThreadedRenderer_preload(JNIEnv*, jclass) {
RenderProxy::preload();
}
+// Plumbs the display density down to DeviceInfo.
+static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, jint densityDpi) {
+ // Convert from dpi to density-independent pixels.
+ const float density = densityDpi / 160.0;
+ DeviceInfo::setDensity(density);
+}
+
+static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv*, jclass, jint physicalWidth,
+ jint physicalHeight, jfloat refreshRate,
+ jfloat maxRefreshRate,
+ jint wideColorDataspace,
+ jlong appVsyncOffsetNanos,
+ jlong presentationDeadlineNanos) {
+ DeviceInfo::setWidth(physicalWidth);
+ DeviceInfo::setHeight(physicalHeight);
+ DeviceInfo::setRefreshRate(refreshRate);
+ DeviceInfo::setMaxRefreshRate(maxRefreshRate);
+ DeviceInfo::setWideColorDataspace(static_cast<ADataSpace>(wideColorDataspace));
+ DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos);
+ DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos);
+}
+
// ----------------------------------------------------------------------------
// HardwareRendererObserver
// ----------------------------------------------------------------------------
@@ -637,67 +660,83 @@ static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, job
const char* const kClassPathName = "android/graphics/HardwareRenderer";
static const JNINativeMethod gMethods[] = {
- { "nRotateProcessStatsBuffer", "()V", (void*) android_view_ThreadedRenderer_rotateProcessStatsBuffer },
- { "nSetProcessStatsBuffer", "(I)V", (void*) android_view_ThreadedRenderer_setProcessStatsBuffer },
- { "nGetRenderThreadTid", "(J)I", (void*) android_view_ThreadedRenderer_getRenderThreadTid },
- { "nCreateRootRenderNode", "()J", (void*) android_view_ThreadedRenderer_createRootRenderNode },
- { "nCreateProxy", "(ZZJ)J", (void*) android_view_ThreadedRenderer_createProxy },
- { "nDeleteProxy", "(J)V", (void*) android_view_ThreadedRenderer_deleteProxy },
- { "nLoadSystemProperties", "(J)Z", (void*) android_view_ThreadedRenderer_loadSystemProperties },
- { "nSetName", "(JLjava/lang/String;)V", (void*) android_view_ThreadedRenderer_setName },
- { "nSetSurface", "(JLandroid/view/Surface;Z)V", (void*) android_view_ThreadedRenderer_setSurface },
- { "nPause", "(J)Z", (void*) android_view_ThreadedRenderer_pause },
- { "nSetStopped", "(JZ)V", (void*) android_view_ThreadedRenderer_setStopped },
- { "nSetLightAlpha", "(JFF)V", (void*) android_view_ThreadedRenderer_setLightAlpha },
- { "nSetLightGeometry", "(JFFFF)V", (void*) android_view_ThreadedRenderer_setLightGeometry },
- { "nSetOpaque", "(JZ)V", (void*) android_view_ThreadedRenderer_setOpaque },
- { "nSetWideGamut", "(JZ)V", (void*) android_view_ThreadedRenderer_setWideGamut },
- { "nSyncAndDrawFrame", "(J[JI)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame },
- { "nDestroy", "(JJ)V", (void*) android_view_ThreadedRenderer_destroy },
- { "nRegisterAnimatingRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_registerAnimatingRenderNode },
- { "nRegisterVectorDrawableAnimator", "(JJ)V", (void*) android_view_ThreadedRenderer_registerVectorDrawableAnimator },
- { "nInvokeFunctor", "(JZ)V", (void*) android_view_ThreadedRenderer_invokeFunctor },
- { "nCreateTextureLayer", "(J)J", (void*) android_view_ThreadedRenderer_createTextureLayer },
- { "nBuildLayer", "(JJ)V", (void*) android_view_ThreadedRenderer_buildLayer },
- { "nCopyLayerInto", "(JJJ)Z", (void*) android_view_ThreadedRenderer_copyLayerInto },
- { "nPushLayerUpdate", "(JJ)V", (void*) android_view_ThreadedRenderer_pushLayerUpdate },
- { "nCancelLayerUpdate", "(JJ)V", (void*) android_view_ThreadedRenderer_cancelLayerUpdate },
- { "nDetachSurfaceTexture", "(JJ)V", (void*) android_view_ThreadedRenderer_detachSurfaceTexture },
- { "nDestroyHardwareResources", "(J)V", (void*) android_view_ThreadedRenderer_destroyHardwareResources },
- { "nTrimMemory", "(I)V", (void*) android_view_ThreadedRenderer_trimMemory },
- { "nOverrideProperty", "(Ljava/lang/String;Ljava/lang/String;)V", (void*) android_view_ThreadedRenderer_overrideProperty },
- { "nFence", "(J)V", (void*) android_view_ThreadedRenderer_fence },
- { "nStopDrawing", "(J)V", (void*) android_view_ThreadedRenderer_stopDrawing },
- { "nNotifyFramePending", "(J)V", (void*) android_view_ThreadedRenderer_notifyFramePending },
- { "nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V", (void*) android_view_ThreadedRenderer_dumpProfileInfo },
- { "setupShadersDiskCache", "(Ljava/lang/String;Ljava/lang/String;)V",
- (void*) android_view_ThreadedRenderer_setupShadersDiskCache },
- { "nAddRenderNode", "(JJZ)V", (void*) android_view_ThreadedRenderer_addRenderNode},
- { "nRemoveRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_removeRenderNode},
- { "nDrawRenderNode", "(JJ)V", (void*) android_view_ThreadedRendererd_drawRenderNode},
- { "nSetContentDrawBounds", "(JIIII)V", (void*)android_view_ThreadedRenderer_setContentDrawBounds},
- { "nSetPictureCaptureCallback", "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V",
- (void*) android_view_ThreadedRenderer_setPictureCapturedCallbackJNI },
- { "nSetFrameCallback", "(JLandroid/graphics/HardwareRenderer$FrameDrawingCallback;)V",
- (void*)android_view_ThreadedRenderer_setFrameCallback},
- { "nSetFrameCompleteCallback", "(JLandroid/graphics/HardwareRenderer$FrameCompleteCallback;)V",
- (void*)android_view_ThreadedRenderer_setFrameCompleteCallback },
- { "nAddObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_addObserver },
- { "nRemoveObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_removeObserver },
- { "nCopySurfaceInto", "(Landroid/view/Surface;IIIIJ)I",
- (void*)android_view_ThreadedRenderer_copySurfaceInto },
- { "nCreateHardwareBitmap", "(JII)Landroid/graphics/Bitmap;",
- (void*)android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode },
- { "disableVsync", "()V", (void*)android_view_ThreadedRenderer_disableVsync },
- { "nSetHighContrastText", "(Z)V", (void*)android_view_ThreadedRenderer_setHighContrastText },
- { "nHackySetRTAnimationsEnabled", "(Z)V",
- (void*)android_view_ThreadedRenderer_hackySetRTAnimationsEnabled },
- { "nSetDebuggingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDebuggingEnabled },
- { "nSetIsolatedProcess", "(Z)V", (void*)android_view_ThreadedRenderer_setIsolatedProcess },
- { "nSetContextPriority", "(I)V", (void*)android_view_ThreadedRenderer_setContextPriority },
- { "nAllocateBuffers", "(J)V", (void*)android_view_ThreadedRenderer_allocateBuffers },
- { "nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark },
- { "preload", "()V", (void*)android_view_ThreadedRenderer_preload },
+ {"nRotateProcessStatsBuffer", "()V",
+ (void*)android_view_ThreadedRenderer_rotateProcessStatsBuffer},
+ {"nSetProcessStatsBuffer", "(I)V",
+ (void*)android_view_ThreadedRenderer_setProcessStatsBuffer},
+ {"nGetRenderThreadTid", "(J)I", (void*)android_view_ThreadedRenderer_getRenderThreadTid},
+ {"nCreateRootRenderNode", "()J", (void*)android_view_ThreadedRenderer_createRootRenderNode},
+ {"nCreateProxy", "(ZJ)J", (void*)android_view_ThreadedRenderer_createProxy},
+ {"nDeleteProxy", "(J)V", (void*)android_view_ThreadedRenderer_deleteProxy},
+ {"nLoadSystemProperties", "(J)Z",
+ (void*)android_view_ThreadedRenderer_loadSystemProperties},
+ {"nSetName", "(JLjava/lang/String;)V", (void*)android_view_ThreadedRenderer_setName},
+ {"nSetSurface", "(JLandroid/view/Surface;Z)V",
+ (void*)android_view_ThreadedRenderer_setSurface},
+ {"nPause", "(J)Z", (void*)android_view_ThreadedRenderer_pause},
+ {"nSetStopped", "(JZ)V", (void*)android_view_ThreadedRenderer_setStopped},
+ {"nSetLightAlpha", "(JFF)V", (void*)android_view_ThreadedRenderer_setLightAlpha},
+ {"nSetLightGeometry", "(JFFFF)V", (void*)android_view_ThreadedRenderer_setLightGeometry},
+ {"nSetOpaque", "(JZ)V", (void*)android_view_ThreadedRenderer_setOpaque},
+ {"nSetColorMode", "(JI)V", (void*)android_view_ThreadedRenderer_setColorMode},
+ {"nSetSdrWhitePoint", "(JF)V", (void*)android_view_ThreadedRenderer_setSdrWhitePoint},
+ {"nSyncAndDrawFrame", "(J[JI)I", (void*)android_view_ThreadedRenderer_syncAndDrawFrame},
+ {"nDestroy", "(JJ)V", (void*)android_view_ThreadedRenderer_destroy},
+ {"nRegisterAnimatingRenderNode", "(JJ)V",
+ (void*)android_view_ThreadedRenderer_registerAnimatingRenderNode},
+ {"nRegisterVectorDrawableAnimator", "(JJ)V",
+ (void*)android_view_ThreadedRenderer_registerVectorDrawableAnimator},
+ {"nCreateTextureLayer", "(J)J", (void*)android_view_ThreadedRenderer_createTextureLayer},
+ {"nBuildLayer", "(JJ)V", (void*)android_view_ThreadedRenderer_buildLayer},
+ {"nCopyLayerInto", "(JJJ)Z", (void*)android_view_ThreadedRenderer_copyLayerInto},
+ {"nPushLayerUpdate", "(JJ)V", (void*)android_view_ThreadedRenderer_pushLayerUpdate},
+ {"nCancelLayerUpdate", "(JJ)V", (void*)android_view_ThreadedRenderer_cancelLayerUpdate},
+ {"nDetachSurfaceTexture", "(JJ)V",
+ (void*)android_view_ThreadedRenderer_detachSurfaceTexture},
+ {"nDestroyHardwareResources", "(J)V",
+ (void*)android_view_ThreadedRenderer_destroyHardwareResources},
+ {"nTrimMemory", "(I)V", (void*)android_view_ThreadedRenderer_trimMemory},
+ {"nOverrideProperty", "(Ljava/lang/String;Ljava/lang/String;)V",
+ (void*)android_view_ThreadedRenderer_overrideProperty},
+ {"nFence", "(J)V", (void*)android_view_ThreadedRenderer_fence},
+ {"nStopDrawing", "(J)V", (void*)android_view_ThreadedRenderer_stopDrawing},
+ {"nNotifyFramePending", "(J)V", (void*)android_view_ThreadedRenderer_notifyFramePending},
+ {"nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V",
+ (void*)android_view_ThreadedRenderer_dumpProfileInfo},
+ {"setupShadersDiskCache", "(Ljava/lang/String;Ljava/lang/String;)V",
+ (void*)android_view_ThreadedRenderer_setupShadersDiskCache},
+ {"nAddRenderNode", "(JJZ)V", (void*)android_view_ThreadedRenderer_addRenderNode},
+ {"nRemoveRenderNode", "(JJ)V", (void*)android_view_ThreadedRenderer_removeRenderNode},
+ {"nDrawRenderNode", "(JJ)V", (void*)android_view_ThreadedRendererd_drawRenderNode},
+ {"nSetContentDrawBounds", "(JIIII)V",
+ (void*)android_view_ThreadedRenderer_setContentDrawBounds},
+ {"nSetPictureCaptureCallback",
+ "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V",
+ (void*)android_view_ThreadedRenderer_setPictureCapturedCallbackJNI},
+ {"nSetFrameCallback", "(JLandroid/graphics/HardwareRenderer$FrameDrawingCallback;)V",
+ (void*)android_view_ThreadedRenderer_setFrameCallback},
+ {"nSetFrameCompleteCallback",
+ "(JLandroid/graphics/HardwareRenderer$FrameCompleteCallback;)V",
+ (void*)android_view_ThreadedRenderer_setFrameCompleteCallback},
+ {"nAddObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_addObserver},
+ {"nRemoveObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_removeObserver},
+ {"nCopySurfaceInto", "(Landroid/view/Surface;IIIIJ)I",
+ (void*)android_view_ThreadedRenderer_copySurfaceInto},
+ {"nCreateHardwareBitmap", "(JII)Landroid/graphics/Bitmap;",
+ (void*)android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode},
+ {"disableVsync", "()V", (void*)android_view_ThreadedRenderer_disableVsync},
+ {"nSetHighContrastText", "(Z)V", (void*)android_view_ThreadedRenderer_setHighContrastText},
+ {"nHackySetRTAnimationsEnabled", "(Z)V",
+ (void*)android_view_ThreadedRenderer_hackySetRTAnimationsEnabled},
+ {"nSetDebuggingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDebuggingEnabled},
+ {"nSetIsolatedProcess", "(Z)V", (void*)android_view_ThreadedRenderer_setIsolatedProcess},
+ {"nSetContextPriority", "(I)V", (void*)android_view_ThreadedRenderer_setContextPriority},
+ {"nAllocateBuffers", "(J)V", (void*)android_view_ThreadedRenderer_allocateBuffers},
+ {"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark},
+ {"nSetDisplayDensityDpi", "(I)V",
+ (void*)android_view_ThreadedRenderer_setDisplayDensityDpi},
+ {"nInitDisplayInfo", "(IIFFIJJ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
+ {"preload", "()V", (void*)android_view_ThreadedRenderer_preload},
};
static JavaVM* mJvm = nullptr;
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index 85c802b40459..4b4aa92b97b7 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -215,6 +215,12 @@ static jboolean android_view_RenderNode_setAlpha(CRITICAL_JNI_PARAMS_COMMA jlong
return SET_AND_DIRTY(setAlpha, alpha, RenderNode::ALPHA);
}
+static jboolean android_view_RenderNode_setRenderEffect(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr,
+ jlong renderEffectPtr) {
+ SkImageFilter* imageFilter = reinterpret_cast<SkImageFilter*>(renderEffectPtr);
+ return SET_AND_DIRTY(mutateLayerProperties().setImageFilter, imageFilter, RenderNode::GENERIC);
+}
+
static jboolean android_view_RenderNode_setHasOverlappingRendering(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr,
bool hasOverlappingRendering) {
return SET_AND_DIRTY(setHasOverlappingRendering, hasOverlappingRendering,
@@ -690,6 +696,7 @@ static const JNINativeMethod gMethods[] = {
{ "nSetRevealClip", "(JZFFF)Z", (void*) android_view_RenderNode_setRevealClip },
{ "nSetAlpha", "(JF)Z", (void*) android_view_RenderNode_setAlpha },
+ { "nSetRenderEffect", "(JJ)V", (void*) android_view_RenderNode_setRenderEffect },
{ "nSetHasOverlappingRendering", "(JZ)Z",
(void*) android_view_RenderNode_setHasOverlappingRendering },
{ "nSetUsageHint", "(JI)V", (void*) android_view_RenderNode_setUsageHint },
diff --git a/libs/hwui/jni/android_graphics_TextureLayer.cpp b/libs/hwui/jni/android_graphics_TextureLayer.cpp
index bd20269d3751..4dbb24ce4347 100644
--- a/libs/hwui/jni/android_graphics_TextureLayer.cpp
+++ b/libs/hwui/jni/android_graphics_TextureLayer.cpp
@@ -67,7 +67,7 @@ static void TextureLayer_updateSurfaceTexture(JNIEnv* env, jobject clazz,
// JNI Glue
// ----------------------------------------------------------------------------
-const char* const kClassPathName = "android/view/TextureLayer";
+const char* const kClassPathName = "android/graphics/TextureLayer";
static const JNINativeMethod gMethods[] = {
{ "nPrepare", "(JIIZ)Z", (void*) TextureLayer_prepare },
@@ -78,7 +78,7 @@ static const JNINativeMethod gMethods[] = {
{ "nUpdateSurfaceTexture", "(J)V", (void*) TextureLayer_updateSurfaceTexture },
};
-int register_android_view_TextureLayer(JNIEnv* env) {
+int register_android_graphics_TextureLayer(JNIEnv* env) {
return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
}
diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp
index 5714cd1d0390..4aee6b94a2be 100644
--- a/libs/hwui/jni/fonts/Font.cpp
+++ b/libs/hwui/jni/fonts/Font.cpp
@@ -18,6 +18,8 @@
#define LOG_TAG "Minikin"
#include "SkData.h"
+#include "SkFont.h"
+#include "SkFontMetrics.h"
#include "SkFontMgr.h"
#include "SkRefCnt.h"
#include "SkTypeface.h"
@@ -27,6 +29,7 @@
#include "FontUtils.h"
#include <hwui/MinikinSkia.h>
+#include <hwui/Paint.h>
#include <hwui/Typeface.h>
#include <minikin/FontFamily.h>
#include <ui/FatVector.h>
@@ -93,19 +96,19 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo
sk_sp<SkData> data(SkData::MakeWithProc(fontPtr, fontSize,
release_global_ref, reinterpret_cast<void*>(fontRef)));
- FatVector<SkFontArguments::Axis, 2> skiaAxes;
+ FatVector<SkFontArguments::VariationPosition::Coordinate, 2> skVariation;
for (const auto& axis : builder->axes) {
- skiaAxes.emplace_back(SkFontArguments::Axis{axis.axisTag, axis.value});
+ skVariation.push_back({axis.axisTag, axis.value});
}
std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(std::move(data)));
- SkFontArguments params;
- params.setCollectionIndex(ttcIndex);
- params.setAxes(skiaAxes.data(), skiaAxes.size());
+ SkFontArguments args;
+ args.setCollectionIndex(ttcIndex);
+ args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())});
sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
- sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), params));
+ sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args));
if (face == nullptr) {
jniThrowException(env, "java/lang/IllegalArgumentException",
"Failed to create internal object. maybe invalid font data.");
@@ -115,11 +118,43 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo
std::make_shared<MinikinFontSkia>(std::move(face), fontPtr, fontSize,
std::string_view(fontPath.c_str(), fontPath.size()),
ttcIndex, builder->axes);
- minikin::Font font = minikin::Font::Builder(minikinFont).setWeight(weight)
+ std::shared_ptr<minikin::Font> font = minikin::Font::Builder(minikinFont).setWeight(weight)
.setSlant(static_cast<minikin::FontStyle::Slant>(italic)).build();
return reinterpret_cast<jlong>(new FontWrapper(std::move(font)));
}
+// Fast Native
+static jlong Font_Builder_clone(JNIEnv* env, jobject clazz, jlong fontPtr, jlong builderPtr,
+ jint weight, jboolean italic, jint ttcIndex) {
+ FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr);
+ MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font->typeface().get());
+ std::unique_ptr<NativeFontBuilder> builder(toBuilder(builderPtr));
+
+ // Reconstruct SkTypeface with different arguments from existing SkTypeface.
+ FatVector<SkFontArguments::VariationPosition::Coordinate, 2> skVariation;
+ for (const auto& axis : builder->axes) {
+ skVariation.push_back({axis.axisTag, axis.value});
+ }
+ SkFontArguments args;
+ args.setCollectionIndex(ttcIndex);
+ args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())});
+
+ sk_sp<SkTypeface> newTypeface = minikinSkia->GetSkTypeface()->makeClone(args);
+
+ std::shared_ptr<minikin::MinikinFont> newMinikinFont = std::make_shared<MinikinFontSkia>(
+ std::move(newTypeface),
+ minikinSkia->GetFontData(),
+ minikinSkia->GetFontSize(),
+ minikinSkia->getFilePath(),
+ minikinSkia->GetFontIndex(),
+ builder->axes);
+ std::shared_ptr<minikin::Font> newFont = minikin::Font::Builder(newMinikinFont)
+ .setWeight(weight)
+ .setSlant(static_cast<minikin::FontStyle::Slant>(italic))
+ .build();
+ return reinterpret_cast<jlong>(new FontWrapper(std::move(newFont)));
+}
+
// Critical Native
static jlong Font_Builder_getReleaseNativeFont(CRITICAL_JNI_PARAMS) {
return reinterpret_cast<jlong>(releaseFont);
@@ -127,16 +162,157 @@ static jlong Font_Builder_getReleaseNativeFont(CRITICAL_JNI_PARAMS) {
///////////////////////////////////////////////////////////////////////////////
+// Fast Native
+static jfloat Font_getGlyphBounds(JNIEnv* env, jobject, jlong fontHandle, jint glyphId,
+ jlong paintHandle, jobject rect) {
+ FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle);
+ MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font->typeface().get());
+ Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+
+ SkFont* skFont = &paint->getSkFont();
+ // We don't use populateSkFont since it is designed to be used for layout result with addressing
+ // auto fake-bolding.
+ skFont->setTypeface(minikinSkia->RefSkTypeface());
+
+ uint16_t glyph16 = glyphId;
+ SkRect skBounds;
+ SkScalar skWidth;
+ skFont->getWidthsBounds(&glyph16, 1, &skWidth, &skBounds, nullptr);
+ GraphicsJNI::rect_to_jrectf(skBounds, env, rect);
+ return SkScalarToFloat(skWidth);
+}
+
+// Fast Native
+static jfloat Font_getFontMetrics(JNIEnv* env, jobject, jlong fontHandle, jlong paintHandle,
+ jobject metricsObj) {
+ FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle);
+ MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font->typeface().get());
+ Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+
+ SkFont* skFont = &paint->getSkFont();
+ // We don't use populateSkFont since it is designed to be used for layout result with addressing
+ // auto fake-bolding.
+ skFont->setTypeface(minikinSkia->RefSkTypeface());
+
+ SkFontMetrics metrics;
+ SkScalar spacing = skFont->getMetrics(&metrics);
+ GraphicsJNI::set_metrics(env, metricsObj, metrics);
+ return spacing;
+}
+
+// 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());
+}
+
+// Critical Native
+static jboolean Font_isSameBufferAddress(CRITICAL_JNI_PARAMS_COMMA jlong lFontHandle,
+ jlong rFontHandle) {
+ FontWrapper* lFont = reinterpret_cast<FontWrapper*>(lFontHandle);
+ FontWrapper* rFont = reinterpret_cast<FontWrapper*>(rFontHandle);
+ const void* lBufferPtr = lFont->font->typeface()->GetFontData();
+ const void* rBufferPtr = rFont->font->typeface()->GetFontData();
+ return lBufferPtr == rBufferPtr;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct FontBufferWrapper {
+ FontBufferWrapper(const std::shared_ptr<minikin::MinikinFont>& font) : minikinFont(font) {}
+ // MinikinFont holds a shared pointer of SkTypeface which has reference to font data.
+ std::shared_ptr<minikin::MinikinFont> minikinFont;
+};
+
+static void unrefBuffer(jlong nativePtr) {
+ FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr);
+ delete wrapper;
+}
+
+// Critical Native
+static jlong FontBufferHelper_refFontBuffer(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) {
+ const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle);
+ return reinterpret_cast<jlong>(new FontBufferWrapper(font->typeface()));
+}
+
+// Fast Native
+static jobject FontBufferHelper_wrapByteBuffer(JNIEnv* env, jobject, jlong nativePtr) {
+ FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr);
+ return env->NewDirectByteBuffer(
+ const_cast<void*>(wrapper->minikinFont->GetFontData()),
+ wrapper->minikinFont->GetFontSize());
+}
+
+// Critical Native
+static jlong FontBufferHelper_getReleaseFunc(CRITICAL_JNI_PARAMS) {
+ return reinterpret_cast<jlong>(unrefBuffer);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
static const JNINativeMethod gFontBuilderMethods[] = {
{ "nInitBuilder", "()J", (void*) Font_Builder_initBuilder },
{ "nAddAxis", "(JIF)V", (void*) Font_Builder_addAxis },
{ "nBuild", "(JLjava/nio/ByteBuffer;Ljava/lang/String;IZI)J", (void*) Font_Builder_build },
+ { "nClone", "(JJIZI)J", (void*) Font_Builder_clone },
{ "nGetReleaseNativeFont", "()J", (void*) Font_Builder_getReleaseNativeFont },
};
+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 },
+ { "nIsSameBufferAddress", "(JJ)Z", (void*) Font_isSameBufferAddress },
+};
+
+static const JNINativeMethod gFontBufferHelperMethods[] = {
+ { "nRefFontBuffer", "(J)J", (void*) FontBufferHelper_refFontBuffer },
+ { "nWrapByteBuffer", "(J)Ljava/nio/ByteBuffer;", (void*) FontBufferHelper_wrapByteBuffer },
+ { "nGetReleaseFunc", "()J", (void*) FontBufferHelper_getReleaseFunc },
+};
+
int register_android_graphics_fonts_Font(JNIEnv* env) {
return RegisterMethodsOrDie(env, "android/graphics/fonts/Font$Builder", gFontBuilderMethods,
- NELEM(gFontBuilderMethods));
+ NELEM(gFontBuilderMethods)) +
+ RegisterMethodsOrDie(env, "android/graphics/fonts/Font", gFontMethods,
+ NELEM(gFontMethods)) +
+ RegisterMethodsOrDie(env, "android/graphics/fonts/NativeFontBufferHelper",
+ gFontBufferHelperMethods, NELEM(gFontBufferHelperMethods));
}
}
diff --git a/libs/hwui/jni/fonts/FontFamily.cpp b/libs/hwui/jni/fonts/FontFamily.cpp
index df619d9f1406..37e52766f2ef 100644
--- a/libs/hwui/jni/fonts/FontFamily.cpp
+++ b/libs/hwui/jni/fonts/FontFamily.cpp
@@ -30,7 +30,7 @@
namespace android {
struct NativeFamilyBuilder {
- std::vector<minikin::Font> fonts;
+ std::vector<std::shared_ptr<minikin::Font>> fonts;
};
static inline NativeFamilyBuilder* toBuilder(jlong ptr) {
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
new file mode 100644
index 000000000000..9785aa537f65
--- /dev/null
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -0,0 +1,206 @@
+/*
+ * 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.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "TextShaper"
+
+#include "graphics_jni_helpers.h"
+#include <nativehelper/ScopedStringChars.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+#include <set>
+#include <algorithm>
+
+#include "SkPaint.h"
+#include "SkTypeface.h"
+#include <hwui/MinikinSkia.h>
+#include <hwui/MinikinUtils.h>
+#include <hwui/Paint.h>
+#include <minikin/MinikinPaint.h>
+#include <minikin/MinikinFont.h>
+
+namespace android {
+
+struct LayoutWrapper {
+ LayoutWrapper(minikin::Layout&& layout, float ascent, float descent)
+ : layout(std::move(layout)), ascent(ascent), descent(descent) {}
+ minikin::Layout layout;
+ float ascent;
+ float descent;
+};
+
+static void releaseLayout(jlong ptr) {
+ delete reinterpret_cast<LayoutWrapper*>(ptr);
+}
+
+static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int count,
+ int contextStart, int contextCount, minikin::Bidi bidiFlags,
+ const Paint& paint, const Typeface* typeface) {
+
+ minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(&paint, typeface);
+
+ minikin::Layout layout = MinikinUtils::doLayout(&paint, bidiFlags, typeface,
+ text, textSize, start, count, contextStart, contextCount, nullptr);
+
+ std::set<const minikin::Font*> seenFonts;
+ float overallAscent = 0;
+ float overallDescent = 0;
+ for (int i = 0; i < layout.nGlyphs(); ++i) {
+ const minikin::Font* font = layout.getFont(i);
+ if (seenFonts.find(font) != seenFonts.end()) continue;
+ minikin::MinikinExtent extent = {};
+ font->typeface()->GetFontExtent(&extent, minikinPaint, layout.getFakery(i));
+ overallAscent = std::min(overallAscent, extent.ascent);
+ overallDescent = std::max(overallDescent, extent.descent);
+ }
+
+ std::unique_ptr<LayoutWrapper> ptr = std::make_unique<LayoutWrapper>(
+ std::move(layout), overallAscent, overallDescent
+ );
+
+ return reinterpret_cast<jlong>(ptr.release());
+}
+
+static jlong TextShaper_shapeTextRunChars(JNIEnv *env, jobject, jcharArray charArray,
+ jint start, jint count, jint contextStart, jint contextCount, jboolean isRtl,
+ jlong paintPtr) {
+ ScopedCharArrayRO text(env, charArray);
+ Paint* paint = reinterpret_cast<Paint*>(paintPtr);
+ const Typeface* typeface = paint->getAndroidTypeface();
+ const minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
+ return shapeTextRun(
+ text.get(), text.size(),
+ start, count,
+ contextStart, contextCount,
+ bidiFlags,
+ *paint, typeface);
+
+}
+
+static jlong TextShaper_shapeTextRunString(JNIEnv *env, jobject, jstring string,
+ jint start, jint count, jint contextStart, jint contextCount, jboolean isRtl,
+ jlong paintPtr) {
+ ScopedStringChars text(env, string);
+ Paint* paint = reinterpret_cast<Paint*>(paintPtr);
+ const Typeface* typeface = paint->getAndroidTypeface();
+ const minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
+ return shapeTextRun(
+ text.get(), text.size(),
+ start, count,
+ contextStart, contextCount,
+ bidiFlags,
+ *paint, typeface);
+}
+
+// CriticalNative
+static jint TextShaper_Result_getGlyphCount(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->layout.nGlyphs();
+}
+
+// CriticalNative
+static jfloat TextShaper_Result_getTotalAdvance(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->layout.getAdvance();
+}
+
+// CriticalNative
+static jfloat TextShaper_Result_getAscent(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->ascent;
+}
+
+// CriticalNative
+static jfloat TextShaper_Result_getDescent(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->descent;
+}
+
+// CriticalNative
+static jint TextShaper_Result_getGlyphId(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->layout.getGlyphId(i);
+}
+
+// CriticalNative
+static jfloat TextShaper_Result_getX(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->layout.getX(i);
+}
+
+// CriticalNative
+static jfloat TextShaper_Result_getY(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->layout.getY(i);
+}
+
+// CriticalNative
+static jlong TextShaper_Result_getFont(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return reinterpret_cast<jlong>(layout->layout.getFont(i));
+}
+
+// CriticalNative
+static jlong TextShaper_Result_nReleaseFunc(CRITICAL_JNI_PARAMS) {
+ return reinterpret_cast<jlong>(releaseLayout);
+}
+
+static const JNINativeMethod gMethods[] = {
+ // Fast Natives
+ {"nativeShapeTextRun", "("
+ "[C" // text
+ "I" // start
+ "I" // count
+ "I" // contextStart
+ "I" // contextCount
+ "Z" // isRtl
+ "J)" // paint
+ "J", // LayoutPtr
+ (void*) TextShaper_shapeTextRunChars},
+
+ {"nativeShapeTextRun", "("
+ "Ljava/lang/String;" // text
+ "I" // start
+ "I" // count
+ "I" // contextStart
+ "I" // contextCount
+ "Z" // isRtl
+ "J)" // paint
+ "J", // LayoutPtr
+ (void*) TextShaper_shapeTextRunString},
+
+};
+
+static const JNINativeMethod gResultMethods[] = {
+ { "nGetGlyphCount", "(J)I", (void*)TextShaper_Result_getGlyphCount },
+ { "nGetTotalAdvance", "(J)F", (void*)TextShaper_Result_getTotalAdvance },
+ { "nGetAscent", "(J)F", (void*)TextShaper_Result_getAscent },
+ { "nGetDescent", "(J)F", (void*)TextShaper_Result_getDescent },
+ { "nGetGlyphId", "(JI)I", (void*)TextShaper_Result_getGlyphId },
+ { "nGetX", "(JI)F", (void*)TextShaper_Result_getX },
+ { "nGetY", "(JI)F", (void*)TextShaper_Result_getY },
+ { "nGetFont", "(JI)J", (void*)TextShaper_Result_getFont },
+ { "nReleaseFunc", "()J", (void*)TextShaper_Result_nReleaseFunc },
+};
+
+int register_android_graphics_text_TextShaper(JNIEnv* env) {
+ return RegisterMethodsOrDie(env, "android/graphics/text/TextRunShaper", gMethods,
+ NELEM(gMethods))
+ + RegisterMethodsOrDie(env, "android/graphics/text/PositionedGlyphs",
+ gResultMethods, NELEM(gResultMethods));
+}
+
+}
+
diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt
new file mode 100644
index 000000000000..73de0d12a60b
--- /dev/null
+++ b/libs/hwui/libhwui.map.txt
@@ -0,0 +1,70 @@
+LIBHWUI {
+ global:
+ /* listing of all C APIs to be exposed by libhwui to consumers outside of the module */
+ ABitmap_getInfoFromJava;
+ ABitmap_acquireBitmapFromJava;
+ ABitmap_copy;
+ ABitmap_acquireRef;
+ ABitmap_releaseRef;
+ ABitmap_getInfo;
+ ABitmap_getDataSpace;
+ ABitmap_getPixels;
+ ABitmap_notifyPixelsChanged;
+ ABitmapConfig_getFormatFromConfig;
+ ABitmapConfig_getConfigFromFormat;
+ ABitmap_compress;
+ ABitmap_getHardwareBuffer;
+ ACanvas_isSupportedPixelFormat;
+ ACanvas_getNativeHandleFromJava;
+ ACanvas_createCanvas;
+ ACanvas_destroyCanvas;
+ ACanvas_setBuffer;
+ ACanvas_clipRect;
+ ACanvas_clipOutRect;
+ ACanvas_drawRect;
+ ACanvas_drawBitmap;
+ init_android_graphics;
+ register_android_graphics_classes;
+ register_android_graphics_GraphicsStatsService;
+ zygote_preload_graphics;
+ AMatrix_getContents;
+ APaint_createPaint;
+ APaint_destroyPaint;
+ APaint_setBlendMode;
+ ARegionIterator_acquireIterator;
+ ARegionIterator_releaseIterator;
+ ARegionIterator_isComplex;
+ ARegionIterator_isDone;
+ ARegionIterator_next;
+ ARegionIterator_getRect;
+ ARegionIterator_getTotalBounds;
+ ARenderThread_dumpGraphicsMemory;
+ local:
+ *;
+};
+
+LIBHWUI_PLATFORM {
+ global:
+ extern "C++" {
+ /* required by libwebviewchromium_plat_support */
+ android::uirenderer::ColorSpaceToADataSpace*;
+ android::uirenderer::WebViewFunctor_*;
+ GraphicsJNI::getNativeCanvas*;
+ SkCanvasStateUtils::ReleaseCanvasState*;
+ SkColorSpace::toXYZD50*;
+ SkColorSpace::transferFn*;
+ /* required by libjnigraphics */
+ android::ImageDecoder::*;
+ android::uirenderer::DataSpaceToColorSpace*;
+ android::uirenderer::ColorSpaceToADataSpace*;
+ getMimeType*;
+ SkAndroidCodec::*;
+ SkCodec::MakeFromStream*;
+ SkColorInfo::*;
+ SkFILEStream::SkFILEStream*;
+ SkImageInfo::*;
+ SkMemoryStream::SkMemoryStream*;
+ };
+ local:
+ *;
+};
diff --git a/libs/hwui/pipeline/skia/FunctorDrawable.h b/libs/hwui/pipeline/skia/FunctorDrawable.h
index cf2f93b95e71..988a896b6267 100644
--- a/libs/hwui/pipeline/skia/FunctorDrawable.h
+++ b/libs/hwui/pipeline/skia/FunctorDrawable.h
@@ -16,8 +16,6 @@
#pragma once
-#include "GlFunctorLifecycleListener.h"
-
#include <SkCanvas.h>
#include <SkDrawable.h>
@@ -36,44 +34,21 @@ namespace skiapipeline {
*/
class FunctorDrawable : public SkDrawable {
public:
- FunctorDrawable(Functor* functor, GlFunctorLifecycleListener* listener, SkCanvas* canvas)
- : mBounds(canvas->getLocalClipBounds())
- , mAnyFunctor(std::in_place_type<LegacyFunctor>, functor, listener) {}
-
FunctorDrawable(int functor, SkCanvas* canvas)
: mBounds(canvas->getLocalClipBounds())
- , mAnyFunctor(std::in_place_type<NewFunctor>, functor) {}
+ , mWebViewHandle(WebViewFunctorManager::instance().handleFor(functor)) {}
virtual ~FunctorDrawable() {}
virtual void syncFunctor(const WebViewSyncData& data) const {
- if (mAnyFunctor.index() == 0) {
- std::get<0>(mAnyFunctor).handle->sync(data);
- } else {
- (*(std::get<1>(mAnyFunctor).functor))(DrawGlInfo::kModeSync, nullptr);
- }
+ mWebViewHandle->sync(data);
}
protected:
virtual SkRect onGetBounds() override { return mBounds; }
const SkRect mBounds;
-
- struct LegacyFunctor {
- explicit LegacyFunctor(Functor* functor, GlFunctorLifecycleListener* listener)
- : functor(functor), listener(listener) {}
- Functor* functor;
- sp<GlFunctorLifecycleListener> listener;
- };
-
- struct NewFunctor {
- explicit NewFunctor(int functor) {
- handle = WebViewFunctorManager::instance().handleFor(functor);
- }
- sp<WebViewFunctor::Handle> handle;
- };
-
- std::variant<NewFunctor, LegacyFunctor> mAnyFunctor;
+ sp<WebViewFunctor::Handle> mWebViewHandle;
};
} // namespace skiapipeline
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
index 8f67f97fb4bc..bfbdc5c009c0 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
@@ -15,10 +15,9 @@
*/
#include "GLFunctorDrawable.h"
-#include <GrContext.h>
+#include <GrDirectContext.h>
#include <private/hwui/DrawGlInfo.h>
#include "FunctorDrawable.h"
-#include "GlFunctorLifecycleListener.h"
#include "GrBackendSurface.h"
#include "GrRenderTarget.h"
#include "GrRenderTargetContext.h"
@@ -26,20 +25,12 @@
#include "SkAndroidFrameworkUtils.h"
#include "SkClipStack.h"
#include "SkRect.h"
-#include "include/private/SkM44.h"
+#include "SkM44.h"
namespace android {
namespace uirenderer {
namespace skiapipeline {
-GLFunctorDrawable::~GLFunctorDrawable() {
- if (auto lp = std::get_if<LegacyFunctor>(&mAnyFunctor)) {
- if (lp->listener) {
- lp->listener->onGlFunctorReleased(lp->functor);
- }
- }
-}
-
static void setScissor(int viewportHeight, const SkIRect& clip) {
SkASSERT(!clip.isEmpty());
// transform to Y-flipped GL space, and prevent negatives
@@ -65,7 +56,8 @@ static void GetFboDetails(SkCanvas* canvas, GLuint* outFboID, SkISize* outFboSiz
}
void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
- if (canvas->getGrContext() == nullptr) {
+ GrDirectContext* directContext = GrAsDirectContext(canvas->recordingContext());
+ if (directContext == nullptr) {
// We're dumping a picture, render a light-blue rectangle instead
// TODO: Draw the WebView text on top? Seemingly complicated as SkPaint doesn't
// seem to have a default typeface that works. We only ever use drawGlyphs, which
@@ -85,7 +77,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
SkIRect surfaceBounds = canvas->internal_private_getTopLayerBounds();
SkIRect clipBounds = canvas->getDeviceClipBounds();
- SkM44 mat4(canvas->experimental_getLocalToDevice());
+ SkM44 mat4(canvas->getLocalToDevice());
SkRegion clipRegion;
canvas->temporary_internal_getRgnClip(&clipRegion);
@@ -96,7 +88,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
SkImageInfo surfaceInfo =
canvas->imageInfo().makeWH(clipBounds.width(), clipBounds.height());
tmpSurface =
- SkSurface::MakeRenderTarget(canvas->getGrContext(), SkBudgeted::kYes, surfaceInfo);
+ SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, surfaceInfo);
tmpSurface->getCanvas()->clear(SK_ColorTRANSPARENT);
GrGLFramebufferInfo fboInfo;
@@ -150,7 +142,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
// notify Skia that we just updated the FBO and stencil
const uint32_t grState = kStencil_GrGLBackendState | kRenderTarget_GrGLBackendState;
- canvas->getGrContext()->resetContext(grState);
+ directContext->resetContext(grState);
SkCanvas* tmpCanvas = canvas;
if (tmpSurface) {
@@ -186,11 +178,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
setScissor(info.height, clipRegion.getBounds());
}
- if (mAnyFunctor.index() == 0) {
- std::get<0>(mAnyFunctor).handle->drawGl(info);
- } else {
- (*(std::get<1>(mAnyFunctor).functor))(DrawGlInfo::kModeDraw, &info);
- }
+ mWebViewHandle->drawGl(info);
if (clearStencilAfterFunctor) {
// clear stencil buffer as it may be used by Skia
@@ -201,7 +189,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
glClear(GL_STENCIL_BUFFER_BIT);
}
- canvas->getGrContext()->resetContext();
+ directContext->resetContext();
// if there were unclipped save layers involved we draw our offscreen surface to the canvas
if (tmpSurface) {
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.h b/libs/hwui/pipeline/skia/GLFunctorDrawable.h
index 2ea4f67428bc..4092e8dfa3a5 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.h
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.h
@@ -33,7 +33,7 @@ class GLFunctorDrawable : public FunctorDrawable {
public:
using FunctorDrawable::FunctorDrawable;
- virtual ~GLFunctorDrawable();
+ virtual ~GLFunctorDrawable() {}
protected:
void onDraw(SkCanvas* canvas) override;
diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp
index f839213e9007..f95f347cffaf 100644
--- a/libs/hwui/pipeline/skia/LayerDrawable.cpp
+++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp
@@ -29,7 +29,7 @@ namespace skiapipeline {
void LayerDrawable::onDraw(SkCanvas* canvas) {
Layer* layer = mLayerUpdater->backingLayer();
if (layer) {
- DrawLayer(canvas->getGrContext(), canvas, layer, nullptr, nullptr, true);
+ DrawLayer(canvas->recordingContext(), canvas, layer, nullptr, nullptr, true);
}
}
@@ -67,8 +67,12 @@ static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, cons
isIntegerAligned(dstDevRect.y()));
}
-bool LayerDrawable::DrawLayer(GrContext* context, SkCanvas* canvas, Layer* layer,
- const SkRect* srcRect, const SkRect* dstRect,
+// TODO: Context arg probably doesn't belong here – do debug check at callsite instead.
+bool LayerDrawable::DrawLayer(GrRecordingContext* context,
+ SkCanvas* canvas,
+ Layer* layer,
+ const SkRect* srcRect,
+ const SkRect* dstRect,
bool useLayerTransform) {
if (context == nullptr) {
SkDEBUGF(("Attempting to draw LayerDrawable into an unsupported surface"));
diff --git a/libs/hwui/pipeline/skia/LayerDrawable.h b/libs/hwui/pipeline/skia/LayerDrawable.h
index 7cd515ae9fcb..ffbb480023ac 100644
--- a/libs/hwui/pipeline/skia/LayerDrawable.h
+++ b/libs/hwui/pipeline/skia/LayerDrawable.h
@@ -32,8 +32,12 @@ class LayerDrawable : public SkDrawable {
public:
explicit LayerDrawable(DeferredLayerUpdater* layerUpdater) : mLayerUpdater(layerUpdater) {}
- static bool DrawLayer(GrContext* context, SkCanvas* canvas, Layer* layer, const SkRect* srcRect,
- const SkRect* dstRect, bool useLayerTransform);
+ static bool DrawLayer(GrRecordingContext* context,
+ SkCanvas* canvas,
+ Layer* layer,
+ const SkRect* srcRect,
+ const SkRect* dstRect,
+ bool useLayerTransform);
protected:
virtual SkRect onGetBounds() override {
diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
index 00ceb2d84f9e..1473b3e5abb7 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -172,10 +172,12 @@ static bool layerNeedsPaint(const LayerProperties& properties, float alphaMultip
SkPaint* paint) {
paint->setFilterQuality(kLow_SkFilterQuality);
if (alphaMultiplier < 1.0f || properties.alpha() < 255 ||
- properties.xferMode() != SkBlendMode::kSrcOver || properties.getColorFilter() != nullptr) {
+ properties.xferMode() != SkBlendMode::kSrcOver || properties.getColorFilter() != nullptr ||
+ properties.getImageFilter() != nullptr) {
paint->setAlpha(properties.alpha() * alphaMultiplier);
paint->setBlendMode(properties.xferMode());
paint->setColorFilter(sk_ref_sp(properties.getColorFilter()));
+ paint->setImageFilter(sk_ref_sp(properties.getImageFilter()));
return true;
}
return false;
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index 66aa8c203799..3baff7ea8f90 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -15,7 +15,7 @@
*/
#include "ShaderCache.h"
-#include <GrContext.h>
+#include <GrDirectContext.h>
#include <log/log.h>
#include <openssl/sha.h>
#include <algorithm>
@@ -206,7 +206,7 @@ void ShaderCache::store(const SkData& key, const SkData& data) {
}
}
-void ShaderCache::onVkFrameFlushed(GrContext* context) {
+void ShaderCache::onVkFrameFlushed(GrDirectContext* context) {
{
std::lock_guard<std::mutex> lock(mMutex);
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index 0898017d52a1..4dcc9fb49802 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -37,7 +37,7 @@ public:
* "get" returns a pointer to the singleton ShaderCache object. This
* singleton object will never be destroyed.
*/
- ANDROID_API static ShaderCache& get();
+ static ShaderCache& get();
/**
* initShaderDiskCache" loads the serialized cache contents from disk,
@@ -80,7 +80,7 @@ public:
* Pipeline cache is saved on disk only if the size of the data has changed or there was
* a new shader compiled.
*/
- void onVkFrameFlushed(GrContext* context);
+ void onVkFrameFlushed(GrDirectContext* context);
private:
// Creation and (the lack of) destruction is handled internally.
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index 24a6228242a5..389fe7eed7c7 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -87,6 +87,8 @@ bool SkiaOpenGLPipeline::draw(const Frame& frame, const SkRect& screenDirty, con
// Note: The default preference of pixel format is RGBA_8888, when other
// pixel format is available, we should branch out and do more check.
fboInfo.fFormat = GL_RGBA8;
+ } else if (colorType == kRGBA_1010102_SkColorType) {
+ fboInfo.fFormat = GL_RGB10_A2;
} else {
LOG_ALWAYS_FATAL("Unsupported color type.");
}
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index 5088494d6a07..6e7493cb443d 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -35,6 +35,7 @@
#include "VectorDrawable.h"
#include "thread/CommonPool.h"
#include "tools/SkSharingProc.h"
+#include "utils/Color.h"
#include "utils/String8.h"
#include "utils/TraceUtils.h"
@@ -85,7 +86,7 @@ void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry,
}
void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
- sk_sp<GrContext> cachedContext;
+ sk_sp<GrDirectContext> cachedContext;
// Render all layers that need to be updated, in order.
for (size_t i = 0; i < layers.entries().size(); i++) {
@@ -141,11 +142,12 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque)
// cache the current context so that we can defer flushing it until
// either all the layers have been rendered or the context changes
- GrContext* currentContext = layerNode->getLayerSurface()->getCanvas()->getGrContext();
+ GrDirectContext* currentContext =
+ GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext());
if (cachedContext.get() != currentContext) {
if (cachedContext.get()) {
ATRACE_NAME("flush layers (context changed)");
- cachedContext->flush();
+ cachedContext->flushAndSubmit();
}
cachedContext.reset(SkSafeRef(currentContext));
}
@@ -153,7 +155,7 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque)
if (cachedContext.get()) {
ATRACE_NAME("flush layers");
- cachedContext->flush();
+ cachedContext->flushAndSubmit();
}
}
@@ -200,7 +202,7 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator
}
void SkiaPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
- GrContext* context = thread.getGrContext();
+ GrDirectContext* context = thread.getGrContext();
if (context) {
ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height());
auto image = bitmap->makeImage();
@@ -450,7 +452,7 @@ void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& cli
}
ATRACE_NAME("flush commands");
- surface->getCanvas()->flush();
+ surface->flushAndSubmit();
Properties::skpCaptureEnabled = previousSkpEnabled;
}
@@ -587,14 +589,23 @@ void SkiaPipeline::dumpResourceCacheUsage() const {
void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
mColorMode = colorMode;
- if (colorMode == ColorMode::SRGB) {
- mSurfaceColorType = SkColorType::kN32_SkColorType;
- mSurfaceColorSpace = SkColorSpace::MakeSRGB();
- } else if (colorMode == ColorMode::WideColorGamut) {
- mSurfaceColorType = DeviceInfo::get()->getWideColorType();
- mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace();
- } else {
- LOG_ALWAYS_FATAL("Unreachable: unsupported color mode.");
+ switch (colorMode) {
+ case ColorMode::Default:
+ mSurfaceColorType = SkColorType::kN32_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeSRGB();
+ break;
+ case ColorMode::WideColorGamut:
+ mSurfaceColorType = DeviceInfo::get()->getWideColorType();
+ mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace();
+ break;
+ case ColorMode::Hdr:
+ mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020);
+ break;
+ case ColorMode::Hdr10:
+ mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020);
+ break;
}
}
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index 8341164edc19..100bfb6b159a 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -50,7 +50,7 @@ public:
bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
ErrorHandler* errorHandler) override;
- void setSurfaceColorProperties(renderthread::ColorMode colorMode) override;
+ void setSurfaceColorProperties(ColorMode colorMode) override;
SkColorType getSurfaceColorType() const override { return mSurfaceColorType; }
sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; }
@@ -76,7 +76,7 @@ protected:
renderthread::RenderThread& mRenderThread;
- renderthread::ColorMode mColorMode = renderthread::ColorMode::SRGB;
+ ColorMode mColorMode = ColorMode::Default;
SkColorType mSurfaceColorType;
sk_sp<SkColorSpace> mSurfaceColorSpace;
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index d67cf8c9c73f..e292cbdd101f 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -57,7 +57,7 @@ void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, in
uirenderer::DisplayList* SkiaRecordingCanvas::finishRecording() {
// close any existing chunks if necessary
- insertReorderBarrier(false);
+ enableZ(false);
mRecorder.restoreToCount(1);
return mDisplayList.release();
}
@@ -85,8 +85,8 @@ void SkiaRecordingCanvas::drawCircle(uirenderer::CanvasPropertyPrimitive* x,
drawDrawable(mDisplayList->allocateDrawable<AnimatedCircle>(x, y, radius, paint));
}
-void SkiaRecordingCanvas::insertReorderBarrier(bool enableReorder) {
- if (mCurrentBarrier && enableReorder) {
+void SkiaRecordingCanvas::enableZ(bool enableZ) {
+ if (mCurrentBarrier && enableZ) {
// Already in a re-order section, nothing to do
return;
}
@@ -98,7 +98,7 @@ void SkiaRecordingCanvas::insertReorderBarrier(bool enableReorder) {
mCurrentBarrier = nullptr;
drawDrawable(drawable);
}
- if (enableReorder) {
+ if (enableZ) {
mCurrentBarrier =
mDisplayList->allocateDrawable<StartReorderBarrierDrawable>(mDisplayList.get());
drawDrawable(mCurrentBarrier);
@@ -132,23 +132,6 @@ void SkiaRecordingCanvas::drawRenderNode(uirenderer::RenderNode* renderNode) {
}
}
-
-void SkiaRecordingCanvas::callDrawGLFunction(Functor* functor,
- uirenderer::GlFunctorLifecycleListener* listener) {
-#ifdef __ANDROID__ // Layoutlib does not support GL, Vulcan etc.
- FunctorDrawable* functorDrawable;
- if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaVulkan) {
- functorDrawable = mDisplayList->allocateDrawable<VkInteropFunctorDrawable>(
- functor, listener, asSkCanvas());
- } else {
- functorDrawable =
- mDisplayList->allocateDrawable<GLFunctorDrawable>(functor, listener, asSkCanvas());
- }
- mDisplayList->mChildFunctors.push_back(functorDrawable);
- drawDrawable(functorDrawable);
-#endif
-}
-
void SkiaRecordingCanvas::drawWebViewFunctor(int functor) {
#ifdef __ANDROID__ // Layoutlib does not support GL, Vulcan etc.
FunctorDrawable* functorDrawable;
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index bd5274c94e75..83e934974afd 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -69,11 +69,10 @@ public:
virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override;
- virtual void insertReorderBarrier(bool enableReorder) override;
+ virtual void enableZ(bool enableZ) override;
virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) override;
virtual void drawRenderNode(uirenderer::RenderNode* renderNode) override;
- virtual void callDrawGLFunction(Functor* functor,
- uirenderer::GlFunctorLifecycleListener* listener) override;
+
void drawWebViewFunctor(int functor) override;
private:
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index 212a4284a824..ad6363b4452d 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -29,7 +29,7 @@
#include <SkSurface.h>
#include <SkTypes.h>
-#include <GrContext.h>
+#include <GrDirectContext.h>
#include <GrTypes.h>
#include <vk/GrVkTypes.h>
diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
index 68f111752a4c..6efe1762976b 100644
--- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
@@ -20,7 +20,7 @@
#include <GrBackendDrawableInfo.h>
#include <SkAndroidFrameworkUtils.h>
#include <SkImage.h>
-#include "include/private/SkM44.h"
+#include <SkM44.h>
#include <utils/Color.h>
#include <utils/Trace.h>
#include <utils/TraceUtils.h>
@@ -96,7 +96,7 @@ void VkFunctorDrawable::onDraw(SkCanvas* canvas) {
// "VkFunctorDrawable::onDraw" is not invoked for the most common case, when drawing in a GPU
// canvas.
- if (canvas->getGrContext() == nullptr) {
+ if (canvas->recordingContext() == nullptr) {
// We're dumping a picture, render a light-blue rectangle instead
SkPaint paint;
paint.setColor(0xFF81D4FA);
@@ -121,12 +121,7 @@ std::unique_ptr<FunctorDrawable::GpuDrawHandler> VkFunctorDrawable::onSnapGpuDra
return nullptr;
}
std::unique_ptr<VkFunctorDrawHandler> draw;
- if (mAnyFunctor.index() == 0) {
- return std::make_unique<VkFunctorDrawHandler>(std::get<0>(mAnyFunctor).handle, matrix, clip,
- image_info);
- } else {
- LOG_ALWAYS_FATAL("Not implemented");
- }
+ return std::make_unique<VkFunctorDrawHandler>(mWebViewHandle, matrix, clip, image_info);
}
} // namespace skiapipeline
diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.h b/libs/hwui/pipeline/skia/VkFunctorDrawable.h
index d3f97773b91d..fbfc6e76595e 100644
--- a/libs/hwui/pipeline/skia/VkFunctorDrawable.h
+++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.h
@@ -19,7 +19,6 @@
#include "FunctorDrawable.h"
#include <SkImageInfo.h>
-#include <ui/GraphicBuffer.h>
#include <utils/RefBase.h>
namespace android {
diff --git a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp
index 241d3708def5..bc8ce428ce2e 100644
--- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp
@@ -15,23 +15,24 @@
*/
#include "VkInteropFunctorDrawable.h"
-#include <private/hwui/DrawGlInfo.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#include <GLES3/gl3.h>
+#include <private/hwui/DrawGlInfo.h>
#include <utils/Color.h>
+#include <utils/GLUtils.h>
#include <utils/Trace.h>
#include <utils/TraceUtils.h>
+
#include <thread>
+
#include "renderthread/EglManager.h"
#include "thread/ThreadBase.h"
#include "utils/TimeUtils.h"
-#include <EGL/eglext.h>
-#include <GLES2/gl2.h>
-#include <GLES2/gl2ext.h>
-#include <GLES3/gl3.h>
-
-#include <utils/GLUtils.h>
-
namespace android {
namespace uirenderer {
namespace skiapipeline {
@@ -66,7 +67,7 @@ void VkInteropFunctorDrawable::vkInvokeFunctor(Functor* functor) {
void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) {
ATRACE_CALL();
- if (canvas->getGrContext() == nullptr) {
+ if (canvas->recordingContext() == nullptr) {
SkDEBUGF(("Attempting to draw VkInteropFunctor into an unsupported surface"));
return;
}
@@ -75,20 +76,23 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) {
SkImageInfo surfaceInfo = canvas->imageInfo();
- if (!mFrameBuffer.get() || mFBInfo != surfaceInfo) {
+ if (mFrameBuffer == nullptr || mFBInfo != surfaceInfo) {
// Buffer will be used as an OpenGL ES render target.
- mFrameBuffer = new GraphicBuffer(
- // TODO: try to reduce the size of the buffer: possibly by using clip bounds.
- static_cast<uint32_t>(surfaceInfo.width()),
- static_cast<uint32_t>(surfaceInfo.height()),
- ColorTypeToPixelFormat(surfaceInfo.colorType()),
- GraphicBuffer::USAGE_HW_TEXTURE | GraphicBuffer::USAGE_SW_WRITE_NEVER |
- GraphicBuffer::USAGE_SW_READ_NEVER | GraphicBuffer::USAGE_HW_RENDER,
- std::string("VkInteropFunctorDrawable::onDraw pid [") + std::to_string(getpid()) +
- "]");
- status_t error = mFrameBuffer->initCheck();
- if (error < 0) {
- ALOGW("VkInteropFunctorDrawable::onDraw() failed in GraphicBuffer.create()");
+ AHardwareBuffer_Desc desc = {
+ .width = static_cast<uint32_t>(surfaceInfo.width()),
+ .height = static_cast<uint32_t>(surfaceInfo.height()),
+ .layers = 1,
+ .format = ColorTypeToBufferFormat(surfaceInfo.colorType()),
+ .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER |
+ AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER |
+ AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
+ AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER,
+ };
+
+ mFrameBuffer = allocateAHardwareBuffer(desc);
+
+ if (!mFrameBuffer) {
+ ALOGW("VkInteropFunctorDrawable::onDraw() failed in AHardwareBuffer_allocate()");
return;
}
@@ -106,7 +110,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) {
uirenderer::renderthread::EglManager::eglErrorString());
// We use an EGLImage to access the content of the GraphicBuffer
// The EGL image is later bound to a 2D texture
- EGLClientBuffer clientBuffer = (EGLClientBuffer)mFrameBuffer->getNativeBuffer();
+ const EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(mFrameBuffer.get());
AutoEglImage autoImage(display, clientBuffer);
if (autoImage.image == EGL_NO_IMAGE_KHR) {
ALOGW("Could not create EGL image, err =%s",
@@ -121,7 +125,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) {
glBindTexture(GL_TEXTURE_2D, 0);
DrawGlInfo info;
- SkM44 mat4(canvas->experimental_getLocalToDevice());
+ SkM44 mat4(canvas->getLocalToDevice());
SkIRect clipBounds = canvas->getDeviceClipBounds();
info.clipLeft = clipBounds.fLeft;
@@ -151,11 +155,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
- if (mAnyFunctor.index() == 0) {
- std::get<0>(mAnyFunctor).handle->drawGl(info);
- } else {
- (*(std::get<1>(mAnyFunctor).functor))(DrawGlInfo::kModeDraw, &info);
- }
+ mWebViewHandle->drawGl(info);
EGLSyncKHR glDrawFinishedFence =
eglCreateSyncKHR(eglGetCurrentDisplay(), EGL_SYNC_FENCE_KHR, NULL);
@@ -179,22 +179,13 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) {
// drawing into the offscreen surface, so we need to reset it here.
canvas->resetMatrix();
- auto functorImage = SkImage::MakeFromAHardwareBuffer(
- reinterpret_cast<AHardwareBuffer*>(mFrameBuffer.get()), kPremul_SkAlphaType,
- canvas->imageInfo().refColorSpace(), kBottomLeft_GrSurfaceOrigin);
+ auto functorImage = SkImage::MakeFromAHardwareBuffer(mFrameBuffer.get(), kPremul_SkAlphaType,
+ canvas->imageInfo().refColorSpace(),
+ kBottomLeft_GrSurfaceOrigin);
canvas->drawImage(functorImage, 0, 0, &paint);
canvas->restore();
}
-VkInteropFunctorDrawable::~VkInteropFunctorDrawable() {
- if (auto lp = std::get_if<LegacyFunctor>(&mAnyFunctor)) {
- if (lp->listener) {
- ScopedDrawRequest _drawRequest{};
- lp->listener->onGlFunctorReleased(lp->functor);
- }
- }
-}
-
void VkInteropFunctorDrawable::syncFunctor(const WebViewSyncData& data) const {
ScopedDrawRequest _drawRequest{};
FunctorDrawable::syncFunctor(data);
diff --git a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h
index c47ee114263f..e6ea175929c0 100644
--- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h
+++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h
@@ -16,11 +16,12 @@
#pragma once
-#include "FunctorDrawable.h"
-
-#include <ui/GraphicBuffer.h>
+#include <android/hardware_buffer.h>
+#include <utils/NdkUtils.h>
#include <utils/RefBase.h>
+#include "FunctorDrawable.h"
+
namespace android {
namespace uirenderer {
@@ -34,7 +35,7 @@ class VkInteropFunctorDrawable : public FunctorDrawable {
public:
using FunctorDrawable::FunctorDrawable;
- virtual ~VkInteropFunctorDrawable();
+ virtual ~VkInteropFunctorDrawable() {}
static void vkInvokeFunctor(Functor* functor);
@@ -45,7 +46,7 @@ protected:
private:
// Variables below describe/store temporary offscreen buffer used for Vulkan pipeline.
- sp<GraphicBuffer> mFrameBuffer;
+ UniqueAHardwareBuffer mFrameBuffer;
SkImageInfo mFBInfo;
};
diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp
index 1e5877356e8d..85924c5e8939 100644
--- a/libs/hwui/renderthread/CacheManager.cpp
+++ b/libs/hwui/renderthread/CacheManager.cpp
@@ -61,7 +61,7 @@ CacheManager::CacheManager()
SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes);
}
-void CacheManager::reset(sk_sp<GrContext> context) {
+void CacheManager::reset(sk_sp<GrDirectContext> context) {
if (context != mGrContext) {
destroy();
}
@@ -101,7 +101,7 @@ void CacheManager::trimMemory(TrimMemoryMode mode) {
return;
}
- mGrContext->flush();
+ mGrContext->flushAndSubmit();
switch (mode) {
case TrimMemoryMode::Complete:
@@ -122,14 +122,15 @@ void CacheManager::trimMemory(TrimMemoryMode mode) {
// We must sync the cpu to make sure deletions of resources still queued up on the GPU actually
// happen.
- mGrContext->flush(kSyncCpu_GrFlushFlag, 0, nullptr);
+ mGrContext->flush({});
+ mGrContext->submit(true);
}
void CacheManager::trimStaleResources() {
if (!mGrContext) {
return;
}
- mGrContext->flush();
+ mGrContext->flushAndSubmit();
mGrContext->purgeResourcesNotUsedInMs(std::chrono::seconds(30));
}
diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h
index b009cc4f48f2..0a6b8dc26cc3 100644
--- a/libs/hwui/renderthread/CacheManager.h
+++ b/libs/hwui/renderthread/CacheManager.h
@@ -18,7 +18,7 @@
#define CACHEMANAGER_H
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
-#include <GrContext.h>
+#include <GrDirectContext.h>
#endif
#include <SkSurface.h>
#include <utils/String8.h>
@@ -58,13 +58,13 @@ private:
explicit CacheManager();
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
- void reset(sk_sp<GrContext> grContext);
+ void reset(sk_sp<GrDirectContext> grContext);
#endif
void destroy();
const size_t mMaxSurfaceArea;
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
- sk_sp<GrContext> mGrContext;
+ sk_sp<GrDirectContext> mGrContext;
#endif
const size_t mMaxResourceBytes;
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 667a7517a24c..eacabfd1dbf9 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -176,7 +176,10 @@ void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) {
} else {
mNativeSurface = nullptr;
}
+ setupPipelineSurface();
+}
+void CanvasContext::setupPipelineSurface() {
bool hasSurface = mRenderPipeline->setSurface(
mNativeSurface ? mNativeSurface->getNativeWindow() : nullptr, mSwapBehavior);
@@ -187,7 +190,7 @@ void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) {
mFrameNumber = -1;
- if (window != nullptr && hasSurface) {
+ if (mNativeSurface != nullptr && hasSurface) {
mHaveNewSurface = true;
mSwapHistory.clear();
// Enable frame stats after the surface has been bound to the appropriate graphics API.
@@ -242,9 +245,9 @@ void CanvasContext::setOpaque(bool opaque) {
mOpaque = opaque;
}
-void CanvasContext::setWideGamut(bool wideGamut) {
- ColorMode colorMode = wideGamut ? ColorMode::WideColorGamut : ColorMode::SRGB;
- mRenderPipeline->setSurfaceColorProperties(colorMode);
+void CanvasContext::setColorMode(ColorMode mode) {
+ mRenderPipeline->setSurfaceColorProperties(mode);
+ setupPipelineSurface();
}
bool CanvasContext::makeCurrent() {
@@ -462,6 +465,7 @@ void CanvasContext::draw() {
mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
// Notify the callbacks, even if there's nothing to draw so they aren't waiting
// indefinitely
+ waitOnFences();
for (auto& func : mFrameCompleteCallbacks) {
std::invoke(func, mFrameNumber);
}
@@ -484,6 +488,14 @@ void CanvasContext::draw() {
waitOnFences();
+ if (mNativeSurface) {
+ // TODO(b/165985262): measure performance impact
+ if (const auto vsyncId = mCurrentFrameInfo->get(FrameInfoIndex::FrameTimelineVsyncId);
+ vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) {
+ native_window_set_frame_timeline_vsync(mNativeSurface->getNativeWindow(), vsyncId);
+ }
+ }
+
bool requireSwap = false;
int error = OK;
bool didSwap =
@@ -617,8 +629,12 @@ void CanvasContext::prepareAndDraw(RenderNode* node) {
ATRACE_CALL();
nsecs_t vsync = mRenderThread.timeLord().computeFrameTimeNanos();
+ int64_t vsyncId = mRenderThread.timeLord().lastVsyncId();
+ int64_t frameDeadline = mRenderThread.timeLord().lastFrameDeadline();
int64_t frameInfo[UI_THREAD_FRAME_INFO_SIZE];
- UiFrameInfoBuilder(frameInfo).addFlag(FrameInfoFlags::RTAnimation).setVsync(vsync, vsync);
+ UiFrameInfoBuilder(frameInfo)
+ .addFlag(FrameInfoFlags::RTAnimation)
+ .setVsync(vsync, vsync, vsyncId, frameDeadline);
TreeInfo info(TreeInfo::MODE_RT_ONLY, *this);
prepareTree(info, frameInfo, systemTime(SYSTEM_TIME_MONOTONIC), node);
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 0f1b8aebf56c..cc4eb3285bbe 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -30,6 +30,7 @@
#include "renderthread/RenderTask.h"
#include "renderthread/RenderThread.h"
#include "utils/RingBuffer.h"
+#include "ColorMode.h"
#include <SkBitmap.h>
#include <SkRect.h>
@@ -105,7 +106,7 @@ public:
* If Properties::isSkiaEnabled() is true then this will return the Skia
* grContext associated with the current RenderPipeline.
*/
- GrContext* getGrContext() const { return mRenderThread.getGrContext(); }
+ GrDirectContext* getGrContext() const { return mRenderThread.getGrContext(); }
// Won't take effect until next EGLSurface creation
void setSwapBehavior(SwapBehavior swapBehavior);
@@ -119,7 +120,7 @@ public:
void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
void setLightGeometry(const Vector3& lightCenter, float lightRadius);
void setOpaque(bool opaque);
- void setWideGamut(bool wideGamut);
+ void setColorMode(ColorMode mode);
bool makeCurrent();
void prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued, RenderNode* target);
void draw();
@@ -170,9 +171,9 @@ public:
}
// Used to queue up work that needs to be completed before this frame completes
- ANDROID_API void enqueueFrameWork(std::function<void()>&& func);
+ void enqueueFrameWork(std::function<void()>&& func);
- ANDROID_API int64_t getFrameNumber();
+ int64_t getFrameNumber();
void waitOnFences();
@@ -211,6 +212,7 @@ private:
bool isSwapChainStuffed();
bool surfaceRequiresRedraw();
void setPresentTime();
+ void setupPipelineSurface();
SkRect computeDirtyRect(const Frame& frame, SkRect* dirty);
diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp
index 1e593388d063..c9146b2fc2d1 100644
--- a/libs/hwui/renderthread/DrawFrameTask.cpp
+++ b/libs/hwui/renderthread/DrawFrameTask.cpp
@@ -128,7 +128,10 @@ void DrawFrameTask::run() {
bool DrawFrameTask::syncFrameState(TreeInfo& info) {
ATRACE_CALL();
int64_t vsync = mFrameInfo[static_cast<int>(FrameInfoIndex::Vsync)];
- mRenderThread->timeLord().vsyncReceived(vsync);
+ int64_t intendedVsync = mFrameInfo[static_cast<int>(FrameInfoIndex::IntendedVsync)];
+ int64_t vsyncId = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameTimelineVsyncId)];
+ int64_t frameDeadline = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameDeadline)];
+ mRenderThread->timeLord().vsyncReceived(vsync, intendedVsync, vsyncId, frameDeadline);
bool canDraw = mContext->makeCurrent();
mContext->unpinImages();
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 7982ab664c1b..a11678189bad 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -76,6 +76,7 @@ static struct {
bool glColorSpace = false;
bool scRGB = false;
bool displayP3 = false;
+ bool hdr = false;
bool contextPriority = false;
bool surfacelessContext = false;
bool nativeFenceSync = false;
@@ -86,7 +87,8 @@ static struct {
EglManager::EglManager()
: mEglDisplay(EGL_NO_DISPLAY)
, mEglConfig(nullptr)
- , mEglConfigWideGamut(nullptr)
+ , mEglConfigF16(nullptr)
+ , mEglConfig1010102(nullptr)
, mEglContext(EGL_NO_CONTEXT)
, mPBufferSurface(EGL_NO_SURFACE)
, mCurrentSurface(EGL_NO_SURFACE)
@@ -136,15 +138,14 @@ void EglManager::initialize() {
LOG_ALWAYS_FATAL_IF(!DeviceInfo::get()->getWideColorSpace()->toXYZD50(&wideColorGamut),
"Could not get gamut matrix from wideColorSpace");
bool hasWideColorSpaceExtension = false;
- if (memcmp(&wideColorGamut, &SkNamedGamut::kDCIP3, sizeof(wideColorGamut)) == 0) {
+ if (memcmp(&wideColorGamut, &SkNamedGamut::kDisplayP3, sizeof(wideColorGamut)) == 0) {
hasWideColorSpaceExtension = EglExtensions.displayP3;
} else if (memcmp(&wideColorGamut, &SkNamedGamut::kSRGB, sizeof(wideColorGamut)) == 0) {
hasWideColorSpaceExtension = EglExtensions.scRGB;
} else {
LOG_ALWAYS_FATAL("Unsupported wide color space.");
}
- mHasWideColorGamutSupport = EglExtensions.glColorSpace && hasWideColorSpaceExtension &&
- mEglConfigWideGamut != EGL_NO_CONFIG_KHR;
+ mHasWideColorGamutSupport = EglExtensions.glColorSpace && hasWideColorSpaceExtension;
}
EGLConfig EglManager::load8BitsConfig(EGLDisplay display, EglManager::SwapBehavior swapBehavior) {
@@ -177,6 +178,35 @@ EGLConfig EglManager::load8BitsConfig(EGLDisplay display, EglManager::SwapBehavi
return config;
}
+EGLConfig EglManager::load1010102Config(EGLDisplay display, SwapBehavior swapBehavior) {
+ EGLint eglSwapBehavior =
+ (swapBehavior == SwapBehavior::Preserved) ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0;
+ // If we reached this point, we have a valid swap behavior
+ EGLint attribs[] = {EGL_RENDERABLE_TYPE,
+ EGL_OPENGL_ES2_BIT,
+ EGL_RED_SIZE,
+ 10,
+ EGL_GREEN_SIZE,
+ 10,
+ EGL_BLUE_SIZE,
+ 10,
+ EGL_ALPHA_SIZE,
+ 2,
+ EGL_DEPTH_SIZE,
+ 0,
+ EGL_STENCIL_SIZE,
+ STENCIL_BUFFER_SIZE,
+ EGL_SURFACE_TYPE,
+ EGL_WINDOW_BIT | eglSwapBehavior,
+ EGL_NONE};
+ EGLConfig config = EGL_NO_CONFIG_KHR;
+ EGLint numConfigs = 1;
+ if (!eglChooseConfig(display, attribs, &config, numConfigs, &numConfigs) || numConfigs != 1) {
+ return EGL_NO_CONFIG_KHR;
+ }
+ return config;
+}
+
EGLConfig EglManager::loadFP16Config(EGLDisplay display, SwapBehavior swapBehavior) {
EGLint eglSwapBehavior =
(swapBehavior == SwapBehavior::Preserved) ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0;
@@ -208,12 +238,8 @@ EGLConfig EglManager::loadFP16Config(EGLDisplay display, SwapBehavior swapBehavi
return config;
}
-extern "C" EGLAPI const char* eglQueryStringImplementationANDROID(EGLDisplay dpy, EGLint name);
-
void EglManager::initExtensions() {
auto extensions = StringUtils::split(eglQueryString(mEglDisplay, EGL_EXTENSIONS));
- auto extensionsAndroid =
- StringUtils::split(eglQueryStringImplementationANDROID(mEglDisplay, EGL_EXTENSIONS));
// For our purposes we don't care if EGL_BUFFER_AGE is a result of
// EGL_EXT_buffer_age or EGL_KHR_partial_update as our usage is covered
@@ -230,14 +256,12 @@ void EglManager::initExtensions() {
EglExtensions.pixelFormatFloat = extensions.has("EGL_EXT_pixel_format_float");
EglExtensions.scRGB = extensions.has("EGL_EXT_gl_colorspace_scrgb");
EglExtensions.displayP3 = extensions.has("EGL_EXT_gl_colorspace_display_p3_passthrough");
+ EglExtensions.hdr = extensions.has("EGL_EXT_gl_colorspace_bt2020_pq");
EglExtensions.contextPriority = extensions.has("EGL_IMG_context_priority");
EglExtensions.surfacelessContext = extensions.has("EGL_KHR_surfaceless_context");
EglExtensions.fenceSync = extensions.has("EGL_KHR_fence_sync");
EglExtensions.waitSync = extensions.has("EGL_KHR_wait_sync");
-
- // EGL_ANDROID_native_fence_sync is not exposed to applications, so access
- // this through the private Android-specific query instead.
- EglExtensions.nativeFenceSync = extensionsAndroid.has("EGL_ANDROID_native_fence_sync");
+ EglExtensions.nativeFenceSync = extensions.has("EGL_ANDROID_native_fence_sync");
}
bool EglManager::hasEglContext() {
@@ -260,18 +284,20 @@ void EglManager::loadConfigs() {
LOG_ALWAYS_FATAL("Failed to choose config, error = %s", eglErrorString());
}
}
- SkColorType wideColorType = DeviceInfo::get()->getWideColorType();
// When we reach this point, we have a valid swap behavior
- if (wideColorType == SkColorType::kRGBA_F16_SkColorType && EglExtensions.pixelFormatFloat) {
- mEglConfigWideGamut = loadFP16Config(mEglDisplay, mSwapBehavior);
- if (mEglConfigWideGamut == EGL_NO_CONFIG_KHR) {
+ if (EglExtensions.pixelFormatFloat) {
+ mEglConfigF16 = loadFP16Config(mEglDisplay, mSwapBehavior);
+ if (mEglConfigF16 == EGL_NO_CONFIG_KHR) {
ALOGE("Device claims wide gamut support, cannot find matching config, error = %s",
eglErrorString());
EglExtensions.pixelFormatFloat = false;
}
- } else if (wideColorType == SkColorType::kN32_SkColorType) {
- mEglConfigWideGamut = load8BitsConfig(mEglDisplay, mSwapBehavior);
+ }
+ mEglConfig1010102 = load1010102Config(mEglDisplay, mSwapBehavior);
+ if (mEglConfig1010102 == EGL_NO_CONFIG_KHR) {
+ ALOGW("Failed to initialize 101010-2 format, error = %s",
+ eglErrorString());
}
}
@@ -311,8 +337,9 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window,
sk_sp<SkColorSpace> colorSpace) {
LOG_ALWAYS_FATAL_IF(!hasEglContext(), "Not initialized");
- bool wideColorGamut = colorMode == ColorMode::WideColorGamut && mHasWideColorGamutSupport &&
- EglExtensions.noConfigContext;
+ if (!mHasWideColorGamutSupport || !EglExtensions.noConfigContext) {
+ colorMode = ColorMode::Default;
+ }
// The color space we want to use depends on whether linear blending is turned
// on and whether the app has requested wide color gamut rendering. When wide
@@ -338,26 +365,47 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window,
// list is considered empty if the first entry is EGL_NONE
EGLint attribs[] = {EGL_NONE, EGL_NONE, EGL_NONE};
+ EGLConfig config = mEglConfig;
+ if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) {
+ if (mEglConfigF16 == EGL_NO_CONFIG_KHR) {
+ colorMode = ColorMode::Default;
+ } else {
+ config = mEglConfigF16;
+ }
+ }
if (EglExtensions.glColorSpace) {
attribs[0] = EGL_GL_COLORSPACE_KHR;
- if (wideColorGamut) {
- skcms_Matrix3x3 colorGamut;
- LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut),
- "Could not get gamut matrix from color space");
- if (memcmp(&colorGamut, &SkNamedGamut::kDCIP3, sizeof(colorGamut)) == 0) {
- attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT;
- } else if (memcmp(&colorGamut, &SkNamedGamut::kSRGB, sizeof(colorGamut)) == 0) {
- attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT;
- } else {
- LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space.");
+ switch (colorMode) {
+ case ColorMode::Default:
+ attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR;
+ break;
+ case ColorMode::WideColorGamut: {
+ skcms_Matrix3x3 colorGamut;
+ LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut),
+ "Could not get gamut matrix from color space");
+ if (memcmp(&colorGamut, &SkNamedGamut::kDisplayP3, sizeof(colorGamut)) == 0) {
+ attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT;
+ } else if (memcmp(&colorGamut, &SkNamedGamut::kSRGB, sizeof(colorGamut)) == 0) {
+ attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT;
+ } else if (memcmp(&colorGamut, &SkNamedGamut::kRec2020, sizeof(colorGamut)) == 0) {
+ attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
+ } else {
+ LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space.");
+ }
+ break;
}
- } else {
- attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR;
+ case ColorMode::Hdr:
+ config = mEglConfigF16;
+ attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
+ break;
+ case ColorMode::Hdr10:
+ config = mEglConfig1010102;
+ attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
+ break;
}
}
- EGLSurface surface = eglCreateWindowSurface(
- mEglDisplay, wideColorGamut ? mEglConfigWideGamut : mEglConfig, window, attribs);
+ EGLSurface surface = eglCreateWindowSurface(mEglDisplay, config, window, attribs);
if (surface == EGL_NO_SURFACE) {
return Error<EGLint>{eglGetError()};
}
diff --git a/libs/hwui/renderthread/EglManager.h b/libs/hwui/renderthread/EglManager.h
index a893e245b214..69f3ed014c53 100644
--- a/libs/hwui/renderthread/EglManager.h
+++ b/libs/hwui/renderthread/EglManager.h
@@ -21,7 +21,6 @@
#include <SkImageInfo.h>
#include <SkRect.h>
#include <cutils/compiler.h>
-#include <ui/GraphicBuffer.h>
#include <utils/StrongPointer.h>
#include "IRenderPipeline.h"
@@ -89,6 +88,7 @@ private:
static EGLConfig load8BitsConfig(EGLDisplay display, SwapBehavior swapBehavior);
static EGLConfig loadFP16Config(EGLDisplay display, SwapBehavior swapBehavior);
+ static EGLConfig load1010102Config(EGLDisplay display, SwapBehavior swapBehavior);
void initExtensions();
void createPBufferSurface();
@@ -98,7 +98,8 @@ private:
EGLDisplay mEglDisplay;
EGLConfig mEglConfig;
- EGLConfig mEglConfigWideGamut;
+ EGLConfig mEglConfigF16;
+ EGLConfig mEglConfig1010102;
EGLContext mEglContext;
EGLSurface mPBufferSurface;
EGLSurface mCurrentSurface;
diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h
index c3c22869a42f..aceb5a528fc8 100644
--- a/libs/hwui/renderthread/IRenderPipeline.h
+++ b/libs/hwui/renderthread/IRenderPipeline.h
@@ -22,11 +22,12 @@
#include "Lighting.h"
#include "SwapBehavior.h"
#include "hwui/Bitmap.h"
+#include "ColorMode.h"
#include <SkRect.h>
#include <utils/RefBase.h>
-class GrContext;
+class GrDirectContext;
struct ANativeWindow;
@@ -42,16 +43,6 @@ namespace renderthread {
enum class MakeCurrentResult { AlreadyCurrent, Failed, Succeeded };
-enum class ColorMode {
- // SRGB means HWUI will produce buffer in SRGB color space.
- SRGB,
- // WideColorGamut means HWUI would support rendering scRGB non-linear into
- // a signed buffer with enough range to support the wide color gamut of the
- // display.
- WideColorGamut,
- // Hdr
-};
-
class Frame;
class IRenderPipeline {
diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp
index dcf1fc189588..c29cc11fa7ea 100644
--- a/libs/hwui/renderthread/ReliableSurface.cpp
+++ b/libs/hwui/renderthread/ReliableSurface.cpp
@@ -149,21 +149,25 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) {
return AHardwareBuffer_to_ANativeWindowBuffer(mScratchBuffer.get());
}
- AHardwareBuffer_Desc desc;
- desc.usage = mUsage;
- desc.format = mFormat;
- desc.width = 1;
- desc.height = 1;
- desc.layers = 1;
- desc.rfu0 = 0;
- desc.rfu1 = 0;
- AHardwareBuffer* newBuffer = nullptr;
- int err = AHardwareBuffer_allocate(&desc, &newBuffer);
- if (err) {
+ AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{
+ .usage = mUsage,
+ .format = mFormat,
+ .width = 1,
+ .height = 1,
+ .layers = 1,
+ .rfu0 = 0,
+ .rfu1 = 0,
+ };
+
+ AHardwareBuffer* newBuffer;
+ int result = AHardwareBuffer_allocate(&desc, &newBuffer);
+
+ if (result != NO_ERROR) {
// Allocate failed, that sucks
- ALOGW("Failed to allocate scratch buffer, error=%d", err);
+ ALOGW("Failed to allocate scratch buffer, error=%d", result);
return nullptr;
}
+
mScratchBuffer.reset(newBuffer);
return AHardwareBuffer_to_ANativeWindowBuffer(newBuffer);
}
diff --git a/libs/hwui/renderthread/ReliableSurface.h b/libs/hwui/renderthread/ReliableSurface.h
index f699eb1fe6b3..41969e776fc8 100644
--- a/libs/hwui/renderthread/ReliableSurface.h
+++ b/libs/hwui/renderthread/ReliableSurface.h
@@ -21,6 +21,7 @@
#include <apex/window.h>
#include <utils/Errors.h>
#include <utils/Macros.h>
+#include <utils/NdkUtils.h>
#include <utils/StrongPointer.h>
#include <memory>
@@ -67,8 +68,7 @@ private:
uint64_t mUsage = AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER;
AHardwareBuffer_Format mFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
- std::unique_ptr<AHardwareBuffer, void (*)(AHardwareBuffer*)> mScratchBuffer{
- nullptr, AHardwareBuffer_release};
+ UniqueAHardwareBuffer mScratchBuffer;
ANativeWindowBuffer* mReservedBuffer = nullptr;
base::unique_fd mReservedFenceFd;
bool mHasDequeuedBuffer = false;
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index b66a13d1efda..b51f6dcfc66f 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -77,10 +77,10 @@ void RenderProxy::setName(const char* name) {
}
void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) {
- ANativeWindow_acquire(window);
+ if (window) { ANativeWindow_acquire(window); }
mRenderThread.queue().post([this, win = window, enableTimeout]() mutable {
mContext->setSurface(win, enableTimeout);
- ANativeWindow_release(win);
+ if (win) { ANativeWindow_release(win); }
});
}
@@ -109,8 +109,8 @@ void RenderProxy::setOpaque(bool opaque) {
mRenderThread.queue().post([=]() { mContext->setOpaque(opaque); });
}
-void RenderProxy::setWideGamut(bool wideGamut) {
- mRenderThread.queue().post([=]() { mContext->setWideGamut(wideGamut); });
+void RenderProxy::setColorMode(ColorMode mode) {
+ mRenderThread.queue().post([=]() { mContext->setColorMode(mode); });
}
int64_t* RenderProxy::frameInfo() {
@@ -128,20 +128,6 @@ void RenderProxy::destroy() {
mRenderThread.queue().runSync([=]() { mContext->destroy(); });
}
-void RenderProxy::invokeFunctor(Functor* functor, bool waitForCompletion) {
- ATRACE_CALL();
- RenderThread& thread = RenderThread::getInstance();
- auto invoke = [&thread, functor]() { CanvasContext::invokeFunctor(thread, functor); };
- if (waitForCompletion) {
- // waitForCompletion = true is expected to be fairly rare and only
- // happen in destruction. Thus it should be fine to temporarily
- // create a Mutex
- thread.queue().runSync(std::move(invoke));
- } else {
- thread.queue().post(std::move(invoke));
- }
-}
-
void RenderProxy::destroyFunctor(int functor) {
ATRACE_CALL();
RenderThread& thread = RenderThread::getInstance();
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 3baeb2f7a476..33dabc9895b1 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -24,6 +24,7 @@
#include "../FrameMetricsObserver.h"
#include "../IContextFactory.h"
+#include "ColorMode.h"
#include "DrawFrameTask.h"
#include "SwapBehavior.h"
#include "hwui/Bitmap.h"
@@ -60,69 +61,67 @@ enum {
* references RenderProxy fields. This is safe as RenderProxy cannot
* be deleted if it is blocked inside a call.
*/
-class ANDROID_API RenderProxy {
+class RenderProxy {
public:
- ANDROID_API RenderProxy(bool opaque, RenderNode* rootNode, IContextFactory* contextFactory);
- ANDROID_API virtual ~RenderProxy();
+ RenderProxy(bool opaque, RenderNode* rootNode, IContextFactory* contextFactory);
+ virtual ~RenderProxy();
// Won't take effect until next EGLSurface creation
- ANDROID_API void setSwapBehavior(SwapBehavior swapBehavior);
- ANDROID_API bool loadSystemProperties();
- ANDROID_API void setName(const char* name);
-
- ANDROID_API void setSurface(ANativeWindow* window, bool enableTimeout = true);
- ANDROID_API void allocateBuffers();
- ANDROID_API bool pause();
- ANDROID_API void setStopped(bool stopped);
- ANDROID_API void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
- ANDROID_API void setLightGeometry(const Vector3& lightCenter, float lightRadius);
- ANDROID_API void setOpaque(bool opaque);
- ANDROID_API void setWideGamut(bool wideGamut);
- ANDROID_API int64_t* frameInfo();
- ANDROID_API int syncAndDrawFrame();
- ANDROID_API void destroy();
-
- ANDROID_API static void invokeFunctor(Functor* functor, bool waitForCompletion);
+ void setSwapBehavior(SwapBehavior swapBehavior);
+ bool loadSystemProperties();
+ void setName(const char* name);
+
+ void setSurface(ANativeWindow* window, bool enableTimeout = true);
+ void allocateBuffers();
+ bool pause();
+ void setStopped(bool stopped);
+ void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
+ void setLightGeometry(const Vector3& lightCenter, float lightRadius);
+ void setOpaque(bool opaque);
+ void setColorMode(ColorMode mode);
+ int64_t* frameInfo();
+ int syncAndDrawFrame();
+ void destroy();
+
static void destroyFunctor(int functor);
- ANDROID_API DeferredLayerUpdater* createTextureLayer();
- ANDROID_API void buildLayer(RenderNode* node);
- ANDROID_API bool copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap);
- ANDROID_API void pushLayerUpdate(DeferredLayerUpdater* layer);
- ANDROID_API void cancelLayerUpdate(DeferredLayerUpdater* layer);
- ANDROID_API void detachSurfaceTexture(DeferredLayerUpdater* layer);
+ DeferredLayerUpdater* createTextureLayer();
+ void buildLayer(RenderNode* node);
+ bool copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap);
+ void pushLayerUpdate(DeferredLayerUpdater* layer);
+ void cancelLayerUpdate(DeferredLayerUpdater* layer);
+ void detachSurfaceTexture(DeferredLayerUpdater* layer);
- ANDROID_API void destroyHardwareResources();
- ANDROID_API static void trimMemory(int level);
- ANDROID_API static void overrideProperty(const char* name, const char* value);
+ void destroyHardwareResources();
+ static void trimMemory(int level);
+ static void overrideProperty(const char* name, const char* value);
- ANDROID_API void fence();
- ANDROID_API static int maxTextureSize();
- ANDROID_API void stopDrawing();
- ANDROID_API void notifyFramePending();
+ void fence();
+ static int maxTextureSize();
+ void stopDrawing();
+ void notifyFramePending();
- ANDROID_API void dumpProfileInfo(int fd, int dumpFlags);
+ void dumpProfileInfo(int fd, int dumpFlags);
// Not exported, only used for testing
void resetProfileInfo();
uint32_t frameTimePercentile(int p);
- ANDROID_API static void dumpGraphicsMemory(int fd);
+ static void dumpGraphicsMemory(int fd);
- ANDROID_API static void rotateProcessStatsBuffer();
- ANDROID_API static void setProcessStatsBuffer(int fd);
- ANDROID_API int getRenderThreadTid();
+ static void rotateProcessStatsBuffer();
+ static void setProcessStatsBuffer(int fd);
+ int getRenderThreadTid();
- ANDROID_API void addRenderNode(RenderNode* node, bool placeFront);
- ANDROID_API void removeRenderNode(RenderNode* node);
- ANDROID_API void drawRenderNode(RenderNode* node);
- ANDROID_API void setContentDrawBounds(int left, int top, int right, int bottom);
- ANDROID_API void setPictureCapturedCallback(
- const std::function<void(sk_sp<SkPicture>&&)>& callback);
- ANDROID_API void setFrameCallback(std::function<void(int64_t)>&& callback);
- ANDROID_API void setFrameCompleteCallback(std::function<void(int64_t)>&& callback);
+ void addRenderNode(RenderNode* node, bool placeFront);
+ void removeRenderNode(RenderNode* node);
+ void drawRenderNode(RenderNode* node);
+ void setContentDrawBounds(int left, int top, int right, int bottom);
+ void setPictureCapturedCallback(const std::function<void(sk_sp<SkPicture>&&)>& callback);
+ void setFrameCallback(std::function<void(int64_t)>&& callback);
+ void setFrameCompleteCallback(std::function<void(int64_t)>&& callback);
- ANDROID_API void addFrameMetricsObserver(FrameMetricsObserver* observer);
- ANDROID_API void removeFrameMetricsObserver(FrameMetricsObserver* observer);
- ANDROID_API void setForceDark(bool enable);
+ void addFrameMetricsObserver(FrameMetricsObserver* observer);
+ void removeFrameMetricsObserver(FrameMetricsObserver* observer);
+ void setForceDark(bool enable);
/**
* Sets a render-ahead depth on the backing renderer. This will increase latency by
@@ -139,17 +138,17 @@ public:
*
* @param renderAhead How far to render ahead, must be in the range [0..2]
*/
- ANDROID_API void setRenderAheadDepth(int renderAhead);
+ void setRenderAheadDepth(int renderAhead);
- ANDROID_API static int copySurfaceInto(ANativeWindow* window, int left, int top, int right,
+ static int copySurfaceInto(ANativeWindow* window, int left, int top, int right,
int bottom, SkBitmap* bitmap);
- ANDROID_API static void prepareToDraw(Bitmap& bitmap);
+ static void prepareToDraw(Bitmap& bitmap);
static int copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap);
- ANDROID_API static void disableVsync();
+ static void disableVsync();
- ANDROID_API static void preload();
+ static void preload();
private:
RenderThread& mRenderThread;
diff --git a/libs/hwui/renderthread/RenderTask.h b/libs/hwui/renderthread/RenderTask.h
index c56a3578ad58..3e3a381d65fe 100644
--- a/libs/hwui/renderthread/RenderTask.h
+++ b/libs/hwui/renderthread/RenderTask.h
@@ -45,12 +45,12 @@ namespace renderthread {
* malloc/free churn of small objects?
*/
-class ANDROID_API RenderTask {
+class RenderTask {
public:
- ANDROID_API RenderTask() : mNext(nullptr), mRunAt(0) {}
- ANDROID_API virtual ~RenderTask() {}
+ RenderTask() : mNext(nullptr), mRunAt(0) {}
+ virtual ~RenderTask() {}
- ANDROID_API virtual void run() = 0;
+ virtual void run() = 0;
RenderTask* mNext;
nsecs_t mRunAt; // nano-seconds on the SYSTEM_TIME_MONOTONIC clock
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 206b58f62ea7..a101d46f6da0 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -51,8 +51,11 @@ static JVMAttachHook gOnStartHook = nullptr;
void RenderThread::frameCallback(int64_t frameTimeNanos, void* data) {
RenderThread* rt = reinterpret_cast<RenderThread*>(data);
+ int64_t vsyncId = AChoreographer_getVsyncId(rt->mChoreographer);
+ int64_t frameDeadline = AChoreographer_getFrameDeadline(rt->mChoreographer);
rt->mVsyncRequested = false;
- if (rt->timeLord().vsyncReceived(frameTimeNanos) && !rt->mFrameCallbackTaskPending) {
+ if (rt->timeLord().vsyncReceived(frameTimeNanos, frameTimeNanos, vsyncId, frameDeadline) &&
+ !rt->mFrameCallbackTaskPending) {
ATRACE_NAME("queue mFrameCallbackTask");
rt->mFrameCallbackTaskPending = true;
nsecs_t runAt = (frameTimeNanos + rt->mDispatchFrameDelay);
@@ -131,8 +134,7 @@ RenderThread::RenderThread()
, mFrameCallbackTaskPending(false)
, mRenderState(nullptr)
, mEglManager(nullptr)
- , mFunctorManager(WebViewFunctorManager::instance())
- , mVkManager(nullptr) {
+ , mFunctorManager(WebViewFunctorManager::instance()) {
Properties::load();
start("RenderThread");
}
@@ -166,7 +168,7 @@ void RenderThread::initThreadLocals() {
initializeChoreographer();
mEglManager = new EglManager();
mRenderState = new RenderState(*this);
- mVkManager = new VulkanManager();
+ mVkManager = VulkanManager::getInstance();
mCacheManager = new CacheManager();
}
@@ -190,13 +192,14 @@ void RenderThread::requireGlContext() {
auto glesVersion = reinterpret_cast<const char*>(glGetString(GL_VERSION));
auto size = glesVersion ? strlen(glesVersion) : -1;
cacheManager().configureContext(&options, glesVersion, size);
- sk_sp<GrContext> grContext(GrContext::MakeGL(std::move(glInterface), options));
+ sk_sp<GrDirectContext> grContext(GrDirectContext::MakeGL(std::move(glInterface), options));
LOG_ALWAYS_FATAL_IF(!grContext.get());
setGrContext(grContext);
}
void RenderThread::requireVkContext() {
- if (mVkManager->hasVkContext()) {
+ // the getter creates the context in the event it had been destroyed by destroyRenderingContext
+ if (vulkanManager().hasVkContext()) {
return;
}
mVkManager->initialize();
@@ -204,7 +207,7 @@ void RenderThread::requireVkContext() {
initGrContextOptions(options);
auto vkDriverVersion = mVkManager->getDriverVersion();
cacheManager().configureContext(&options, &vkDriverVersion, sizeof(vkDriverVersion));
- sk_sp<GrContext> grContext = mVkManager->createContext(options);
+ sk_sp<GrDirectContext> grContext = mVkManager->createContext(options);
LOG_ALWAYS_FATAL_IF(!grContext.get());
setGrContext(grContext);
}
@@ -222,11 +225,16 @@ void RenderThread::destroyRenderingContext() {
mEglManager->destroy();
}
} else {
- if (vulkanManager().hasVkContext()) {
- setGrContext(nullptr);
- vulkanManager().destroy();
- }
+ setGrContext(nullptr);
+ mVkManager.clear();
+ }
+}
+
+VulkanManager& RenderThread::vulkanManager() {
+ if (!mVkManager.get()) {
+ mVkManager = VulkanManager::getInstance();
}
+ return *mVkManager.get();
}
void RenderThread::dumpGraphicsMemory(int fd) {
@@ -263,7 +271,7 @@ Readback& RenderThread::readback() {
return *mReadback;
}
-void RenderThread::setGrContext(sk_sp<GrContext> context) {
+void RenderThread::setGrContext(sk_sp<GrDirectContext> context) {
mCacheManager->reset(context);
if (mGrContext) {
mRenderState->onContextDestroyed();
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index 8be46a6d16e1..4fbb07168ac0 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -17,10 +17,10 @@
#ifndef RENDERTHREAD_H_
#define RENDERTHREAD_H_
-#include <GrContext.h>
+#include <GrDirectContext.h>
#include <SkBitmap.h>
-#include <apex/choreographer.h>
#include <cutils/compiler.h>
+#include <private/android/choreographer.h>
#include <thread/ThreadBase.h>
#include <utils/Looper.h>
#include <utils/Thread.h>
@@ -88,7 +88,7 @@ class RenderThread : private ThreadBase {
public:
// Sets a callback that fires before any RenderThread setup has occurred.
- ANDROID_API static void setOnStartHook(JVMAttachHook onStartHook);
+ static void setOnStartHook(JVMAttachHook onStartHook);
static JVMAttachHook getOnStartHook();
WorkQueue& queue() { return ThreadBase::queue(); }
@@ -106,11 +106,11 @@ public:
ProfileDataContainer& globalProfileData() { return mGlobalProfileData; }
Readback& readback();
- GrContext* getGrContext() const { return mGrContext.get(); }
- void setGrContext(sk_sp<GrContext> cxt);
+ GrDirectContext* getGrContext() const { return mGrContext.get(); }
+ void setGrContext(sk_sp<GrDirectContext> cxt);
CacheManager& cacheManager() { return *mCacheManager; }
- VulkanManager& vulkanManager() { return *mVkManager; }
+ VulkanManager& vulkanManager();
sk_sp<Bitmap> allocateHardwareBitmap(SkBitmap& skBitmap);
void dumpGraphicsMemory(int fd);
@@ -186,9 +186,9 @@ private:
ProfileDataContainer mGlobalProfileData;
Readback* mReadback = nullptr;
- sk_sp<GrContext> mGrContext;
+ sk_sp<GrDirectContext> mGrContext;
CacheManager* mCacheManager;
- VulkanManager* mVkManager;
+ sp<VulkanManager> mVkManager;
};
} /* namespace renderthread */
diff --git a/libs/hwui/renderthread/TimeLord.cpp b/libs/hwui/renderthread/TimeLord.cpp
index 784068f1d877..abb633028363 100644
--- a/libs/hwui/renderthread/TimeLord.cpp
+++ b/libs/hwui/renderthread/TimeLord.cpp
@@ -14,14 +14,26 @@
* limitations under the License.
*/
#include "TimeLord.h"
+#include <limits>
namespace android {
namespace uirenderer {
namespace renderthread {
-TimeLord::TimeLord() : mFrameIntervalNanos(milliseconds_to_nanoseconds(16)), mFrameTimeNanos(0) {}
+TimeLord::TimeLord() : mFrameIntervalNanos(milliseconds_to_nanoseconds(16)),
+ mFrameTimeNanos(0),
+ mFrameIntendedTimeNanos(0),
+ mFrameVsyncId(-1),
+ mFrameDeadline(std::numeric_limits<int64_t>::max()){}
+
+bool TimeLord::vsyncReceived(nsecs_t vsync, nsecs_t intendedVsync, int64_t vsyncId,
+ int64_t frameDeadline) {
+ if (intendedVsync > mFrameIntendedTimeNanos) {
+ mFrameIntendedTimeNanos = intendedVsync;
+ mFrameVsyncId = vsyncId;
+ mFrameDeadline = frameDeadline;
+ }
-bool TimeLord::vsyncReceived(nsecs_t vsync) {
if (vsync > mFrameTimeNanos) {
mFrameTimeNanos = vsync;
return true;
@@ -36,6 +48,8 @@ nsecs_t TimeLord::computeFrameTimeNanos() {
if (jitterNanos >= mFrameIntervalNanos) {
nsecs_t lastFrameOffset = jitterNanos % mFrameIntervalNanos;
mFrameTimeNanos = now - lastFrameOffset;
+ // mFrameVsyncId is not adjusted here as we still want to send
+ // the vsync id that started this frame to the Surface Composer
}
return mFrameTimeNanos;
}
diff --git a/libs/hwui/renderthread/TimeLord.h b/libs/hwui/renderthread/TimeLord.h
index 68a0f7f971b9..fa05c030fa0f 100644
--- a/libs/hwui/renderthread/TimeLord.h
+++ b/libs/hwui/renderthread/TimeLord.h
@@ -32,9 +32,12 @@ public:
nsecs_t frameIntervalNanos() const { return mFrameIntervalNanos; }
// returns true if the vsync is newer, false if it was rejected for staleness
- bool vsyncReceived(nsecs_t vsync);
+ bool vsyncReceived(nsecs_t vsync, nsecs_t indendedVsync, int64_t vsyncId,
+ int64_t frameDeadline);
nsecs_t latestVsync() { return mFrameTimeNanos; }
nsecs_t computeFrameTimeNanos();
+ int64_t lastVsyncId() const { return mFrameVsyncId; }
+ int64_t lastFrameDeadline() const { return mFrameDeadline; }
private:
friend class RenderThread;
@@ -44,6 +47,9 @@ private:
nsecs_t mFrameIntervalNanos;
nsecs_t mFrameTimeNanos;
+ nsecs_t mFrameIntendedTimeNanos;
+ int64_t mFrameVsyncId;
+ int64_t mFrameDeadline;
};
} /* namespace renderthread */
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index ba70afc8b8d2..1333b92037c3 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -20,7 +20,7 @@
#include <EGL/eglext.h>
#include <GrBackendSemaphore.h>
#include <GrBackendSurface.h>
-#include <GrContext.h>
+#include <GrDirectContext.h>
#include <GrTypes.h>
#include <android/sync.h>
#include <ui/FatVector.h>
@@ -57,12 +57,22 @@ static void free_features_extensions_structs(const VkPhysicalDeviceFeatures2& fe
#define GET_INST_PROC(F) m##F = (PFN_vk##F)vkGetInstanceProcAddr(mInstance, "vk" #F)
#define GET_DEV_PROC(F) m##F = (PFN_vk##F)vkGetDeviceProcAddr(mDevice, "vk" #F)
-void VulkanManager::destroy() {
- if (VK_NULL_HANDLE != mCommandPool) {
- mDestroyCommandPool(mDevice, mCommandPool, nullptr);
- mCommandPool = VK_NULL_HANDLE;
+sp<VulkanManager> VulkanManager::getInstance() {
+ // cache a weakptr to the context to enable a second thread to share the same vulkan state
+ static wp<VulkanManager> sWeakInstance = nullptr;
+ static std::mutex sLock;
+
+ std::lock_guard _lock{sLock};
+ sp<VulkanManager> vulkanManager = sWeakInstance.promote();
+ if (!vulkanManager.get()) {
+ vulkanManager = new VulkanManager();
+ sWeakInstance = vulkanManager;
}
+ return vulkanManager;
+}
+
+VulkanManager::~VulkanManager() {
if (mDevice != VK_NULL_HANDLE) {
mDeviceWaitIdle(mDevice);
mDestroyDevice(mDevice, nullptr);
@@ -73,7 +83,7 @@ void VulkanManager::destroy() {
}
mGraphicsQueue = VK_NULL_HANDLE;
- mPresentQueue = VK_NULL_HANDLE;
+ mAHBUploadQueue = VK_NULL_HANDLE;
mDevice = VK_NULL_HANDLE;
mPhysicalDevice = VK_NULL_HANDLE;
mInstance = VK_NULL_HANDLE;
@@ -175,15 +185,12 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
for (uint32_t i = 0; i < queueCount; i++) {
if (queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
mGraphicsQueueIndex = i;
+ LOG_ALWAYS_FATAL_IF(queueProps[i].queueCount < 2);
break;
}
}
LOG_ALWAYS_FATAL_IF(mGraphicsQueueIndex == queueCount);
- // All physical devices and queue families on Android must be capable of
- // presentation with any native window. So just use the first one.
- mPresentQueueIndex = 0;
-
{
uint32_t extensionCount = 0;
err = mEnumerateDeviceExtensionProperties(mPhysicalDevice, nullptr, &extensionCount,
@@ -277,31 +284,21 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
queueNextPtr = &queuePriorityCreateInfo;
}
- const VkDeviceQueueCreateInfo queueInfo[2] = {
- {
- VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType
- queueNextPtr, // pNext
- 0, // VkDeviceQueueCreateFlags
- mGraphicsQueueIndex, // queueFamilyIndex
- 1, // queueCount
- queuePriorities, // pQueuePriorities
- },
- {
- VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType
- queueNextPtr, // pNext
- 0, // VkDeviceQueueCreateFlags
- mPresentQueueIndex, // queueFamilyIndex
- 1, // queueCount
- queuePriorities, // pQueuePriorities
- }};
- uint32_t queueInfoCount = (mPresentQueueIndex != mGraphicsQueueIndex) ? 2 : 1;
+ const VkDeviceQueueCreateInfo queueInfo = {
+ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType
+ queueNextPtr, // pNext
+ 0, // VkDeviceQueueCreateFlags
+ mGraphicsQueueIndex, // queueFamilyIndex
+ 2, // queueCount
+ queuePriorities, // pQueuePriorities
+ };
const VkDeviceCreateInfo deviceInfo = {
VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, // sType
&features, // pNext
0, // VkDeviceCreateFlags
- queueInfoCount, // queueCreateInfoCount
- queueInfo, // pQueueCreateInfos
+ 1, // queueCreateInfoCount
+ &queueInfo, // pQueueCreateInfos
0, // layerCount
nullptr, // ppEnabledLayerNames
(uint32_t)mDeviceExtensions.size(), // extensionCount
@@ -347,29 +344,15 @@ void VulkanManager::initialize() {
this->setupDevice(mExtensions, mPhysicalDeviceFeatures2);
mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 0, &mGraphicsQueue);
-
- // create the command pool for the command buffers
- if (VK_NULL_HANDLE == mCommandPool) {
- VkCommandPoolCreateInfo commandPoolInfo;
- memset(&commandPoolInfo, 0, sizeof(VkCommandPoolCreateInfo));
- commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
- // this needs to be on the render queue
- commandPoolInfo.queueFamilyIndex = mGraphicsQueueIndex;
- commandPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
- SkDEBUGCODE(VkResult res =)
- mCreateCommandPool(mDevice, &commandPoolInfo, nullptr, &mCommandPool);
- SkASSERT(VK_SUCCESS == res);
- }
- LOG_ALWAYS_FATAL_IF(mCommandPool == VK_NULL_HANDLE);
-
- mGetDeviceQueue(mDevice, mPresentQueueIndex, 0, &mPresentQueue);
+ mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 1, &mAHBUploadQueue);
if (Properties::enablePartialUpdates && Properties::useBufferAge) {
mSwapBehavior = SwapBehavior::BufferAge;
}
}
-sk_sp<GrContext> VulkanManager::createContext(const GrContextOptions& options) {
+sk_sp<GrDirectContext> VulkanManager::createContext(const GrContextOptions& options,
+ ContextType contextType) {
auto getProc = [](const char* proc_name, VkInstance instance, VkDevice device) {
if (device != VK_NULL_HANDLE) {
return vkGetDeviceProcAddr(device, proc_name);
@@ -381,14 +364,15 @@ sk_sp<GrContext> VulkanManager::createContext(const GrContextOptions& options) {
backendContext.fInstance = mInstance;
backendContext.fPhysicalDevice = mPhysicalDevice;
backendContext.fDevice = mDevice;
- backendContext.fQueue = mGraphicsQueue;
+ backendContext.fQueue = (contextType == ContextType::kRenderThread) ? mGraphicsQueue
+ : mAHBUploadQueue;
backendContext.fGraphicsQueueIndex = mGraphicsQueueIndex;
backendContext.fMaxAPIVersion = mAPIVersion;
backendContext.fVkExtensions = &mExtensions;
backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2;
backendContext.fGetProc = std::move(getProc);
- return GrContext::MakeVulkan(backendContext, options);
+ return GrDirectContext::MakeVulkan(backendContext, options);
}
VkFunctorInitParams VulkanManager::getVkFunctorInitParams() const {
@@ -459,7 +443,7 @@ Frame VulkanManager::dequeueNextBuffer(VulkanSurface* surface) {
// The following flush blocks the GPU immediately instead of waiting for other
// drawing ops. It seems dequeue_fence is not respected otherwise.
// TODO: remove the flush after finding why backendSemaphore is not working.
- bufferInfo->skSurface->flush();
+ bufferInfo->skSurface->flushAndSubmit();
}
}
}
@@ -525,9 +509,16 @@ void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect)
int fenceFd = -1;
DestroySemaphoreInfo* destroyInfo =
new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore);
+ GrFlushInfo flushInfo;
+ flushInfo.fNumSemaphores = 1;
+ flushInfo.fSignalSemaphores = &backendSemaphore;
+ flushInfo.fFinishedProc = destroy_semaphore;
+ flushInfo.fFinishedContext = destroyInfo;
GrSemaphoresSubmitted submitted = bufferInfo->skSurface->flush(
- SkSurface::BackendSurfaceAccess::kPresent, kNone_GrFlushFlags, 1, &backendSemaphore,
- destroy_semaphore, destroyInfo);
+ SkSurface::BackendSurfaceAccess::kPresent, flushInfo);
+ GrDirectContext* context = GrAsDirectContext(bufferInfo->skSurface->recordingContext());
+ ALOGE_IF(!context, "Surface is not backed by gpu");
+ context->submit();
if (submitted == GrSemaphoresSubmitted::kYes) {
VkSemaphoreGetFdInfoKHR getFdInfo;
getFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR;
@@ -548,17 +539,19 @@ void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect)
void VulkanManager::destroySurface(VulkanSurface* surface) {
// Make sure all submit commands have finished before starting to destroy objects.
- if (VK_NULL_HANDLE != mPresentQueue) {
- mQueueWaitIdle(mPresentQueue);
+ if (VK_NULL_HANDLE != mGraphicsQueue) {
+ mQueueWaitIdle(mGraphicsQueue);
}
mDeviceWaitIdle(mDevice);
delete surface;
}
-VulkanSurface* VulkanManager::createSurface(ANativeWindow* window, ColorMode colorMode,
+VulkanSurface* VulkanManager::createSurface(ANativeWindow* window,
+ ColorMode colorMode,
sk_sp<SkColorSpace> surfaceColorSpace,
- SkColorType surfaceColorType, GrContext* grContext,
+ SkColorType surfaceColorType,
+ GrDirectContext* grContext,
uint32_t extraBuffers) {
LOG_ALWAYS_FATAL_IF(!hasVkContext(), "Not initialized");
if (!window) {
@@ -569,7 +562,7 @@ VulkanSurface* VulkanManager::createSurface(ANativeWindow* window, ColorMode col
*this, extraBuffers);
}
-status_t VulkanManager::fenceWait(int fence, GrContext* grContext) {
+status_t VulkanManager::fenceWait(int fence, GrDirectContext* grContext) {
if (!hasVkContext()) {
ALOGE("VulkanManager::fenceWait: VkDevice not initialized");
return INVALID_OPERATION;
@@ -612,12 +605,12 @@ status_t VulkanManager::fenceWait(int fence, GrContext* grContext) {
// Skia takes ownership of the semaphore and will delete it once the wait has finished.
grContext->wait(1, &beSemaphore);
- grContext->flush();
+ grContext->flushAndSubmit();
return OK;
}
-status_t VulkanManager::createReleaseFence(int* nativeFence, GrContext* grContext) {
+status_t VulkanManager::createReleaseFence(int* nativeFence, GrDirectContext* grContext) {
*nativeFence = -1;
if (!hasVkContext()) {
ALOGE("VulkanManager::createReleaseFence: VkDevice not initialized");
@@ -648,8 +641,13 @@ status_t VulkanManager::createReleaseFence(int* nativeFence, GrContext* grContex
// Even if Skia fails to submit the semaphore, it will still call the destroy_semaphore callback
// which will remove its ref to the semaphore. The VulkanManager must still release its ref,
// when it is done with the semaphore.
- GrSemaphoresSubmitted submitted = grContext->flush(kNone_GrFlushFlags, 1, &backendSemaphore,
- destroy_semaphore, destroyInfo);
+ GrFlushInfo flushInfo;
+ flushInfo.fNumSemaphores = 1;
+ flushInfo.fSignalSemaphores = &backendSemaphore;
+ flushInfo.fFinishedProc = destroy_semaphore;
+ flushInfo.fFinishedContext = destroyInfo;
+ GrSemaphoresSubmitted submitted = grContext->flush(flushInfo);
+ grContext->submit();
if (submitted == GrSemaphoresSubmitted::kNo) {
ALOGE("VulkanManager::createReleaseFence: Failed to submit semaphore");
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index 8b19f13fdfb9..7a77466303cd 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -43,10 +43,9 @@ class RenderThread;
// This class contains the shared global Vulkan objects, such as VkInstance, VkDevice and VkQueue,
// which are re-used by CanvasContext. This class is created once and should be used by all vulkan
// windowing contexts. The VulkanManager must be initialized before use.
-class VulkanManager {
+class VulkanManager final : public RefBase {
public:
- explicit VulkanManager() {}
- ~VulkanManager() { destroy(); }
+ static sp<VulkanManager> getInstance();
// Sets up the vulkan context that is shared amonst all clients of the VulkanManager. This must
// be call once before use of the VulkanManager. Multiple calls after the first will simiply
@@ -57,36 +56,47 @@ public:
bool hasVkContext() { return mDevice != VK_NULL_HANDLE; }
// Create and destroy functions for wrapping an ANativeWindow in a VulkanSurface
- VulkanSurface* createSurface(ANativeWindow* window, ColorMode colorMode,
+ VulkanSurface* createSurface(ANativeWindow* window,
+ ColorMode colorMode,
sk_sp<SkColorSpace> surfaceColorSpace,
- SkColorType surfaceColorType, GrContext* grContext,
+ SkColorType surfaceColorType,
+ GrDirectContext* grContext,
uint32_t extraBuffers);
void destroySurface(VulkanSurface* surface);
Frame dequeueNextBuffer(VulkanSurface* surface);
void swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect);
- // Cleans up all the global state in the VulkanManger.
- void destroy();
-
// Inserts a wait on fence command into the Vulkan command buffer.
- status_t fenceWait(int fence, GrContext* grContext);
+ status_t fenceWait(int fence, GrDirectContext* grContext);
// Creates a fence that is signaled when all the pending Vulkan commands are finished on the
// GPU.
- status_t createReleaseFence(int* nativeFence, GrContext* grContext);
+ status_t createReleaseFence(int* nativeFence, GrDirectContext* grContext);
// Returned pointers are owned by VulkanManager.
// An instance of VkFunctorInitParams returned from getVkFunctorInitParams refers to
// the internal state of VulkanManager: VulkanManager must be alive to use the returned value.
VkFunctorInitParams getVkFunctorInitParams() const;
- sk_sp<GrContext> createContext(const GrContextOptions& options);
+
+ enum class ContextType {
+ kRenderThread,
+ kUploadThread
+ };
+
+ // returns a Skia graphic context used to draw content on the specified thread
+ sk_sp<GrDirectContext> createContext(const GrContextOptions& options,
+ ContextType contextType = ContextType::kRenderThread);
uint32_t getDriverVersion() const { return mDriverVersion; }
private:
friend class VulkanSurface;
+
+ explicit VulkanManager() {}
+ ~VulkanManager();
+
// Sets up the VkInstance and VkDevice objects. Also fills out the passed in
// VkPhysicalDeviceFeatures struct.
void setupDevice(GrVkExtensions&, VkPhysicalDeviceFeatures2&);
@@ -152,9 +162,7 @@ private:
uint32_t mGraphicsQueueIndex;
VkQueue mGraphicsQueue = VK_NULL_HANDLE;
- uint32_t mPresentQueueIndex;
- VkQueue mPresentQueue = VK_NULL_HANDLE;
- VkCommandPool mCommandPool = VK_NULL_HANDLE;
+ VkQueue mAHBUploadQueue = VK_NULL_HANDLE;
// Variables saved to populate VkFunctorInitParams.
static const uint32_t mAPIVersion = VK_MAKE_VERSION(1, 1, 0);
diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp
index a7ea21d8c4de..acf4931d6144 100644
--- a/libs/hwui/renderthread/VulkanSurface.cpp
+++ b/libs/hwui/renderthread/VulkanSurface.cpp
@@ -16,6 +16,7 @@
#include "VulkanSurface.h"
+#include <GrDirectContext.h>
#include <SkSurface.h>
#include <algorithm>
@@ -117,7 +118,7 @@ static bool ConnectAndSetWindowDefaults(ANativeWindow* window) {
VulkanSurface* VulkanSurface::Create(ANativeWindow* window, ColorMode colorMode,
SkColorType colorType, sk_sp<SkColorSpace> colorSpace,
- GrContext* grContext, const VulkanManager& vkManager,
+ GrDirectContext* grContext, const VulkanManager& vkManager,
uint32_t extraBuffers) {
// Connect and set native window to default configurations.
if (!ConnectAndSetWindowDefaults(window)) {
@@ -200,16 +201,16 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode
"Could not get gamut matrix from color space");
if (memcmp(&surfaceGamut, &SkNamedGamut::kSRGB, sizeof(surfaceGamut)) == 0) {
outWindowInfo->dataspace = HAL_DATASPACE_V0_SCRGB;
- } else if (memcmp(&surfaceGamut, &SkNamedGamut::kDCIP3, sizeof(surfaceGamut)) == 0) {
+ } else if (memcmp(&surfaceGamut, &SkNamedGamut::kDisplayP3, sizeof(surfaceGamut)) == 0) {
outWindowInfo->dataspace = HAL_DATASPACE_DISPLAY_P3;
} else {
LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space.");
}
}
- outWindowInfo->pixelFormat = ColorTypeToPixelFormat(colorType);
+ outWindowInfo->bufferFormat = ColorTypeToBufferFormat(colorType);
VkFormat vkPixelFormat = VK_FORMAT_R8G8B8A8_UNORM;
- if (outWindowInfo->pixelFormat == PIXEL_FORMAT_RGBA_FP16) {
+ if (outWindowInfo->bufferFormat == AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT) {
vkPixelFormat = VK_FORMAT_R16G16B16A16_SFLOAT;
}
@@ -263,10 +264,10 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode
bool VulkanSurface::UpdateWindow(ANativeWindow* window, const WindowInfo& windowInfo) {
ATRACE_CALL();
- int err = native_window_set_buffers_format(window, windowInfo.pixelFormat);
+ int err = native_window_set_buffers_format(window, windowInfo.bufferFormat);
if (err != 0) {
ALOGE("VulkanSurface::UpdateWindow() native_window_set_buffers_format(%d) failed: %s (%d)",
- windowInfo.pixelFormat, strerror(-err), err);
+ windowInfo.bufferFormat, strerror(-err), err);
return false;
}
@@ -310,7 +311,7 @@ bool VulkanSurface::UpdateWindow(ANativeWindow* window, const WindowInfo& window
}
VulkanSurface::VulkanSurface(ANativeWindow* window, const WindowInfo& windowInfo,
- GrContext* grContext)
+ GrDirectContext* grContext)
: mNativeWindow(window), mWindowInfo(windowInfo), mGrContext(grContext) {}
VulkanSurface::~VulkanSurface() {
diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h
index bd2362612a13..409921bdfdd7 100644
--- a/libs/hwui/renderthread/VulkanSurface.h
+++ b/libs/hwui/renderthread/VulkanSurface.h
@@ -17,8 +17,6 @@
#include <system/graphics.h>
#include <system/window.h>
-#include <ui/BufferQueueDefs.h>
-#include <ui/PixelFormat.h>
#include <vulkan/vulkan.h>
#include <SkRefCnt.h>
@@ -37,7 +35,7 @@ class VulkanManager;
class VulkanSurface {
public:
static VulkanSurface* Create(ANativeWindow* window, ColorMode colorMode, SkColorType colorType,
- sk_sp<SkColorSpace> colorSpace, GrContext* grContext,
+ sk_sp<SkColorSpace> colorSpace, GrDirectContext* grContext,
const VulkanManager& vkManager, uint32_t extraBuffers);
~VulkanSurface();
@@ -91,7 +89,7 @@ private:
struct WindowInfo {
SkISize size;
- PixelFormat pixelFormat;
+ uint32_t bufferFormat;
android_dataspace dataspace;
int transform;
size_t bufferCount;
@@ -103,7 +101,7 @@ private:
SkMatrix preTransform;
};
- VulkanSurface(ANativeWindow* window, const WindowInfo& windowInfo, GrContext* grContext);
+ VulkanSurface(ANativeWindow* window, const WindowInfo& windowInfo, GrDirectContext* grContext);
static bool InitializeWindowInfoStruct(ANativeWindow* window, ColorMode colorMode,
SkColorType colorType, sk_sp<SkColorSpace> colorSpace,
const VulkanManager& vkManager, uint32_t extraBuffers,
@@ -111,12 +109,17 @@ private:
static bool UpdateWindow(ANativeWindow* window, const WindowInfo& windowInfo);
void releaseBuffers();
+ // TODO: This number comes from ui/BufferQueueDefs. We're not pulling the
+ // header in so that we don't need to depend on libui, but we should share
+ // this constant somewhere. But right now it's okay to keep here because we
+ // can't safely change the slot count anyways.
+ static constexpr size_t kNumBufferSlots = 64;
// TODO: Just use a vector?
- NativeBufferInfo mNativeBuffers[android::BufferQueueDefs::NUM_BUFFER_SLOTS];
+ NativeBufferInfo mNativeBuffers[kNumBufferSlots];
sp<ANativeWindow> mNativeWindow;
WindowInfo mWindowInfo;
- GrContext* mGrContext;
+ GrDirectContext* mGrContext;
uint32_t mPresentCount = 0;
NativeBufferInfo* mCurrentBufferInfo = nullptr;
diff --git a/libs/hwui/service/GraphicsStatsService.h b/libs/hwui/service/GraphicsStatsService.h
index 59e21d039c9d..4063f749f808 100644
--- a/libs/hwui/service/GraphicsStatsService.h
+++ b/libs/hwui/service/GraphicsStatsService.h
@@ -44,18 +44,16 @@ public:
ProtobufStatsd,
};
- ANDROID_API static void saveBuffer(const std::string& path, const std::string& package,
- int64_t versionCode, int64_t startTime, int64_t endTime,
- const ProfileData* data);
-
- ANDROID_API static Dump* createDump(int outFd, DumpType type);
- ANDROID_API static void addToDump(Dump* dump, const std::string& path,
- const std::string& package, int64_t versionCode,
- int64_t startTime, int64_t endTime, const ProfileData* data);
- ANDROID_API static void addToDump(Dump* dump, const std::string& path);
- ANDROID_API static void finishDump(Dump* dump);
- ANDROID_API static void finishDumpInMemory(Dump* dump, AStatsEventList* data,
- bool lastFullDay);
+ static void saveBuffer(const std::string& path, const std::string& package, int64_t versionCode,
+ int64_t startTime, int64_t endTime, const ProfileData* data);
+
+ static Dump* createDump(int outFd, DumpType type);
+ static void addToDump(Dump* dump, const std::string& path, const std::string& package,
+ int64_t versionCode, int64_t startTime, int64_t endTime,
+ const ProfileData* data);
+ static void addToDump(Dump* dump, const std::string& path);
+ static void finishDump(Dump* dump);
+ static void finishDumpInMemory(Dump* dump, AStatsEventList* data, bool lastFullDay);
// Visible for testing
static bool parseFromFile(const std::string& path, protos::GraphicsStatsProto* output);
diff --git a/libs/hwui/shader/BlurShader.cpp b/libs/hwui/shader/BlurShader.cpp
new file mode 100644
index 000000000000..2abd8714204b
--- /dev/null
+++ b/libs/hwui/shader/BlurShader.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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 "BlurShader.h"
+#include "SkImageFilters.h"
+#include "SkRefCnt.h"
+#include "utils/Blur.h"
+
+namespace android::uirenderer {
+BlurShader::BlurShader(float radiusX, float radiusY, Shader* inputShader, SkTileMode edgeTreatment,
+ const SkMatrix* matrix)
+ : Shader(matrix)
+ , skImageFilter(
+ SkImageFilters::Blur(
+ Blur::convertRadiusToSigma(radiusX),
+ Blur::convertRadiusToSigma(radiusY),
+ edgeTreatment,
+ inputShader ? inputShader->asSkImageFilter() : nullptr,
+ nullptr)
+ ) { }
+
+sk_sp<SkImageFilter> BlurShader::makeSkImageFilter() {
+ return skImageFilter;
+}
+
+BlurShader::~BlurShader() {}
+
+} // namespace android::uirenderer \ No newline at end of file
diff --git a/libs/hwui/shader/BlurShader.h b/libs/hwui/shader/BlurShader.h
new file mode 100644
index 000000000000..60a15898893e
--- /dev/null
+++ b/libs/hwui/shader/BlurShader.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include "Shader.h"
+
+namespace android::uirenderer {
+
+/**
+ * Shader implementation that blurs another Shader instance or the source bitmap
+ */
+class BlurShader : public Shader {
+public:
+ /**
+ * Creates a BlurShader instance with the provided radius values to blur along the x and y
+ * axis accordingly.
+ *
+ * This will blur the contents of the provided input shader if it is non-null, otherwise
+ * the source bitmap will be blurred instead.
+ *
+ * The edge treatment parameter determines how content near the edges of the source is to
+ * participate in the blur
+ */
+ BlurShader(float radiusX, float radiusY, Shader* inputShader, SkTileMode edgeTreatment,
+ const SkMatrix* matrix);
+ ~BlurShader() override;
+protected:
+ sk_sp<SkImageFilter> makeSkImageFilter() override;
+private:
+ sk_sp<SkImageFilter> skImageFilter;
+};
+
+} // namespace android::uirenderer \ No newline at end of file
diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h
index 91a808df3657..36c5a8c1b3de 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -287,18 +287,6 @@ public:
static std::unique_ptr<uint16_t[]> asciiToUtf16(const char* str);
- class MockFunctor : public Functor {
- public:
- virtual status_t operator()(int what, void* data) {
- mLastMode = what;
- return DrawGlInfo::kStatusDone;
- }
- int getLastMode() const { return mLastMode; }
-
- private:
- int mLastMode = -1;
- };
-
static SkColor getColor(const sk_sp<SkSurface>& surface, int x, int y);
static SkRect getClipBounds(const SkCanvas* canvas);
@@ -311,30 +299,32 @@ public:
int glesDraw = 0;
};
- static void expectOnRenderThread() { EXPECT_EQ(gettid(), TestUtils::getRenderThreadTid()); }
+ static void expectOnRenderThread(const std::string_view& function = "unknown") {
+ EXPECT_EQ(gettid(), TestUtils::getRenderThreadTid()) << "Called on wrong thread: " << function;
+ }
static WebViewFunctorCallbacks createMockFunctor(RenderMode mode) {
auto callbacks = WebViewFunctorCallbacks{
.onSync =
[](int functor, void* client_data, const WebViewSyncData& data) {
- expectOnRenderThread();
+ expectOnRenderThread("onSync");
sMockFunctorCounts[functor].sync++;
},
.onContextDestroyed =
[](int functor, void* client_data) {
- expectOnRenderThread();
+ expectOnRenderThread("onContextDestroyed");
sMockFunctorCounts[functor].contextDestroyed++;
},
.onDestroyed =
[](int functor, void* client_data) {
- expectOnRenderThread();
+ expectOnRenderThread("onDestroyed");
sMockFunctorCounts[functor].destroyed++;
},
};
switch (mode) {
case RenderMode::OpenGL_ES:
callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params) {
- expectOnRenderThread();
+ expectOnRenderThread("draw");
sMockFunctorCounts[functor].glesDraw++;
};
break;
diff --git a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp
index f4fce277454d..edadf78db051 100644
--- a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp
@@ -56,9 +56,9 @@ public:
(float)magnifier->height(), 0, 0, (float)props.getWidth(),
(float)props.getHeight(), nullptr);
});
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
canvas.drawRenderNode(zoomImageView.get());
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
}
void doFrame(int frameNr) override {
diff --git a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp
index 3480a0f18407..1c2507867f6e 100644
--- a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp
@@ -36,7 +36,7 @@ public:
int cardsize = std::min(width, height) - dp(64);
renderer.drawColor(Color::White, SkBlendMode::kSrcOver);
- renderer.insertReorderBarrier(true);
+ renderer.enableZ(true);
int x = dp(32);
for (int i = 0; i < 4; i++) {
@@ -52,7 +52,7 @@ public:
mCards.push_back(card);
}
- renderer.insertReorderBarrier(false);
+ renderer.enableZ(false);
}
void doFrame(int frameNr) override {
diff --git a/libs/hwui/tests/common/scenes/RectGridAnimation.cpp b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp
index 80b5cc191089..f37bcbc3ee1b 100644
--- a/libs/hwui/tests/common/scenes/RectGridAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp
@@ -29,7 +29,7 @@ public:
sp<RenderNode> card;
void createContent(int width, int height, Canvas& canvas) override {
canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver);
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
card = TestUtils::createNode(50, 50, 250, 250, [](RenderProperties& props, Canvas& canvas) {
canvas.drawColor(0xFFFF00FF, SkBlendMode::kSrcOver);
@@ -47,7 +47,7 @@ public:
});
canvas.drawRenderNode(card.get());
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
}
void doFrame(int frameNr) override {
int curFrame = frameNr % 150;
diff --git a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp
index 314e922e9f38..163745b04ed2 100644
--- a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp
@@ -27,7 +27,7 @@ public:
std::vector<sp<RenderNode> > cards;
void createContent(int width, int height, Canvas& canvas) override {
canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver);
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
int ci = 0;
for (int x = 0; x < width; x += mSpacing) {
@@ -45,7 +45,7 @@ public:
}
}
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
}
void doFrame(int frameNr) override {
int curFrame = frameNr % 50;
diff --git a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp
index bdc991ba1890..c13e80e8c204 100644
--- a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp
+++ b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp
@@ -29,7 +29,7 @@ public:
std::vector<sp<RenderNode> > cards;
void createContent(int width, int height, Canvas& canvas) override {
canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver);
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
for (int x = dp(8); x < (width - dp(58)); x += dp(58)) {
for (int y = dp(8); y < (height - dp(58)); y += dp(58)) {
@@ -39,7 +39,7 @@ public:
}
}
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
}
void doFrame(int frameNr) override {
int curFrame = frameNr % 150;
diff --git a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp
index a12fd4d69280..772b98e32220 100644
--- a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp
@@ -29,7 +29,7 @@ public:
std::vector<sp<RenderNode> > cards;
void createContent(int width, int height, Canvas& canvas) override {
canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver);
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
for (int x = dp(16); x < (width - dp(116)); x += dp(116)) {
for (int y = dp(16); y < (height - dp(116)); y += dp(116)) {
@@ -39,7 +39,7 @@ public:
}
}
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
}
void doFrame(int frameNr) override {
int curFrame = frameNr % 150;
diff --git a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp
index 9f599100200e..0019da5fd80b 100644
--- a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp
@@ -29,7 +29,7 @@ public:
std::vector<sp<RenderNode> > cards;
void createContent(int width, int height, Canvas& canvas) override {
canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver);
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
int outset = 50;
for (int i = 0; i < 10; i++) {
@@ -39,7 +39,7 @@ public:
cards.push_back(card);
}
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
}
void doFrame(int frameNr) override {
int curFrame = frameNr % 10;
diff --git a/libs/hwui/tests/common/scenes/TvApp.cpp b/libs/hwui/tests/common/scenes/TvApp.cpp
index bac887053d2f..1b0a07a98b3f 100644
--- a/libs/hwui/tests/common/scenes/TvApp.cpp
+++ b/libs/hwui/tests/common/scenes/TvApp.cpp
@@ -67,7 +67,7 @@ public:
mBg = createBitmapNode(canvas, 0xFF9C27B0, 0, 0, width, height);
canvas.drawRenderNode(mBg.get());
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
mSingleBitmap = mAllocator(dp(160), dp(120), kRGBA_8888_SkColorType,
[](SkBitmap& skBitmap) { skBitmap.eraseColor(0xFF0000FF); });
@@ -80,7 +80,7 @@ public:
mCards.push_back(card);
}
}
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
}
void doFrame(int frameNr) override {
diff --git a/libs/hwui/tests/macrobench/TestSceneRunner.cpp b/libs/hwui/tests/macrobench/TestSceneRunner.cpp
index 801cb7d9e8c5..eda5d2266dcf 100644
--- a/libs/hwui/tests/macrobench/TestSceneRunner.cpp
+++ b/libs/hwui/tests/macrobench/TestSceneRunner.cpp
@@ -145,7 +145,8 @@ void run(const TestScene::Info& info, const TestScene::Options& opts,
for (int i = 0; i < warmupFrameCount; i++) {
testContext.waitForVsync();
nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC);
- UiFrameInfoBuilder(proxy->frameInfo()).setVsync(vsync, vsync);
+ UiFrameInfoBuilder(proxy->frameInfo())
+ .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID, std::numeric_limits<int64_t>::max());
proxy->syncAndDrawFrame();
}
@@ -165,7 +166,8 @@ void run(const TestScene::Info& info, const TestScene::Options& opts,
nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC);
{
ATRACE_NAME("UI-Draw Frame");
- UiFrameInfoBuilder(proxy->frameInfo()).setVsync(vsync, vsync);
+ UiFrameInfoBuilder(proxy->frameInfo())
+ .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID, std::numeric_limits<int64_t>::max());
scene->doFrame(i);
proxy->syncAndDrawFrame();
}
diff --git a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp
index 4ce6c32470ea..d393c693c774 100644
--- a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp
+++ b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp
@@ -133,14 +133,14 @@ void BM_DisplayListCanvas_basicViewGroupDraw(benchmark::State& benchState) {
int clipRestoreCount = canvas->save(SaveFlags::MatrixClip);
canvas->clipRect(1, 1, 199, 199, SkClipOp::kIntersect);
- canvas->insertReorderBarrier(true);
+ canvas->enableZ(true);
// Draw child loop
for (int i = 0; i < benchState.range(0); i++) {
canvas->drawRenderNode(child.get());
}
- canvas->insertReorderBarrier(false);
+ canvas->enableZ(false);
canvas->restoreToCount(clipRestoreCount);
delete canvas->finishRecording();
diff --git a/libs/hwui/tests/scripts/prep_generic.sh b/libs/hwui/tests/scripts/prep_generic.sh
index 223bf373c65a..89826ff69463 100755
--- a/libs/hwui/tests/scripts/prep_generic.sh
+++ b/libs/hwui/tests/scripts/prep_generic.sh
@@ -28,11 +28,17 @@
# performance between different device models.
# Fun notes for maintaining this file:
-# `expr` can deal with ints > INT32_MAX, but if compares cannot. This is why we use MHz.
-# `expr` can sometimes evaluate right-to-left. This is why we use parens.
+# $((arithmetic expressions)) can deal with ints > INT32_MAX, but if compares cannot. This is
+# why we use MHz.
+# $((arithmetic expressions)) can sometimes evaluate right-to-left. This is why we use parens.
# Everything below the initial host-check isn't bash - Android uses mksh
# mksh allows `\n` in an echo, bash doesn't
# can't use `awk`
+# can't use `sed`
+# can't use `cut` on < L
+# can't use `expr` on < L
+
+ARG_CORES=${1:-big}
CPU_TARGET_FREQ_PERCENT=50
GPU_TARGET_FREQ_PERCENT=50
@@ -43,7 +49,7 @@ if [ "`command -v getprop`" == "" ]; then
echo "Pushing $0 and running it on device..."
dest=/data/local/tmp/`basename $0`
adb push $0 ${dest}
- adb shell ${dest}
+ adb shell ${dest} $@
adb shell rm ${dest}
exit
else
@@ -56,7 +62,7 @@ if [ "`command -v getprop`" == "" ]; then
fi
# require root
-if [ "`id -u`" -ne "0" ]; then
+if [[ `id` != "uid=0"* ]]; then
echo "Not running as root, cannot lock clocks, aborting"
exit -1
fi
@@ -64,74 +70,175 @@ fi
DEVICE=`getprop ro.product.device`
MODEL=`getprop ro.product.model`
-# Find CPU max frequency, and lock big cores to an available frequency
-# that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores.
+if [ "$ARG_CORES" == "big" ]; then
+ CPU_IDEAL_START_FREQ_KHZ=0
+elif [ "$ARG_CORES" == "little" ]; then
+ CPU_IDEAL_START_FREQ_KHZ=100000000 ## finding min of max freqs, so start at 100M KHz (100 GHz)
+else
+ echo "Invalid argument \$1 for ARG_CORES, should be 'big' or 'little', but was $ARG_CORES"
+ exit -1
+fi
+
+function_core_check() {
+ if [ "$ARG_CORES" == "big" ]; then
+ [ $1 -gt $2 ]
+ elif [ "$ARG_CORES" == "little" ]; then
+ [ $1 -lt $2 ]
+ else
+ echo "Invalid argument \$1 for ARG_CORES, should be 'big' or 'little', but was $ARG_CORES"
+ exit -1
+ fi
+}
+
+function_setup_go() {
+ if [ -f /d/fpsgo/common/force_onoff ]; then
+ # Disable fpsgo
+ echo 0 > /d/fpsgo/common/force_onoff
+ fpsgoState=`cat /d/fpsgo/common/force_onoff`
+ if [ "$fpsgoState" != "0" ] && [ "$fpsgoState" != "force off" ]; then
+ echo "Failed to disable fpsgo"
+ exit -1
+ fi
+ fi
+}
+
+# Find the min or max (little vs big) of CPU max frequency, and lock cores of the selected type to
+# an available frequency that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores.
function_lock_cpu() {
CPU_BASE=/sys/devices/system/cpu
GOV=cpufreq/scaling_governor
+ # Options to make clock locking on go devices more sticky.
+ function_setup_go
+
# Find max CPU freq, and associated list of available freqs
- cpuMaxFreq=0
+ cpuIdealFreq=$CPU_IDEAL_START_FREQ_KHZ
cpuAvailFreqCmpr=0
cpuAvailFreq=0
enableIndices=''
disableIndices=''
cpu=0
- while [ -f ${CPU_BASE}/cpu${cpu}/online ]; do
- # enable core, so we can find its frequencies
- echo 1 > ${CPU_BASE}/cpu${cpu}/online
+ while [ -d ${CPU_BASE}/cpu${cpu}/cpufreq ]; do
+ # Try to enable core, so we can find its frequencies.
+ # Note: In cases where the online file is inaccessible, it represents a
+ # core which cannot be turned off, so we simply assume it is enabled if
+ # this command fails.
+ if [ -f "$CPU_BASE/cpu$cpu/online" ]; then
+ echo 1 > ${CPU_BASE}/cpu${cpu}/online || true
+ fi
+
+ # set userspace governor on all CPUs to ensure freq scaling is disabled
+ echo userspace > ${CPU_BASE}/cpu${cpu}/${GOV}
maxFreq=`cat ${CPU_BASE}/cpu$cpu/cpufreq/cpuinfo_max_freq`
availFreq=`cat ${CPU_BASE}/cpu$cpu/cpufreq/scaling_available_frequencies`
availFreqCmpr=${availFreq// /-}
- if [ ${maxFreq} -gt ${cpuMaxFreq} ]; then
- # new highest max freq, look for cpus with same max freq and same avail freq list
- cpuMaxFreq=${maxFreq}
+ if (function_core_check $maxFreq $cpuIdealFreq); then
+ # new min/max of max freq, look for cpus with same max freq and same avail freq list
+ cpuIdealFreq=${maxFreq}
cpuAvailFreq=${availFreq}
cpuAvailFreqCmpr=${availFreqCmpr}
- if [ -z ${disableIndices} ]; then
+ if [ -z "$disableIndices" ]; then
disableIndices="$enableIndices"
else
disableIndices="$disableIndices $enableIndices"
fi
enableIndices=${cpu}
- elif [ ${maxFreq} == ${cpuMaxFreq} ] && [ ${availFreqCmpr} == ${cpuAvailFreqCmpr} ]; then
+ elif [ ${maxFreq} == ${cpuIdealFreq} ] && [ ${availFreqCmpr} == ${cpuAvailFreqCmpr} ]; then
enableIndices="$enableIndices $cpu"
else
- disableIndices="$disableIndices $cpu"
+ if [ -z "$disableIndices" ]; then
+ disableIndices="$cpu"
+ else
+ disableIndices="$disableIndices $cpu"
+ fi
fi
+
cpu=$(($cpu + 1))
done
+ # check that some CPUs will be enabled
+ if [ -z "$enableIndices" ]; then
+ echo "Failed to find any $ARG_CORES cores to enable, aborting."
+ exit -1
+ fi
+
# Chose a frequency to lock to that's >= $CPU_TARGET_FREQ_PERCENT% of max
# (below, 100M = 1K for KHz->MHz * 100 for %)
- TARGET_FREQ_MHZ=`expr \( ${cpuMaxFreq} \* ${CPU_TARGET_FREQ_PERCENT} \) \/ 100000`
+ TARGET_FREQ_MHZ=$(( ($cpuIdealFreq * $CPU_TARGET_FREQ_PERCENT) / 100000 ))
chosenFreq=0
+ chosenFreqDiff=100000000
for freq in ${cpuAvailFreq}; do
- freqMhz=`expr ${freq} \/ 1000`
+ freqMhz=$(( ${freq} / 1000 ))
if [ ${freqMhz} -ge ${TARGET_FREQ_MHZ} ]; then
- chosenFreq=${freq}
- break
+ newChosenFreqDiff=$(( $freq - $TARGET_FREQ_MHZ ))
+ if [ $newChosenFreqDiff -lt $chosenFreqDiff ]; then
+ chosenFreq=${freq}
+ chosenFreqDiff=$(( $chosenFreq - $TARGET_FREQ_MHZ ))
+ fi
fi
done
+ # Lock wembley clocks using high-priority op code method.
+ # This block depends on the shell utility awk, which is only available on API 27+
+ if [ "$DEVICE" == "wembley" ]; then
+ # Get list of available frequencies to lock to by parsing the op-code list.
+ AVAIL_OP_FREQS=`cat /proc/cpufreq/MT_CPU_DVFS_LL/cpufreq_oppidx \
+ | awk '{print $2}' \
+ | tail -n +3 \
+ | while read line; do
+ echo "${line:1:${#line}-2}"
+ done`
+
+ # Compute the closest available frequency to the desired frequency, $chosenFreq.
+ # This assumes the op codes listen in /proc/cpufreq/MT_CPU_DVFS_LL/cpufreq_oppidx are listed
+ # in order and 0-indexed.
+ opCode=-1
+ opFreq=0
+ currOpCode=-1
+ for currOpFreq in $AVAIL_OP_FREQS; do
+ currOpCode=$((currOpCode + 1))
+
+ prevDiff=$((chosenFreq-opFreq))
+ prevDiff=`function_abs $prevDiff`
+ currDiff=$((chosenFreq-currOpFreq))
+ currDiff=`function_abs $currDiff`
+ if [ $currDiff -lt $prevDiff ]; then
+ opCode="$currOpCode"
+ opFreq="$currOpFreq"
+ fi
+ done
+
+ echo "$opCode" > /proc/ppm/policy/ut_fix_freq_idx
+ fi
+
# enable 'big' CPUs
for cpu in ${enableIndices}; do
freq=${CPU_BASE}/cpu$cpu/cpufreq
- echo 1 > ${CPU_BASE}/cpu${cpu}/online
- echo userspace > ${CPU_BASE}/cpu${cpu}/${GOV}
+ # Try to enable core, so we can find its frequencies.
+ # Note: In cases where the online file is inaccessible, it represents a
+ # core which cannot be turned off, so we simply assume it is enabled if
+ # this command fails.
+ if [ -f "$CPU_BASE/cpu$cpu/online" ]; then
+ echo 1 > ${CPU_BASE}/cpu${cpu}/online || true
+ fi
+
+ # scaling_max_freq must be set before scaling_min_freq
echo ${chosenFreq} > ${freq}/scaling_max_freq
echo ${chosenFreq} > ${freq}/scaling_min_freq
echo ${chosenFreq} > ${freq}/scaling_setspeed
+ # Give system a bit of time to propagate the change to scaling_setspeed.
+ sleep 0.1
+
# validate setting the freq worked
obsCur=`cat ${freq}/scaling_cur_freq`
obsMin=`cat ${freq}/scaling_min_freq`
obsMax=`cat ${freq}/scaling_max_freq`
- if [ obsCur -ne ${chosenFreq} ] || [ obsMin -ne ${chosenFreq} ] || [ obsMax -ne ${chosenFreq} ]; then
+ if [ "$obsCur" -ne "$chosenFreq" ] || [ "$obsMin" -ne "$chosenFreq" ] || [ "$obsMax" -ne "$chosenFreq" ]; then
echo "Failed to set CPU$cpu to $chosenFreq Hz! Aborting..."
echo "scaling_cur_freq = $obsCur"
echo "scaling_min_freq = $obsMin"
@@ -145,8 +252,20 @@ function_lock_cpu() {
echo 0 > ${CPU_BASE}/cpu${cpu}/online
done
- echo "\nLocked CPUs ${enableIndices// /,} to $chosenFreq / $maxFreq KHz"
+ echo "=================================="
+ echo "Locked CPUs ${enableIndices// /,} to $chosenFreq / $cpuIdealFreq KHz"
echo "Disabled CPUs ${disableIndices// /,}"
+ echo "=================================="
+}
+
+# Returns the absolute value of the first arg passed to this helper.
+function_abs() {
+ n=$1
+ if [ $n -lt 0 ]; then
+ echo "$((n * -1 ))"
+ else
+ echo "$n"
+ fi
}
# If we have a Qualcomm GPU, find its max frequency, and lock to
@@ -154,12 +273,12 @@ function_lock_cpu() {
function_lock_gpu_kgsl() {
if [ ! -d /sys/class/kgsl/kgsl-3d0/ ]; then
# not kgsl, abort
- echo "\nCurrently don't support locking GPU clocks of $MODEL ($DEVICE)"
+ echo "Currently don't support locking GPU clocks of $MODEL ($DEVICE)"
return -1
fi
if [ ${DEVICE} == "walleye" ] || [ ${DEVICE} == "taimen" ]; then
# Workaround crash
- echo "\nUnable to lock GPU clocks of $MODEL ($DEVICE)"
+ echo "Unable to lock GPU clocks of $MODEL ($DEVICE)"
return -1
fi
@@ -174,13 +293,13 @@ function_lock_gpu_kgsl() {
done
# (below, 100M = 1M for MHz * 100 for %)
- TARGET_FREQ_MHZ=`expr \( ${gpuMaxFreq} \* ${GPU_TARGET_FREQ_PERCENT} \) \/ 100000000`
+ TARGET_FREQ_MHZ=$(( (${gpuMaxFreq} * ${GPU_TARGET_FREQ_PERCENT}) / 100000000 ))
chosenFreq=${gpuMaxFreq}
index=0
chosenIndex=0
for freq in ${gpuAvailFreq}; do
- freqMhz=`expr ${freq} \/ 1000000`
+ freqMhz=$(( ${freq} / 1000000 ))
if [ ${freqMhz} -ge ${TARGET_FREQ_MHZ} ] && [ ${chosenFreq} -ge ${freq} ]; then
# note avail freq are generally in reverse order, so we don't break out of this loop
chosenFreq=${freq}
@@ -190,7 +309,7 @@ function_lock_gpu_kgsl() {
done
lastIndex=$(($index - 1))
- firstFreq=`echo $gpuAvailFreq | cut -d" " -f1`
+ firstFreq=`function_cut_first_from_space_seperated_list $gpuAvailFreq`
if [ ${gpuMaxFreq} != ${firstFreq} ]; then
# pwrlevel is index of desired freq among available frequencies, from highest to lowest.
@@ -226,24 +345,40 @@ function_lock_gpu_kgsl() {
echo "index = $chosenIndex"
exit -1
fi
- echo "\nLocked GPU to $chosenFreq / $gpuMaxFreq Hz"
+ echo "Locked GPU to $chosenFreq / $gpuMaxFreq Hz"
+}
+
+# cut is not available on some devices (Nexus 5 running LRX22C).
+function_cut_first_from_space_seperated_list() {
+ list=$1
+
+ for freq in $list; do
+ echo $freq
+ break
+ done
}
# kill processes that manage thermals / scaling
-stop thermal-engine
-stop perfd
-stop vendor.thermal-engine
-stop vendor.perfd
+stop thermal-engine || true
+stop perfd || true
+stop vendor.thermal-engine || true
+stop vendor.perfd || true
+setprop vendor.powerhal.init 0 || true
+setprop ctl.interface_restart android.hardware.power@1.0::IPower/default || true
function_lock_cpu
-function_lock_gpu_kgsl
+if [ "$DEVICE" -ne "wembley" ]; then
+ function_lock_gpu_kgsl
+else
+ echo "Unable to lock gpu clocks of $MODEL ($DEVICE)."
+fi
# Memory bus - hardcoded per-device for now
if [ ${DEVICE} == "marlin" ] || [ ${DEVICE} == "sailfish" ]; then
echo 13763 > /sys/class/devfreq/soc:qcom,gpubw/max_freq
else
- echo "\nUnable to lock memory bus of $MODEL ($DEVICE)."
+ echo "Unable to lock memory bus of $MODEL ($DEVICE)."
fi
-echo "\n$DEVICE clocks have been locked - to reset, reboot the device\n" \ No newline at end of file
+echo "$DEVICE clocks have been locked - to reset, reboot the device"
diff --git a/libs/hwui/tests/unit/CacheManagerTests.cpp b/libs/hwui/tests/unit/CacheManagerTests.cpp
index c83a3c88cbdd..edd3e4e4f4d4 100644
--- a/libs/hwui/tests/unit/CacheManagerTests.cpp
+++ b/libs/hwui/tests/unit/CacheManagerTests.cpp
@@ -26,7 +26,7 @@ using namespace android;
using namespace android::uirenderer;
using namespace android::uirenderer::renderthread;
-static size_t getCacheUsage(GrContext* grContext) {
+static size_t getCacheUsage(GrDirectContext* grContext) {
size_t cacheUsage;
grContext->getResourceCacheUsage(nullptr, &cacheUsage);
return cacheUsage;
@@ -35,7 +35,7 @@ static size_t getCacheUsage(GrContext* grContext) {
RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) {
int32_t width = DeviceInfo::get()->getWidth();
int32_t height = DeviceInfo::get()->getHeight();
- GrContext* grContext = renderThread.getGrContext();
+ GrDirectContext* grContext = renderThread.getGrContext();
ASSERT_TRUE(grContext != nullptr);
// create pairs of offscreen render targets and images until we exceed the
@@ -47,7 +47,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) {
sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kYes, info);
surface->getCanvas()->drawColor(SK_AlphaTRANSPARENT);
- grContext->flush();
+ grContext->flushAndSubmit();
surfaces.push_back(surface);
}
diff --git a/libs/hwui/tests/unit/CanvasContextTests.cpp b/libs/hwui/tests/unit/CanvasContextTests.cpp
index 28cff5b9b154..1771c3590e10 100644
--- a/libs/hwui/tests/unit/CanvasContextTests.cpp
+++ b/libs/hwui/tests/unit/CanvasContextTests.cpp
@@ -42,14 +42,3 @@ RENDERTHREAD_TEST(CanvasContext, create) {
canvasContext->destroy();
}
-
-RENDERTHREAD_TEST(CanvasContext, invokeFunctor) {
- TestUtils::MockFunctor functor;
- CanvasContext::invokeFunctor(renderThread, &functor);
- if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaVulkan) {
- // we currently don't support OpenGL WebViews on the Vulkan backend
- ASSERT_EQ(functor.getLastMode(), DrawGlInfo::kModeProcessNoContext);
- } else {
- ASSERT_EQ(functor.getLastMode(), DrawGlInfo::kModeProcess);
- }
-}
diff --git a/libs/hwui/tests/unit/FatalTestCanvas.h b/libs/hwui/tests/unit/FatalTestCanvas.h
index 1723c2eb4948..76ae0853b477 100644
--- a/libs/hwui/tests/unit/FatalTestCanvas.h
+++ b/libs/hwui/tests/unit/FatalTestCanvas.h
@@ -81,21 +81,6 @@ public:
const SkPaint*) {
ADD_FAILURE() << "onDrawImageLattice not expected in this test";
}
- void onDrawBitmap(const SkBitmap&, SkScalar dx, SkScalar dy, const SkPaint*) {
- ADD_FAILURE() << "onDrawBitmap not expected in this test";
- }
- void onDrawBitmapRect(const SkBitmap&, const SkRect*, const SkRect&, const SkPaint*,
- SrcRectConstraint) {
- ADD_FAILURE() << "onDrawBitmapRect not expected in this test";
- }
- void onDrawBitmapNine(const SkBitmap&, const SkIRect& center, const SkRect& dst,
- const SkPaint*) {
- ADD_FAILURE() << "onDrawBitmapNine not expected in this test";
- }
- void onDrawBitmapLattice(const SkBitmap&, const Lattice& lattice, const SkRect& dst,
- const SkPaint*) {
- ADD_FAILURE() << "onDrawBitmapLattice not expected in this test";
- }
void onClipRRect(const SkRRect& rrect, SkClipOp, ClipEdgeStyle) {
ADD_FAILURE() << "onClipRRect not expected in this test";
}
diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
index 3632be06c45f..7aa6be8722cf 100644
--- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
@@ -108,27 +108,27 @@ protected:
TEST(RenderNodeDrawable, zReorder) {
auto parent = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props,
SkiaRecordingCanvas& canvas) {
- canvas.insertReorderBarrier(true);
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(true);
+ canvas.enableZ(false);
drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder
drawOrderedRect(&canvas, 1);
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
drawOrderedNode(&canvas, 6, 2.0f);
drawOrderedRect(&canvas, 3);
drawOrderedNode(&canvas, 4, 0.0f);
drawOrderedRect(&canvas, 5);
drawOrderedNode(&canvas, 2, -2.0f);
drawOrderedNode(&canvas, 7, 2.0f);
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
drawOrderedRect(&canvas, 8);
drawOrderedNode(&canvas, 9, -10.0f); // in reorder=false at this point, so played inorder
- canvas.insertReorderBarrier(true); // reorder a node ahead of drawrect op
+ canvas.enableZ(true); // reorder a node ahead of drawrect op
drawOrderedRect(&canvas, 11);
drawOrderedNode(&canvas, 10, -1.0f);
- canvas.insertReorderBarrier(false);
- canvas.insertReorderBarrier(true); // test with two empty reorder sections
- canvas.insertReorderBarrier(true);
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
+ canvas.enableZ(true); // test with two empty reorder sections
+ canvas.enableZ(true);
+ canvas.enableZ(false);
drawOrderedRect(&canvas, 12);
});
@@ -1142,7 +1142,7 @@ TEST(ReorderBarrierDrawable, testShadowMatrix) {
0, 0, CANVAS_WIDTH, CANVAS_HEIGHT,
[](RenderProperties& props, SkiaRecordingCanvas& canvas) {
canvas.translate(TRANSLATE_X, TRANSLATE_Y);
- canvas.insertReorderBarrier(true);
+ canvas.enableZ(true);
auto node = TestUtils::createSkiaNode(
CASTER_X, CASTER_Y, CASTER_X + CASTER_WIDTH, CASTER_Y + CASTER_HEIGHT,
@@ -1152,7 +1152,7 @@ TEST(ReorderBarrierDrawable, testShadowMatrix) {
props.mutableOutline().setShouldClip(true);
});
canvas.drawRenderNode(node.get());
- canvas.insertReorderBarrier(false);
+ canvas.enableZ(false);
});
// create a canvas not backed by any device/pixels, but with dimensions to avoid quick rejection
@@ -1169,7 +1169,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaRecordingCanvas, drawVectorDrawable) {
class VectorDrawableTestCanvas : public TestCanvasBase {
public:
VectorDrawableTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {}
- void onDrawBitmapRect(const SkBitmap& bitmap, const SkRect* src, const SkRect& dst,
+ void onDrawImageRect(const SkImage*, const SkRect* src, const SkRect& dst,
const SkPaint* paint, SrcRectConstraint constraint) override {
const int index = mDrawCounter++;
switch (index) {
diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp
index 1cd9bd8ee9d9..c19e1ed6ce75 100644
--- a/libs/hwui/tests/unit/RenderNodeTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeTests.cpp
@@ -231,39 +231,41 @@ TEST(RenderNode, multiTreeValidity) {
}
TEST(RenderNode, releasedCallback) {
- class DecRefOnReleased : public GlFunctorLifecycleListener {
- public:
- explicit DecRefOnReleased(int* refcnt) : mRefCnt(refcnt) {}
- void onGlFunctorReleased(Functor* functor) override { *mRefCnt -= 1; }
-
- private:
- int* mRefCnt;
- };
-
- int refcnt = 0;
- sp<DecRefOnReleased> listener(new DecRefOnReleased(&refcnt));
- Functor noopFunctor;
+ int functor = WebViewFunctor_create(
+ nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES);
auto node = TestUtils::createNode(0, 0, 200, 400, [&](RenderProperties& props, Canvas& canvas) {
- refcnt++;
- canvas.callDrawGLFunction(&noopFunctor, listener.get());
+ canvas.drawWebViewFunctor(functor);
+ });
+ TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) {
+ TestUtils::syncHierarchyPropertiesAndDisplayList(node);
});
- TestUtils::syncHierarchyPropertiesAndDisplayList(node);
- EXPECT_EQ(1, refcnt);
+ auto& counts = TestUtils::countsForFunctor(functor);
+ EXPECT_EQ(1, counts.sync);
+ EXPECT_EQ(0, counts.destroyed);
TestUtils::recordNode(*node, [&](Canvas& canvas) {
- refcnt++;
- canvas.callDrawGLFunction(&noopFunctor, listener.get());
+ canvas.drawWebViewFunctor(functor);
});
- EXPECT_EQ(2, refcnt);
+ EXPECT_EQ(1, counts.sync);
+ EXPECT_EQ(0, counts.destroyed);
- TestUtils::syncHierarchyPropertiesAndDisplayList(node);
- EXPECT_EQ(1, refcnt);
+ TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) {
+ TestUtils::syncHierarchyPropertiesAndDisplayList(node);
+ });
+ EXPECT_EQ(2, counts.sync);
+ EXPECT_EQ(0, counts.destroyed);
+
+ WebViewFunctor_release(functor);
+ EXPECT_EQ(2, counts.sync);
+ EXPECT_EQ(0, counts.destroyed);
TestUtils::recordNode(*node, [](Canvas& canvas) {});
- EXPECT_EQ(1, refcnt);
- TestUtils::syncHierarchyPropertiesAndDisplayList(node);
- EXPECT_EQ(0, refcnt);
+ TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) {
+ TestUtils::syncHierarchyPropertiesAndDisplayList(node);
+ });
+ EXPECT_EQ(2, counts.sync);
+ EXPECT_EQ(1, counts.destroyed);
}
RENDERTHREAD_TEST(RenderNode, prepareTree_nullableDisplayList) {
diff --git a/libs/hwui/tests/unit/SkiaCanvasTests.cpp b/libs/hwui/tests/unit/SkiaCanvasTests.cpp
index fcc64fdd0be6..f77ca2a8c06c 100644
--- a/libs/hwui/tests/unit/SkiaCanvasTests.cpp
+++ b/libs/hwui/tests/unit/SkiaCanvasTests.cpp
@@ -73,7 +73,7 @@ TEST(SkiaCanvas, colorSpaceXform) {
// Test picture recording.
SkPictureRecorder recorder;
- SkCanvas* skPicCanvas = recorder.beginRecording(1, 1, NULL, 0);
+ SkCanvas* skPicCanvas = recorder.beginRecording(1, 1);
SkiaCanvas picCanvas(skPicCanvas);
picCanvas.drawBitmap(*adobeBitmap, 0, 0, nullptr);
sk_sp<SkPicture> picture = recorder.finishRecordingAsPicture();
@@ -104,7 +104,7 @@ TEST(SkiaCanvas, captureCanvasState) {
// Create a picture canvas.
SkPictureRecorder recorder;
- SkCanvas* skPicCanvas = recorder.beginRecording(1, 1, NULL, 0);
+ SkCanvas* skPicCanvas = recorder.beginRecording(1, 1);
SkiaCanvas picCanvas(skPicCanvas);
state = picCanvas.captureCanvasState();
diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
index d08aea668b2a..74a565439f85 100644
--- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
+++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
@@ -48,7 +48,10 @@ TEST(SkiaDisplayList, reset) {
SkCanvas dummyCanvas;
RenderNodeDrawable drawable(nullptr, &dummyCanvas);
skiaDL->mChildNodes.emplace_back(nullptr, &dummyCanvas);
- GLFunctorDrawable functorDrawable(nullptr, nullptr, &dummyCanvas);
+ int functor1 = WebViewFunctor_create(
+ nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES);
+ GLFunctorDrawable functorDrawable{functor1, &dummyCanvas};
+ WebViewFunctor_release(functor1);
skiaDL->mChildFunctors.push_back(&functorDrawable);
skiaDL->mMutableImages.push_back(nullptr);
skiaDL->appendVD(nullptr);
@@ -97,16 +100,13 @@ TEST(SkiaDisplayList, syncContexts) {
SkiaDisplayList skiaDL;
SkCanvas dummyCanvas;
- TestUtils::MockFunctor functor;
- GLFunctorDrawable functorDrawable(&functor, nullptr, &dummyCanvas);
- skiaDL.mChildFunctors.push_back(&functorDrawable);
- int functor2 = WebViewFunctor_create(
+ int functor1 = WebViewFunctor_create(
nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES);
- auto& counts = TestUtils::countsForFunctor(functor2);
+ auto& counts = TestUtils::countsForFunctor(functor1);
skiaDL.mChildFunctors.push_back(
- skiaDL.allocateDrawable<GLFunctorDrawable>(functor2, &dummyCanvas));
- WebViewFunctor_release(functor2);
+ skiaDL.allocateDrawable<GLFunctorDrawable>(functor1, &dummyCanvas));
+ WebViewFunctor_release(functor1);
SkRect bounds = SkRect::MakeWH(200, 200);
VectorDrawableRoot vectorDrawable(new VectorDrawable::Group());
@@ -120,7 +120,6 @@ TEST(SkiaDisplayList, syncContexts) {
});
});
- EXPECT_EQ(functor.getLastMode(), DrawGlInfo::kModeSync);
EXPECT_EQ(counts.sync, 1);
EXPECT_EQ(counts.destroyed, 0);
EXPECT_EQ(vectorDrawable.mutateProperties()->getBounds(), bounds);
diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp
index 1a09b1c52d8a..5d2aa2ff83c9 100644
--- a/libs/hwui/tests/unit/TypefaceTests.cpp
+++ b/libs/hwui/tests/unit/TypefaceTests.cpp
@@ -31,10 +31,12 @@ using namespace android;
namespace {
-constexpr char kRobotoRegular[] = "/system/fonts/Roboto-Regular.ttf";
-constexpr char kRobotoBold[] = "/system/fonts/Roboto-Bold.ttf";
-constexpr char kRobotoItalic[] = "/system/fonts/Roboto-Italic.ttf";
-constexpr char kRobotoBoldItalic[] = "/system/fonts/Roboto-BoldItalic.ttf";
+constexpr char kRobotoVariable[] = "/system/fonts/Roboto-Regular.ttf";
+
+constexpr char kRegularFont[] = "/system/fonts/NotoSerif-Regular.ttf";
+constexpr char kBoldFont[] = "/system/fonts/NotoSerif-Bold.ttf";
+constexpr char kItalicFont[] = "/system/fonts/NotoSerif-Italic.ttf";
+constexpr char kBoldItalicFont[] = "/system/fonts/NotoSerif-BoldItalic.ttf";
void unmap(const void* ptr, void* context) {
void* p = const_cast<void*>(ptr);
@@ -57,7 +59,7 @@ std::shared_ptr<minikin::FontFamily> buildFamily(const char* fileName) {
std::shared_ptr<minikin::MinikinFont> font =
std::make_shared<MinikinFontSkia>(std::move(typeface), data, st.st_size, fileName, 0,
std::vector<minikin::FontVariation>());
- std::vector<minikin::Font> fonts;
+ std::vector<std::shared_ptr<minikin::Font>> fonts;
fonts.push_back(minikin::Font::Builder(font).build());
return std::make_shared<minikin::FontFamily>(std::move(fonts));
}
@@ -68,7 +70,7 @@ std::vector<std::shared_ptr<minikin::FontFamily>> makeSingleFamlyVector(const ch
TEST(TypefaceTest, resolveDefault_and_setDefaultTest) {
std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
- makeSingleFamlyVector(kRobotoRegular), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
EXPECT_EQ(regular.get(), Typeface::resolveDefault(regular.get()));
// Keep the original to restore it later.
@@ -347,71 +349,71 @@ TEST(TypefaceTest, createFromFamilies_Single) {
// In Java, new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build();
std::unique_ptr<Typeface> regular(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoRegular), 400, false));
+ Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, false));
EXPECT_EQ(400, regular->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
// In Java, new
- // Typeface.Builder("Roboto-Bold.ttf").setWeight(700).setItalic(false).build();
+ // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build();
std::unique_ptr<Typeface> bold(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBold), 700, false));
+ Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, false));
EXPECT_EQ(700, bold->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
// In Java, new
- // Typeface.Builder("Roboto-Italic.ttf").setWeight(400).setItalic(true).build();
+ // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build();
std::unique_ptr<Typeface> italic(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoItalic), 400, true));
+ Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, true));
EXPECT_EQ(400, italic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
// In Java,
// new
- // Typeface.Builder("Roboto-BoldItalic.ttf").setWeight(700).setItalic(true).build();
+ // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build();
std::unique_ptr<Typeface> boldItalic(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBoldItalic), 700, true));
+ Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, true));
EXPECT_EQ(700, boldItalic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
// In Java,
// new
- // Typeface.Builder("Roboto-BoldItalic.ttf").setWeight(1100).setItalic(false).build();
+ // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build();
std::unique_ptr<Typeface> over1000(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBold), 1100, false));
+ Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 1100, false));
EXPECT_EQ(1000, over1000->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
}
TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
- // In Java, new Typeface.Builder("Roboto-Regular.ttf").build();
+ // In Java, new Typeface.Builder("Family-Regular.ttf").build();
std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
- makeSingleFamlyVector(kRobotoRegular), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
EXPECT_EQ(400, regular->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
- // In Java, new Typeface.Builder("Roboto-Bold.ttf").build();
+ // In Java, new Typeface.Builder("Family-Bold.ttf").build();
std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
- makeSingleFamlyVector(kRobotoBold), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
EXPECT_EQ(700, bold->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
- // In Java, new Typeface.Builder("Roboto-Italic.ttf").build();
+ // In Java, new Typeface.Builder("Family-Italic.ttf").build();
std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
- makeSingleFamlyVector(kRobotoItalic), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
EXPECT_EQ(400, italic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
- // In Java, new Typeface.Builder("Roboto-BoldItalic.ttf").build();
+ // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build();
std::unique_ptr<Typeface> boldItalic(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBoldItalic),
+ Typeface::createFromFamilies(makeSingleFamlyVector(kBoldItalicFont),
RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
EXPECT_EQ(700, boldItalic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
@@ -420,8 +422,8 @@ TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
TEST(TypefaceTest, createFromFamilies_Family) {
std::vector<std::shared_ptr<minikin::FontFamily>> families = {
- buildFamily(kRobotoRegular), buildFamily(kRobotoBold), buildFamily(kRobotoItalic),
- buildFamily(kRobotoBoldItalic)};
+ buildFamily(kRegularFont), buildFamily(kBoldFont), buildFamily(kItalicFont),
+ buildFamily(kBoldItalicFont)};
std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies(
std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
EXPECT_EQ(400, typeface->fStyle.weight());
@@ -430,7 +432,7 @@ TEST(TypefaceTest, createFromFamilies_Family) {
TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) {
std::vector<std::shared_ptr<minikin::FontFamily>> families = {
- buildFamily(kRobotoBold), buildFamily(kRobotoItalic), buildFamily(kRobotoBoldItalic)};
+ buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)};
std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies(
std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
EXPECT_EQ(700, typeface->fStyle.weight());
diff --git a/libs/hwui/utils/Blur.h b/libs/hwui/utils/Blur.h
index d6b41b83def8..6b822f01e25c 100644
--- a/libs/hwui/utils/Blur.h
+++ b/libs/hwui/utils/Blur.h
@@ -26,9 +26,9 @@ namespace uirenderer {
class Blur {
public:
// If radius > 0, return the corresponding sigma, else return 0
- ANDROID_API static float convertRadiusToSigma(float radius);
+ static float convertRadiusToSigma(float radius);
// If sigma > 0.5, return the corresponding radius, else return 0
- ANDROID_API static float convertSigmaToRadius(float sigma);
+ static float convertSigmaToRadius(float sigma);
// If the original radius was on an integer boundary then after the sigma to
// radius conversion a small rounding error may be introduced. This function
// accounts for that error and snaps to the appropriate integer boundary.
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
index 71a27ced2e09..87512f0354c8 100644
--- a/libs/hwui/utils/Color.cpp
+++ b/libs/hwui/utils/Color.cpp
@@ -16,8 +16,8 @@
#include "Color.h"
-#include <utils/Log.h>
#include <ui/ColorSpace.h>
+#include <utils/Log.h>
#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows
#include <android/hardware_buffer.h>
@@ -26,6 +26,7 @@
#include <algorithm>
#include <cmath>
+#include <Properties.h>
namespace android {
namespace uirenderer {
@@ -72,46 +73,34 @@ SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc,
sk_sp<SkColorSpace> colorSpace) {
return createImageInfo(bufferDesc.width, bufferDesc.height, bufferDesc.format, colorSpace);
}
-#endif
-android::PixelFormat ColorTypeToPixelFormat(SkColorType colorType) {
+uint32_t ColorTypeToBufferFormat(SkColorType colorType) {
switch (colorType) {
case kRGBA_8888_SkColorType:
- return PIXEL_FORMAT_RGBA_8888;
+ return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
case kRGBA_F16_SkColorType:
- return PIXEL_FORMAT_RGBA_FP16;
+ return AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT;
case kRGB_565_SkColorType:
- return PIXEL_FORMAT_RGB_565;
+ return AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM;
case kRGB_888x_SkColorType:
- return PIXEL_FORMAT_RGBX_8888;
+ return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM;
case kRGBA_1010102_SkColorType:
- return PIXEL_FORMAT_RGBA_1010102;
+ return AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM;
case kARGB_4444_SkColorType:
- return PIXEL_FORMAT_RGBA_4444;
+ // Hardcoding the value from android::PixelFormat
+ static constexpr uint64_t kRGBA4444 = 7;
+ return kRGBA4444;
default:
ALOGV("Unsupported colorType: %d, return RGBA_8888 by default", (int)colorType);
- return PIXEL_FORMAT_RGBA_8888;
- }
-}
-
-SkColorType PixelFormatToColorType(android::PixelFormat format) {
- switch (format) {
- case PIXEL_FORMAT_RGBX_8888: return kRGB_888x_SkColorType;
- case PIXEL_FORMAT_RGBA_8888: return kRGBA_8888_SkColorType;
- case PIXEL_FORMAT_RGBA_FP16: return kRGBA_F16_SkColorType;
- case PIXEL_FORMAT_RGB_565: return kRGB_565_SkColorType;
- case PIXEL_FORMAT_RGBA_1010102: return kRGBA_1010102_SkColorType;
- case PIXEL_FORMAT_RGBA_4444: return kARGB_4444_SkColorType;
- default:
- ALOGV("Unsupported PixelFormat: %d, return kUnknown_SkColorType by default", format);
- return kUnknown_SkColorType;
+ return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
}
}
+#endif
namespace {
static constexpr skcms_TransferFunction k2Dot6 = {2.6f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
-// Skia's SkNamedGamut::kDCIP3 is based on a white point of D65. This gamut
+// Skia's SkNamedGamut::kDisplayP3 is based on a white point of D65. This gamut
// matches the white point used by ColorSpace.Named.DCIP3.
static constexpr skcms_Matrix3x3 kDCIP3 = {{
{0.486143, 0.323835, 0.154234},
@@ -180,7 +169,7 @@ android_dataspace ColorSpaceToADataSpace(SkColorSpace* colorSpace, SkColorType c
}
}
- if (nearlyEqual(fn, SkNamedTransferFn::kSRGB) && nearlyEqual(gamut, SkNamedGamut::kDCIP3)) {
+ if (nearlyEqual(fn, SkNamedTransferFn::kSRGB) && nearlyEqual(gamut, SkNamedGamut::kDisplayP3)) {
return HAL_DATASPACE_DISPLAY_P3;
}
@@ -221,7 +210,7 @@ sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace) {
gamut = SkNamedGamut::kRec2020;
break;
case HAL_DATASPACE_STANDARD_DCI_P3:
- gamut = SkNamedGamut::kDCIP3;
+ gamut = SkNamedGamut::kDisplayP3;
break;
case HAL_DATASPACE_STANDARD_ADOBE_RGB:
gamut = SkNamedGamut::kAdobeRGB;
@@ -356,5 +345,23 @@ SkColor LabToSRGB(const Lab& lab, SkAlpha alpha) {
static_cast<uint8_t>(rgb.b * 255));
}
+skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) {
+ if (sdr_white_level <= 0.f) {
+ sdr_white_level = Properties::defaultSdrWhitePoint;
+ }
+ // The generic PQ transfer function produces normalized luminance values i.e.
+ // the range 0-1 represents 0-10000 nits for the reference display, but we
+ // want to map 1.0 to |sdr_white_level| nits so we need to scale accordingly.
+ const double w = 10000. / sdr_white_level;
+ // Distribute scaling factor W by scaling A and B with X ^ (1/F):
+ // ((A + Bx^C) / (D + Ex^C))^F * W = ((A + Bx^C) / (D + Ex^C) * W^(1/F))^F
+ // See https://crbug.com/1058580#c32 for discussion.
+ skcms_TransferFunction fn = SkNamedTransferFn::kPQ;
+ const double ws = pow(w, 1. / fn.f);
+ fn.a = ws * fn.a;
+ fn.b = ws * fn.b;
+ return fn;
+}
+
} // namespace uirenderer
} // namespace android
diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h
index a76f7e499c37..1654072fd264 100644
--- a/libs/hwui/utils/Color.h
+++ b/libs/hwui/utils/Color.h
@@ -16,14 +16,12 @@
#ifndef COLOR_H
#define COLOR_H
-#include <math.h>
-#include <cutils/compiler.h>
-#include <system/graphics.h>
-#include <ui/PixelFormat.h>
-
#include <SkColor.h>
#include <SkColorSpace.h>
#include <SkImageInfo.h>
+#include <cutils/compiler.h>
+#include <math.h>
+#include <system/graphics.h>
struct ANativeWindow_Buffer;
struct AHardwareBuffer_Desc;
@@ -93,15 +91,14 @@ static constexpr float EOCF_sRGB(float srgb) {
}
#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows
-ANDROID_API SkImageInfo ANativeWindowToImageInfo(const ANativeWindow_Buffer& buffer,
+SkImageInfo ANativeWindowToImageInfo(const ANativeWindow_Buffer& buffer,
sk_sp<SkColorSpace> colorSpace);
SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc,
sk_sp<SkColorSpace> colorSpace);
-#endif
-android::PixelFormat ColorTypeToPixelFormat(SkColorType colorType);
-ANDROID_API SkColorType PixelFormatToColorType(android::PixelFormat format);
+uint32_t ColorTypeToBufferFormat(SkColorType colorType);
+#endif
ANDROID_API sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace);
@@ -129,6 +126,7 @@ struct Lab {
Lab sRGBToLab(SkColor color);
SkColor LabToSRGB(const Lab& lab, SkAlpha alpha);
+skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level = 0.f);
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/utils/MathUtils.h b/libs/hwui/utils/MathUtils.h
index cc8d83f10d43..62bf39ca8a7a 100644
--- a/libs/hwui/utils/MathUtils.h
+++ b/libs/hwui/utils/MathUtils.h
@@ -31,7 +31,9 @@ public:
* Check for floats that are close enough to zero.
*/
inline static bool isZero(float value) {
- return (value >= -NON_ZERO_EPSILON) && (value <= NON_ZERO_EPSILON);
+ // Using fabsf is more performant as ARM computes
+ // fabsf in a single instruction.
+ return fabsf(value) <= NON_ZERO_EPSILON;
}
inline static bool isOne(float value) {
diff --git a/libs/hwui/GlFunctorLifecycleListener.h b/libs/hwui/utils/NdkUtils.cpp
index 5adc46961c8b..de6274ee5bcc 100644
--- a/libs/hwui/GlFunctorLifecycleListener.h
+++ b/libs/hwui/utils/NdkUtils.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright 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.
@@ -14,19 +14,19 @@
* limitations under the License.
*/
-#pragma once
-
-#include <utils/Functor.h>
-#include <utils/RefBase.h>
+#include <utils/NdkUtils.h>
namespace android {
namespace uirenderer {
-class GlFunctorLifecycleListener : public VirtualLightRefBase {
-public:
- virtual ~GlFunctorLifecycleListener() {}
- virtual void onGlFunctorReleased(Functor* functor) = 0;
-};
+UniqueAHardwareBuffer allocateAHardwareBuffer(const AHardwareBuffer_Desc& desc) {
+ AHardwareBuffer* buffer;
+ if (AHardwareBuffer_allocate(&desc, &buffer) != 0) {
+ return nullptr;
+ } else {
+ return UniqueAHardwareBuffer{buffer};
+ }
+}
} // namespace uirenderer
} // namespace android
diff --git a/libs/hwui/utils/NdkUtils.h b/libs/hwui/utils/NdkUtils.h
new file mode 100644
index 000000000000..f218eb2ff404
--- /dev/null
+++ b/libs/hwui/utils/NdkUtils.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <android/hardware_buffer.h>
+
+#include <memory>
+
+namespace android {
+namespace uirenderer {
+
+// Deleter for an AHardwareBuffer, to be passed to an std::unique_ptr.
+struct AHardwareBuffer_deleter {
+ void operator()(AHardwareBuffer* ahb) const { AHardwareBuffer_release(ahb); }
+};
+
+using UniqueAHardwareBuffer = std::unique_ptr<AHardwareBuffer, AHardwareBuffer_deleter>;
+
+// Allocates a UniqueAHardwareBuffer with the provided buffer description.
+// Returns nullptr if allocation did not succeed.
+UniqueAHardwareBuffer allocateAHardwareBuffer(const AHardwareBuffer_Desc& desc);
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/utils/VectorDrawableUtils.h b/libs/hwui/utils/VectorDrawableUtils.h
index 4be48fb942fc..4f63959165db 100644
--- a/libs/hwui/utils/VectorDrawableUtils.h
+++ b/libs/hwui/utils/VectorDrawableUtils.h
@@ -28,10 +28,10 @@ namespace uirenderer {
class VectorDrawableUtils {
public:
- ANDROID_API static bool canMorph(const PathData& morphFrom, const PathData& morphTo);
- ANDROID_API static bool interpolatePathData(PathData* outData, const PathData& morphFrom,
+ static bool canMorph(const PathData& morphFrom, const PathData& morphTo);
+ static bool interpolatePathData(PathData* outData, const PathData& morphFrom,
const PathData& morphTo, float fraction);
- ANDROID_API static void verbsToPath(SkPath* outPath, const PathData& data);
+ static void verbsToPath(SkPath* outPath, const PathData& data);
static void interpolatePaths(PathData* outPathData, const PathData& from, const PathData& to,
float fraction);
};
diff --git a/libs/usb/tests/AccessoryChat/AndroidManifest.xml b/libs/usb/tests/AccessoryChat/AndroidManifest.xml
index 6667ebaa4d49..b93eeab11324 100644
--- a/libs/usb/tests/AccessoryChat/AndroidManifest.xml
+++ b/libs/usb/tests/AccessoryChat/AndroidManifest.xml
@@ -15,26 +15,28 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.accessorychat">
+ package="com.android.accessorychat">
- <uses-feature android:name="android.hardware.usb.accessory" />
+ <uses-feature android:name="android.hardware.usb.accessory"/>
<application android:label="Accessory Chat">
- <activity android:name="AccessoryChat" android:label="Accessory Chat">
+ <activity android:name="AccessoryChat"
+ android:label="Accessory Chat"
+ android:exported="true">
<intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.LAUNCHER" />
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
- <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
+ <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/>
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
- android:resource="@xml/accessory_filter" />
+ android:resource="@xml/accessory_filter"/>
</activity>
</application>
- <uses-sdk android:minSdkVersion="12" />
+ <uses-sdk android:minSdkVersion="12"/>
</manifest>