From ff322310a319d6ac671bf045f071adcf71c5da6f Mon Sep 17 00:00:00 2001 From: Schneider Victor-tulias Date: Thu, 22 Jul 2021 16:24:10 -0700 Subject: [PATCH 01/11] Convert gesture navigation tutorial still image vector drawables to layout files Bug: 192939067 Test: manual, started full tutorial, started home, back and overview individually, locked and unlocked phone mid-tutorial step. Change-Id: I1cba811c556139bc9ba52459f1b27f21eb156ac0 --- ...e.xml => gesture_tutorial_motion_home.xml} | 2 +- ...esture_tutorial_motion_home_light_mode.xml | 1254 ------------- ...l => gesture_tutorial_motion_overview.xml} | 2 +- .../res}/drawable/gesture_tutorial_ripple.xml | 0 quickstep/res/drawable/mock_conversation.xml | 212 --- .../res/drawable/mock_conversations_list.xml | 361 ---- .../res/drawable/mock_webpage_dark_mode.xml | 251 --- .../res/drawable/mock_webpage_light_mode.xml | 263 --- .../res/layout/gesture_tutorial_dialog.xml | 14 + .../res/layout/gesture_tutorial_fragment.xml | 2 +- .../gesture_tutorial_mock_conversation.xml | 242 +++ ...esture_tutorial_mock_conversation_list.xml | 394 ++++ .../layout/gesture_tutorial_mock_webpage.xml | 274 +++ quickstep/res/values-night/colors.xml | 25 + quickstep/res/values/colors.xml | 38 +- .../BackGestureTutorialController.java | 20 +- .../interaction/EdgeBackGesturePanel.java | 4 +- .../HomeGestureTutorialController.java | 4 +- .../HomeGestureTutorialFragment.java | 4 +- .../OverviewGestureTutorialController.java | 4 +- .../OverviewGestureTutorialFragment.java | 4 +- .../interaction/TutorialController.java | 30 +- ...re_tutorial_motion_overview_light_mode.xml | 1587 ----------------- res/values/colors.xml | 9 - 24 files changed, 1030 insertions(+), 3970 deletions(-) rename quickstep/res/drawable/{gesture_tutorial_motion_home_dark_mode.xml => gesture_tutorial_motion_home.xml} (99%) delete mode 100644 quickstep/res/drawable/gesture_tutorial_motion_home_light_mode.xml rename quickstep/res/drawable/{gesture_tutorial_motion_overview_dark_mode.xml => gesture_tutorial_motion_overview.xml} (99%) rename {res => quickstep/res}/drawable/gesture_tutorial_ripple.xml (100%) delete mode 100644 quickstep/res/drawable/mock_conversation.xml delete mode 100644 quickstep/res/drawable/mock_conversations_list.xml delete mode 100644 quickstep/res/drawable/mock_webpage_dark_mode.xml delete mode 100644 quickstep/res/drawable/mock_webpage_light_mode.xml create mode 100644 quickstep/res/layout/gesture_tutorial_mock_conversation.xml create mode 100644 quickstep/res/layout/gesture_tutorial_mock_conversation_list.xml create mode 100644 quickstep/res/layout/gesture_tutorial_mock_webpage.xml create mode 100644 quickstep/res/values-night/colors.xml delete mode 100644 res/drawable/gesture_tutorial_motion_overview_light_mode.xml diff --git a/quickstep/res/drawable/gesture_tutorial_motion_home_dark_mode.xml b/quickstep/res/drawable/gesture_tutorial_motion_home.xml similarity index 99% rename from quickstep/res/drawable/gesture_tutorial_motion_home_dark_mode.xml rename to quickstep/res/drawable/gesture_tutorial_motion_home.xml index aff35c1bce..020d0b1319 100644 --- a/quickstep/res/drawable/gesture_tutorial_motion_home_dark_mode.xml +++ b/quickstep/res/drawable/gesture_tutorial_motion_home.xml @@ -922,7 +922,7 @@ diff --git a/quickstep/res/drawable/gesture_tutorial_motion_home_light_mode.xml b/quickstep/res/drawable/gesture_tutorial_motion_home_light_mode.xml deleted file mode 100644 index 98d97ad62d..0000000000 --- a/quickstep/res/drawable/gesture_tutorial_motion_home_light_mode.xml +++ /dev/null @@ -1,1254 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/drawable/gesture_tutorial_motion_overview_dark_mode.xml b/quickstep/res/drawable/gesture_tutorial_motion_overview.xml similarity index 99% rename from quickstep/res/drawable/gesture_tutorial_motion_overview_dark_mode.xml rename to quickstep/res/drawable/gesture_tutorial_motion_overview.xml index b007d20f34..3f8b3f58b9 100644 --- a/quickstep/res/drawable/gesture_tutorial_motion_overview_dark_mode.xml +++ b/quickstep/res/drawable/gesture_tutorial_motion_overview.xml @@ -860,7 +860,7 @@ diff --git a/res/drawable/gesture_tutorial_ripple.xml b/quickstep/res/drawable/gesture_tutorial_ripple.xml similarity index 100% rename from res/drawable/gesture_tutorial_ripple.xml rename to quickstep/res/drawable/gesture_tutorial_ripple.xml diff --git a/quickstep/res/drawable/mock_conversation.xml b/quickstep/res/drawable/mock_conversation.xml deleted file mode 100644 index 272d9ed8a8..0000000000 --- a/quickstep/res/drawable/mock_conversation.xml +++ /dev/null @@ -1,212 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/drawable/mock_conversations_list.xml b/quickstep/res/drawable/mock_conversations_list.xml deleted file mode 100644 index 2dbc88f0bf..0000000000 --- a/quickstep/res/drawable/mock_conversations_list.xml +++ /dev/null @@ -1,361 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/drawable/mock_webpage_dark_mode.xml b/quickstep/res/drawable/mock_webpage_dark_mode.xml deleted file mode 100644 index 93b22b7d31..0000000000 --- a/quickstep/res/drawable/mock_webpage_dark_mode.xml +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/drawable/mock_webpage_light_mode.xml b/quickstep/res/drawable/mock_webpage_light_mode.xml deleted file mode 100644 index 98abb92ab7..0000000000 --- a/quickstep/res/drawable/mock_webpage_light_mode.xml +++ /dev/null @@ -1,263 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/layout/gesture_tutorial_dialog.xml b/quickstep/res/layout/gesture_tutorial_dialog.xml index 59bf7b965c..db6ec8533f 100644 --- a/quickstep/res/layout/gesture_tutorial_dialog.xml +++ b/quickstep/res/layout/gesture_tutorial_dialog.xml @@ -1,4 +1,18 @@ + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout/gesture_tutorial_mock_conversation_list.xml b/quickstep/res/layout/gesture_tutorial_mock_conversation_list.xml new file mode 100644 index 0000000000..ad6b165019 --- /dev/null +++ b/quickstep/res/layout/gesture_tutorial_mock_conversation_list.xml @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout/gesture_tutorial_mock_webpage.xml b/quickstep/res/layout/gesture_tutorial_mock_webpage.xml new file mode 100644 index 0000000000..ab00a113b4 --- /dev/null +++ b/quickstep/res/layout/gesture_tutorial_mock_webpage.xml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/values-night/colors.xml b/quickstep/res/values-night/colors.xml new file mode 100644 index 0000000000..c3b253654e --- /dev/null +++ b/quickstep/res/values-night/colors.xml @@ -0,0 +1,25 @@ + + + + + #99000000 + + #000000 + + #202124 + #3c4043 + + \ No newline at end of file diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml index 17980f0eeb..47552924ef 100644 --- a/quickstep/res/values/colors.xml +++ b/quickstep/res/values/colors.xml @@ -14,8 +14,6 @@ limitations under the License. --> - #FFFFFFFF - #99000000 #fff #39000000 @@ -31,4 +29,40 @@ #EBffffff #99000000 + + + #FFFFFFFF + + #f9f9f9 + #A0C2F9 + #6DA1FF + + #3C4043 + #FF000000 + #B7F29F + + + #f1f3f4 + #e8eaed + #dadce0 + #bdc1c6 + #e8eaed + #dadce0 + #dadce0 + + + #dadce0 + #e8eaed + #f8f9fa + #9aa0a6 + #bdc1c6 + #bdc1c6 + + + #f1f3f4 + #6e7175 + #9a9a9a + #e8eaed + #80868b + #bdc1c6 \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java index 3cb22f4ffe..1afcb321a5 100644 --- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java @@ -18,10 +18,9 @@ package com.android.quickstep.interaction; import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION; import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE; +import android.annotation.LayoutRes; import android.graphics.PointF; -import androidx.appcompat.content.res.AppCompatResources; - import com.android.launcher3.R; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult; @@ -44,8 +43,18 @@ final class BackGestureTutorialController extends TutorialController { } @Override - protected int getMockAppTaskThumbnailResId(boolean forDarkMode) { - return R.drawable.mock_conversation; + protected int getMockAppTaskLayoutResId() { + return getMockAppTaskCurrentPageLayoutResId(); + } + + @LayoutRes + int getMockAppTaskCurrentPageLayoutResId() { + return R.layout.gesture_tutorial_mock_conversation; + } + + @LayoutRes + int getMockAppTaskPreviousPageLayoutResId() { + return R.layout.gesture_tutorial_mock_conversation_list; } @Override @@ -72,8 +81,7 @@ final class BackGestureTutorialController extends TutorialController { case BACK_COMPLETED_FROM_RIGHT: mTutorialFragment.releaseGestureVideoView(); hideFeedback(true); - mFakeTaskView.setBackground(AppCompatResources.getDrawable(mContext, - R.drawable.mock_conversations_list)); + updateFakeAppTaskViewLayout(getMockAppTaskPreviousPageLayoutResId()); int subtitleResId = mTutorialFragment.isAtFinalStep() ? R.string.back_gesture_feedback_complete_without_follow_up : R.string.back_gesture_feedback_complete_with_overview_follow_up; diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java index 0521db4d1b..7465db3025 100644 --- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java +++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java @@ -282,9 +282,7 @@ public class EdgeBackGesturePanel extends View { .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - mPaint.setColor(context.getColor(currentNightMode == Configuration.UI_MODE_NIGHT_YES - ? R.color.back_arrow_color_light - : R.color.back_arrow_color_dark)); + mPaint.setColor(context.getColor(R.color.gesture_tutorial_back_arrow_color)); loadDimens(); updateArrowDirection(); diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java index 819c91c55b..10a0b2e87b 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java @@ -42,8 +42,8 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll } @Override - protected int getMockAppTaskThumbnailResId(boolean forDarkMode) { - return forDarkMode ? R.drawable.mock_webpage_dark_mode : R.drawable.mock_webpage_light_mode; + protected int getMockAppTaskLayoutResId() { + return R.layout.gesture_tutorial_mock_webpage; } @Override diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java index 9572637b0f..9206910bdb 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java @@ -25,9 +25,7 @@ public class HomeGestureTutorialFragment extends TutorialFragment { @Nullable @Override Integer getFeedbackVideoResId(boolean forDarkMode) { - return forDarkMode - ? R.drawable.gesture_tutorial_motion_home_dark_mode - : R.drawable.gesture_tutorial_motion_home_light_mode; + return R.drawable.gesture_tutorial_motion_home; } @Nullable diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java index 77ddb2b320..9e2c0babf4 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java @@ -49,8 +49,8 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont } @Override - protected int getMockAppTaskThumbnailResId(boolean forDarkMode) { - return R.drawable.mock_conversations_list; + protected int getMockAppTaskLayoutResId() { + return R.layout.gesture_tutorial_mock_conversation_list; } @Override diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java index d2ec327c81..601358260f 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java @@ -25,9 +25,7 @@ public class OverviewGestureTutorialFragment extends TutorialFragment { @Nullable @Override Integer getFeedbackVideoResId(boolean forDarkMode) { - return forDarkMode - ? R.drawable.gesture_tutorial_motion_overview_dark_mode - : R.drawable.gesture_tutorial_motion_overview_light_mode; + return R.drawable.gesture_tutorial_motion_overview; } @Nullable diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index 4b4e7e6753..e90ca50619 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -16,6 +16,8 @@ package com.android.quickstep.interaction; import static android.view.View.GONE; +import static android.view.View.NO_ID; +import static android.view.View.inflate; import android.content.Context; import android.content.pm.PackageManager; @@ -29,12 +31,14 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.CallSuper; import androidx.annotation.DrawableRes; +import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -73,7 +77,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, final RelativeLayout mFakeLauncherView; final ImageView mFakeHotseatView; final ClipIconView mFakeIconView; - final View mFakeTaskView; + final FrameLayout mFakeTaskView; final View mFakePreviousTaskView; final View mRippleView; final RippleDrawable mRippleDrawable; @@ -134,9 +138,9 @@ abstract class TutorialController implements BackGestureAttemptCallback, return R.drawable.default_sandbox_mock_launcher; } - @DrawableRes - protected int getMockAppTaskThumbnailResId(boolean forDarkMode) { - return R.drawable.default_sandbox_app_task_thumbnail; + @LayoutRes + protected int getMockAppTaskLayoutResId() { + return View.NO_ID; } @DrawableRes @@ -395,6 +399,17 @@ abstract class TutorialController implements BackGestureAttemptCallback, mActionButton.setOnClickListener(this::onActionButtonClicked); } + void updateFakeAppTaskViewLayout(@LayoutRes int mockAppTaskLayoutResId) { + mFakeTaskView.removeAllViews(); + if (mockAppTaskLayoutResId != NO_ID) { + mFakeTaskView.addView( + inflate(mContext, mockAppTaskLayoutResId, null), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + } + } + private void updateSubtext() { mTutorialStepView.setTutorialProgress( mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps()); @@ -406,13 +421,10 @@ abstract class TutorialController implements BackGestureAttemptCallback, mContext, getMockWallpaperResId())); mTutorialFragment.updateFeedbackVideo(); mFakeLauncherView.setBackgroundColor( - mContext.getColor(Utilities.isDarkTheme(mContext) - ? R.color.fake_wallpaper_color_dark_mode - : R.color.fake_wallpaper_color_light_mode)); + mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); mFakeHotseatView.setImageDrawable(AppCompatResources.getDrawable( mContext, getMockHotseatResId())); - mFakeTaskView.setBackground(AppCompatResources.getDrawable( - mContext, getMockAppTaskThumbnailResId(Utilities.isDarkTheme(mContext)))); + updateFakeAppTaskViewLayout(getMockAppTaskLayoutResId()); mFakeTaskView.animate().alpha(1).setListener( AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel())); mFakePreviousTaskView.setBackground(AppCompatResources.getDrawable( diff --git a/res/drawable/gesture_tutorial_motion_overview_light_mode.xml b/res/drawable/gesture_tutorial_motion_overview_light_mode.xml deleted file mode 100644 index 75887c9352..0000000000 --- a/res/drawable/gesture_tutorial_motion_overview_light_mode.xml +++ /dev/null @@ -1,1587 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index cc5c5a39d3..5020127440 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -36,15 +36,6 @@ #E0E0E0 - #A0C2F9 - #6DA1FF - #000000 - #f9f9f9 - - #3C4043 - #FF000000 - #B7F29F - #FFF #F1F3F4 #E0E0E0 From 69c4b54dced0780106aa5267ada62a1b1d727935 Mon Sep 17 00:00:00 2001 From: Schneider Victor-tulias Date: Fri, 23 Jul 2021 17:28:42 -0700 Subject: [PATCH 02/11] Convert gesture navigation feedback videos to animations with real callbacks. Fixes: 192939067 Test: manual, started full tutorial, started home, back and overview individually, locked and unlocked phone mid-tutorial step. Change-Id: I3e7d3c6153eeff6273e4b7ebc8cdb8328df7facc --- .../drawable/gesture_tutorial_finger_dot.xml | 6 + .../drawable/gesture_tutorial_motion_back.xml | 1233 ------------- .../drawable/gesture_tutorial_motion_home.xml | 1254 ------------- .../gesture_tutorial_motion_overview.xml | 1623 ----------------- .../res/layout/gesture_tutorial_fragment.xml | 16 +- .../AssistantGestureTutorialController.java | 1 - .../BackGestureTutorialController.java | 3 +- .../BackGestureTutorialFragment.java | 71 +- .../HomeGestureTutorialController.java | 2 +- .../HomeGestureTutorialFragment.java | 64 +- .../OverviewGestureTutorialController.java | 18 +- .../OverviewGestureTutorialFragment.java | 87 +- .../interaction/RootSandboxLayout.java | 11 + .../SwipeUpGestureTutorialController.java | 64 +- .../interaction/TutorialController.java | 251 ++- .../interaction/TutorialFragment.java | 158 +- 16 files changed, 534 insertions(+), 4328 deletions(-) create mode 100644 quickstep/res/drawable/gesture_tutorial_finger_dot.xml delete mode 100644 quickstep/res/drawable/gesture_tutorial_motion_back.xml delete mode 100644 quickstep/res/drawable/gesture_tutorial_motion_home.xml delete mode 100644 quickstep/res/drawable/gesture_tutorial_motion_overview.xml diff --git a/quickstep/res/drawable/gesture_tutorial_finger_dot.xml b/quickstep/res/drawable/gesture_tutorial_finger_dot.xml new file mode 100644 index 0000000000..5f8aafd30a --- /dev/null +++ b/quickstep/res/drawable/gesture_tutorial_finger_dot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/gesture_tutorial_motion_back.xml b/quickstep/res/drawable/gesture_tutorial_motion_back.xml deleted file mode 100644 index a6860fac64..0000000000 --- a/quickstep/res/drawable/gesture_tutorial_motion_back.xml +++ /dev/null @@ -1,1233 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/drawable/gesture_tutorial_motion_home.xml b/quickstep/res/drawable/gesture_tutorial_motion_home.xml deleted file mode 100644 index 020d0b1319..0000000000 --- a/quickstep/res/drawable/gesture_tutorial_motion_home.xml +++ /dev/null @@ -1,1254 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/drawable/gesture_tutorial_motion_overview.xml b/quickstep/res/drawable/gesture_tutorial_motion_overview.xml deleted file mode 100644 index 3f8b3f58b9..0000000000 --- a/quickstep/res/drawable/gesture_tutorial_motion_overview.xml +++ /dev/null @@ -1,1623 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml index 7466f319f4..cfb3eb07ec 100644 --- a/quickstep/res/layout/gesture_tutorial_fragment.xml +++ b/quickstep/res/layout/gesture_tutorial_fragment.xml @@ -62,7 +62,7 @@ android:background="@drawable/gesture_tutorial_ripple"/> animators = new ArrayList<>(); + + animators.add(fingerDotAppearanceAnimator); + animators.add(translationAnimator); + animators.add(controller.createFingerDotDisappearanceAnimatorSet()); + animators.add(animationPause); + + AnimatorSet finalAnimation = new AnimatorSet(); + finalAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + controller.updateFakeAppTaskViewLayout( + controller.getMockAppTaskCurrentPageLayoutResId()); + } + }); + finalAnimation.playSequentially(animators); + + return finalAnimation; } @Override @@ -49,6 +111,7 @@ public class BackGestureTutorialFragment extends TutorialFragment { @Override public boolean onTouch(View view, MotionEvent motionEvent) { + releaseFeedbackAnimation(); if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) { mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY()); } diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java index 10a0b2e87b..307a8fd230 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java @@ -80,7 +80,7 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll case HOME_NAVIGATION: switch (result) { case HOME_GESTURE_COMPLETED: { - mTutorialFragment.releaseGestureVideoView(); + mTutorialFragment.releaseFeedbackAnimation(); animateFakeTaskViewHome(finalVelocity, null); int subtitleResId = mTutorialFragment.isAtFinalStep() ? R.string.home_gesture_feedback_complete_without_follow_up diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java index 9206910bdb..dcae07d88d 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java @@ -15,23 +15,73 @@ */ package com.android.quickstep.interaction; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.view.MotionEvent; +import android.view.View; + import androidx.annotation.Nullable; import com.android.launcher3.R; import com.android.quickstep.interaction.TutorialController.TutorialType; +import java.util.ArrayList; + /** Shows the Home gesture interactive tutorial. */ public class HomeGestureTutorialFragment extends TutorialFragment { + @Nullable @Override - Integer getFeedbackVideoResId(boolean forDarkMode) { - return R.drawable.gesture_tutorial_motion_home; + Integer getEdgeAnimationResId() { + return R.drawable.gesture_tutorial_loop_home; } @Nullable @Override - Integer getGestureVideoResId() { - return R.drawable.gesture_tutorial_loop_home; + protected Animator createGestureAnimation() { + if (!(mTutorialController instanceof HomeGestureTutorialController)) { + return null; + } + float fingerDotStartTranslationY = (float) mRootView.getFullscreenHeight() / 2; + HomeGestureTutorialController controller = + (HomeGestureTutorialController) mTutorialController; + + AnimatorSet fingerDotAppearanceAnimator = controller.createFingerDotAppearanceAnimatorSet(); + fingerDotAppearanceAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mFingerDotView.setTranslationY(fingerDotStartTranslationY); + } + }); + + Animator animationPause = controller.createAnimationPause(); + animationPause.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + controller.resetFakeTaskView(); + } + }); + ArrayList animators = new ArrayList<>(); + + animators.add(fingerDotAppearanceAnimator); + animators.add(controller.createFingerDotHomeSwipeAnimator(fingerDotStartTranslationY)); + animators.add(controller.createFingerDotDisappearanceAnimatorSet()); + animators.add(animationPause); + + AnimatorSet finalAnimation = new AnimatorSet(); + finalAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + controller.resetFakeTaskView(); + } + }); + finalAnimation.playSequentially(animators); + + return finalAnimation; } @Override @@ -43,4 +93,10 @@ public class HomeGestureTutorialFragment extends TutorialFragment { Class getControllerClass() { return HomeGestureTutorialController.class; } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + releaseFeedbackAnimation(); + return super.onTouch(view, motionEvent); + } } diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java index 9e2c0babf4..b38a641b0e 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java @@ -98,13 +98,8 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge); break; case OVERVIEW_GESTURE_COMPLETED: - mTutorialFragment.releaseGestureVideoView(); - PendingAnimation anim = new PendingAnimation(300); - anim.setFloat(mTaskViewSwipeUpAnimation - .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL); - AnimatorSet animset = anim.buildAnim(); - animset.start(); - mRunningWindowAnim = SwipeUpAnimationLogic.RunningWindowAnim.wrap(animset); + mTutorialFragment.releaseFeedbackAnimation(); + animateTaskViewToOverview(); onMotionPaused(true /*arbitrary value*/); int subtitleResId = mTutorialFragment.getNumSteps() > 1 && mTutorialFragment.isAtFinalStep() @@ -126,4 +121,13 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont break; } } + + public void animateTaskViewToOverview() { + PendingAnimation anim = new PendingAnimation(300); + anim.setFloat(mTaskViewSwipeUpAnimation + .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL); + AnimatorSet animset = anim.buildAnim(); + animset.start(); + mRunningWindowAnim = SwipeUpAnimationLogic.RunningWindowAnim.wrap(animset); + } } diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java index 601358260f..968412bedc 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java @@ -15,23 +15,96 @@ */ package com.android.quickstep.interaction; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.view.MotionEvent; +import android.view.View; + import androidx.annotation.Nullable; import com.android.launcher3.R; import com.android.quickstep.interaction.TutorialController.TutorialType; +import java.util.ArrayList; + /** Shows the Overview gesture interactive tutorial. */ public class OverviewGestureTutorialFragment extends TutorialFragment { + @Nullable @Override - Integer getFeedbackVideoResId(boolean forDarkMode) { - return R.drawable.gesture_tutorial_motion_overview; + Integer getEdgeAnimationResId() { + return R.drawable.gesture_tutorial_loop_overview; } @Nullable @Override - Integer getGestureVideoResId() { - return R.drawable.gesture_tutorial_loop_overview; + protected Animator createGestureAnimation() { + if (!(mTutorialController instanceof OverviewGestureTutorialController)) { + return null; + } + float fingerDotStartTranslationY = (float) mRootView.getFullscreenHeight() / 2; + OverviewGestureTutorialController controller = + (OverviewGestureTutorialController) mTutorialController; + + AnimatorSet fingerDotAppearanceAnimator = controller.createFingerDotAppearanceAnimatorSet(); + fingerDotAppearanceAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + + mFingerDotView.setTranslationY(fingerDotStartTranslationY); + } + }); + + Animator swipeAnimator = + controller.createFingerDotOverviewSwipeAnimator(fingerDotStartTranslationY); + swipeAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mFakePreviousTaskView.setVisibility(View.VISIBLE); + controller.onMotionPaused(true /*arbitrary value*/); + } + }); + + AnimatorSet fingerDotDisappearanceAnimator = + controller.createFingerDotDisappearanceAnimatorSet(); + fingerDotDisappearanceAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + controller.animateTaskViewToOverview(); + } + }); + + Animator animationPause = controller.createAnimationPause(); + animationPause.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + controller.resetFakeTaskView(); + } + }); + ArrayList animators = new ArrayList<>(); + + animators.add(fingerDotAppearanceAnimator); + animators.add(swipeAnimator); + animators.add(controller.createAnimationPause()); + animators.add(fingerDotDisappearanceAnimator); + animators.add(animationPause); + + AnimatorSet finalAnimation = new AnimatorSet(); + finalAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + controller.resetFakeTaskView(); + } + }); + finalAnimation.playSequentially(animators); + + return finalAnimation; } @Override @@ -43,4 +116,10 @@ public class OverviewGestureTutorialFragment extends TutorialFragment { Class getControllerClass() { return OverviewGestureTutorialController.class; } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + releaseFeedbackAnimation(); + return super.onTouch(view, motionEvent); + } } diff --git a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java index db1afc2012..ac0c17d72f 100644 --- a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java +++ b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java @@ -16,8 +16,10 @@ package com.android.quickstep.interaction; import android.content.Context; +import android.graphics.Insets; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.WindowInsets; import android.widget.RelativeLayout; import androidx.fragment.app.FragmentManager; @@ -41,4 +43,13 @@ public class RootSandboxLayout extends RelativeLayout { return ((TutorialFragment) FragmentManager.findFragment(this)) .onInterceptTouch(motionEvent); } + + /** + * Returns this view's fullscreen height. This method is agnostic of this view's actual height. + */ + public int getFullscreenHeight() { + Insets insets = getRootWindowInsets().getInsets(WindowInsets.Type.systemBars()); + + return getHeight() + insets.top + insets.bottom; + } } diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java index 04b147cc1a..c2c8f61b19 100644 --- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java @@ -25,6 +25,7 @@ import static com.android.quickstep.interaction.TutorialController.TutorialType. import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Outline; @@ -32,7 +33,6 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; -import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.View; import android.view.ViewOutlineProvider; @@ -63,23 +63,24 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { private static final int FAKE_PREVIOUS_TASK_MARGIN = Utilities.dpToPx(12); + private static final long HOME_SWIPE_ANIMATION_DURATION_MILLIS = 625; + private static final long OVERVIEW_SWIPE_ANIMATION_DURATION_MILLIS = 1000; + final ViewSwipeUpAnimation mTaskViewSwipeUpAnimation; private float mFakeTaskViewRadius; - private Rect mFakeTaskViewRect = new Rect(); + private final Rect mFakeTaskViewRect = new Rect(); RunningWindowAnim mRunningWindowAnim; private boolean mShowTasks = false; private boolean mShowPreviousTasks = false; - private AnimatorListenerAdapter mResetTaskView = new AnimatorListenerAdapter() { + private final AnimatorListenerAdapter mResetTaskView = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mFakeHotseatView.setVisibility(View.INVISIBLE); mFakeIconView.setVisibility(View.INVISIBLE); if (mTutorialFragment.getActivity() != null) { - DisplayMetrics displayMetrics = - mTutorialFragment.getResources().getDisplayMetrics(); - int height = displayMetrics.heightPixels; - int width = displayMetrics.widthPixels; + int height = mTutorialFragment.getRootView().getFullscreenHeight(); + int width = mTutorialFragment.getRootView().getWidth(); mFakeTaskViewRect.set(0, 0, width, height); } mFakeTaskViewRadius = 0; @@ -108,9 +109,8 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { .copy(mContext); mTaskViewSwipeUpAnimation.initDp(dp); - DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); - int height = displayMetrics.heightPixels; - int width = displayMetrics.widthPixels; + int height = mTutorialFragment.getRootView().getFullscreenHeight(); + int width = mTutorialFragment.getRootView().getWidth(); mFakeTaskViewRect.set(0, 0, width, height); mFakeTaskViewRadius = 0; @@ -138,7 +138,6 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { /** Fades the task view, optionally after animating to a fake Overview. */ void fadeOutFakeTaskView(boolean toOverviewFirst, boolean reset, @Nullable Runnable onEndRunnable) { - hideFeedback(true); cancelRunningAnimation(); PendingAnimation anim = new PendingAnimation(300); if (toOverviewFirst) { @@ -184,6 +183,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { } void resetFakeTaskView() { + mFakeTaskView.setVisibility(View.VISIBLE); PendingAnimation anim = new PendingAnimation(300); anim.setFloat(mTaskViewSwipeUpAnimation .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); @@ -195,7 +195,6 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { } void animateFakeTaskViewHome(PointF finalVelocity, @Nullable Runnable onEndRunnable) { - hideFeedback(true); cancelRunningAnimation(); mFakePreviousTaskView.setVisibility(View.INVISIBLE); mFakeHotseatView.setVisibility(View.VISIBLE); @@ -218,9 +217,6 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { if (mGestureCompleted) { return; } - if (displacement != null) { - hideFeedback(true); - } if (mTutorialType == HOME_NAVIGATION_COMPLETE || mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) { mFakeTaskView.setVisibility(View.INVISIBLE); @@ -336,6 +332,44 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { } } + protected Animator createFingerDotHomeSwipeAnimator(float fingerDotStartTranslationY) { + Animator homeSwipeAnimator = createFingerDotSwipeUpAnimator(fingerDotStartTranslationY) + .setDuration(HOME_SWIPE_ANIMATION_DURATION_MILLIS); + + homeSwipeAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + animateFakeTaskViewHome( + new PointF( + 0f, + fingerDotStartTranslationY / HOME_SWIPE_ANIMATION_DURATION_MILLIS), + null); + } + }); + + return homeSwipeAnimator; + } + + protected Animator createFingerDotOverviewSwipeAnimator(float fingerDotStartTranslationY) { + return createFingerDotSwipeUpAnimator(fingerDotStartTranslationY) + .setDuration(OVERVIEW_SWIPE_ANIMATION_DURATION_MILLIS); + } + + + private Animator createFingerDotSwipeUpAnimator(float fingerDotStartTranslationY) { + ValueAnimator swipeAnimator = ValueAnimator.ofFloat(0f, 1f); + + swipeAnimator.addUpdateListener(valueAnimator -> { + float gestureProgress = + -fingerDotStartTranslationY * valueAnimator.getAnimatedFraction(); + setNavBarGestureProgress(gestureProgress); + mFingerDotView.setTranslationY(fingerDotStartTranslationY + gestureProgress); + }); + + return swipeAnimator; + } + private class FakeTransformParams extends TransformParams { @Override diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index e90ca50619..77bfc31c30 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -19,12 +19,15 @@ import static android.view.View.GONE; import static android.view.View.NO_ID; import static android.view.View.inflate; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.content.Context; import android.content.pm.PackageManager; -import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.util.Log; import android.view.View; @@ -52,18 +55,25 @@ import com.android.launcher3.views.ClipIconView; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback; +import java.util.ArrayList; + abstract class TutorialController implements BackGestureAttemptCallback, NavBarGestureAttemptCallback { private static final String TAG = "TutorialController"; + private static final float FINGER_DOT_VISIBLE_ALPHA = 0.6f; + private static final float FINGER_DOT_SMALL_SCALE = 0.7f; + private static final int FINGER_DOT_ANIMATION_DURATION_MILLIS = 500; + private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips"; private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips"; - private static final int FEEDBACK_ANIMATION_MS = 250; + private static final int FEEDBACK_ANIMATION_MS = 133; private static final int RIPPLE_VISIBLE_MS = 300; private static final int GESTURE_ANIMATION_DELAY_MS = 1500; private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 4000; + private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000; final TutorialFragment mTutorialFragment; TutorialType mTutorialType; @@ -72,8 +82,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, final TextView mCloseButton; final ViewGroup mFeedbackView; final TextView mFeedbackTitleView; - final ImageView mFeedbackVideoView; - final ImageView mGestureVideoView; + final ImageView mEdgeGestureVideoView; final RelativeLayout mFakeLauncherView; final ImageView mFakeHotseatView; final ClipIconView mFakeIconView; @@ -83,6 +92,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, final RippleDrawable mRippleDrawable; final Button mActionButton; final TutorialStepIndicator mTutorialStepView; + final ImageView mFingerDotView; private final AlertDialog mSkipTutorialDialog; protected boolean mGestureCompleted = false; @@ -91,7 +101,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, // views before posting new callbacks. private final Runnable mTitleViewCallback; @Nullable private Runnable mFeedbackViewCallback; - @Nullable private Runnable mFeedbackVideoViewCallback; + @Nullable private Runnable mFakeTaskViewCallback; + private final Runnable mShowFeedbackRunnable; TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { mTutorialFragment = tutorialFragment; @@ -104,8 +115,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view); mFeedbackTitleView = mFeedbackView.findViewById( R.id.gesture_tutorial_fragment_feedback_title); - mFeedbackVideoView = rootView.findViewById(R.id.gesture_tutorial_feedback_video); - mGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_gesture_video); + mEdgeGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_edge_gesture_video); mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view); mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view); mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view); @@ -117,10 +127,34 @@ abstract class TutorialController implements BackGestureAttemptCallback, mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button); mTutorialStepView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); + mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot); mSkipTutorialDialog = createSkipTutorialDialog(); mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent( AccessibilityEvent.TYPE_VIEW_FOCUSED); + mShowFeedbackRunnable = () -> { + mFeedbackView.setAlpha(0f); + mFeedbackView.setScaleX(0.95f); + mFeedbackView.setScaleY(0.95f); + mFeedbackView.setVisibility(View.VISIBLE); + mFeedbackView.animate() + .setDuration(FEEDBACK_ANIMATION_MS) + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .withEndAction(() -> { + if (mGestureCompleted && !mTutorialFragment.isAtFinalStep()) { + if (mFeedbackViewCallback != null) { + mFeedbackView.removeCallbacks(mFeedbackViewCallback); + } + mFeedbackViewCallback = mTutorialFragment::continueTutorial; + mFeedbackView.postDelayed(mFeedbackViewCallback, + ADVANCE_TUTORIAL_TIMEOUT_MS); + } + }) + .start(); + mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); + }; } private void showSkipTutorialDialog() { @@ -177,17 +211,10 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFeedbackView.setTranslationY(0); return; } - AnimatedVectorDrawable tutorialAnimation = mTutorialFragment.getTutorialAnimation(); - AnimatedVectorDrawable gestureAnimation = mTutorialFragment.getGestureAnimation(); - - if (tutorialAnimation != null && gestureAnimation != null) { - TextView title = mFeedbackView.findViewById( - R.id.gesture_tutorial_fragment_feedback_title); - - playFeedbackVideo(tutorialAnimation, gestureAnimation, () -> { - mFeedbackView.setTranslationY(0); - title.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - }, true); + Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); + AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); + if (gestureAnimation != null && edgeAnimation != null) { + playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true); } } @@ -219,8 +246,13 @@ abstract class TutorialController implements BackGestureAttemptCallback, int subtitleResId, boolean isGestureSuccessful, boolean useGestureAnimationDelay) { - mFeedbackTitleView.setText(titleResId); mFeedbackTitleView.removeCallbacks(mTitleViewCallback); + if (mFeedbackViewCallback != null) { + mFeedbackView.removeCallbacks(mFeedbackViewCallback); + mFeedbackViewCallback = null; + } + + mFeedbackTitleView.setText(titleResId); TextView subtitle = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_subtitle); subtitle.setText(subtitleResId); @@ -230,77 +262,68 @@ abstract class TutorialController implements BackGestureAttemptCallback, showActionButton(); } - if (mFeedbackVideoViewCallback != null) { - mFeedbackVideoView.removeCallbacks(mFeedbackVideoViewCallback); - mFeedbackVideoViewCallback = null; + if (mFakeTaskViewCallback != null) { + mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); + mFakeTaskViewCallback = null; } } mGestureCompleted = isGestureSuccessful; - AnimatedVectorDrawable tutorialAnimation = mTutorialFragment.getTutorialAnimation(); - AnimatedVectorDrawable gestureAnimation = mTutorialFragment.getGestureAnimation(); - if (tutorialAnimation != null && gestureAnimation != null) { - if (!isGestureSuccessful) { - playFeedbackVideo(tutorialAnimation, gestureAnimation, () -> { - mFeedbackView.setTranslationY( - -mFeedbackView.getHeight() - mFeedbackView.getTop()); - mFeedbackView.setVisibility(View.VISIBLE); - mFeedbackView.animate() - .setDuration(FEEDBACK_ANIMATION_MS) - .translationY(0) - .start(); - mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); - }, useGestureAnimationDelay); - return; - } else { - mTutorialFragment.releaseFeedbackVideoView(); - } + Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); + AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); + if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) { + playFeedbackAnimation( + gestureAnimation, + edgeAnimation, + mShowFeedbackRunnable, + useGestureAnimationDelay); + return; + } else { + mTutorialFragment.releaseFeedbackAnimation(); } - mFeedbackView.setTranslationY(-mFeedbackView.getHeight() - mFeedbackView.getTop()); - mFeedbackView.setVisibility(View.VISIBLE); - mFeedbackView.animate() - .setDuration(FEEDBACK_ANIMATION_MS) - .translationY(0) - .withEndAction(() -> { - if (isGestureSuccessful && !mTutorialFragment.isAtFinalStep()) { - if (mFeedbackViewCallback != null) { - mFeedbackView.removeCallbacks(mFeedbackViewCallback); - } - mFeedbackViewCallback = mTutorialFragment::continueTutorial; - mFeedbackView.postDelayed(mFeedbackViewCallback, - ADVANCE_TUTORIAL_TIMEOUT_MS); - } - }) - .start(); - mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); + mFeedbackViewCallback = mShowFeedbackRunnable; + + mFeedbackView.post(mFeedbackViewCallback); } - void hideFeedback(boolean releaseFeedbackVideo) { + void hideFeedback() { + cancelQueuedGestureAnimation(); mFeedbackView.clearAnimation(); mFeedbackView.setVisibility(View.INVISIBLE); - if (releaseFeedbackVideo) { - mTutorialFragment.releaseFeedbackVideoView(); - } } - private void playFeedbackVideo( - @NonNull AnimatedVectorDrawable tutorialAnimation, - @NonNull AnimatedVectorDrawable gestureAnimation, + void cancelQueuedGestureAnimation() { + if (mFeedbackViewCallback != null) { + mFeedbackView.removeCallbacks(mFeedbackViewCallback); + mFeedbackViewCallback = null; + } + if (mFakeTaskViewCallback != null) { + mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); + mFakeTaskViewCallback = null; + } + mFeedbackTitleView.removeCallbacks(mTitleViewCallback); + } + + private void playFeedbackAnimation( + @NonNull Animator gestureAnimation, + @NonNull AnimatedVectorDrawable edgeAnimation, @NonNull Runnable onStartRunnable, boolean useGestureAnimationDelay) { - if (tutorialAnimation.isRunning()) { - tutorialAnimation.reset(); + if (gestureAnimation.isRunning()) { + gestureAnimation.cancel(); } - tutorialAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() { - + if (edgeAnimation.isRunning()) { + edgeAnimation.reset(); + } + gestureAnimation.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationStart(Drawable drawable) { - super.onAnimationStart(drawable); + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); - mGestureVideoView.setVisibility(GONE); - if (gestureAnimation.isRunning()) { - gestureAnimation.stop(); + mEdgeGestureVideoView.setVisibility(GONE); + if (edgeAnimation.isRunning()) { + edgeAnimation.stop(); } if (!useGestureAnimationDelay) { @@ -309,37 +332,25 @@ abstract class TutorialController implements BackGestureAttemptCallback, } @Override - public void onAnimationEnd(Drawable drawable) { - super.onAnimationEnd(drawable); + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); - mGestureVideoView.setVisibility(View.VISIBLE); - gestureAnimation.start(); + mEdgeGestureVideoView.setVisibility(View.VISIBLE); + edgeAnimation.start(); - tutorialAnimation.unregisterAnimationCallback(this); + gestureAnimation.removeListener(this); } }); - if (mFeedbackViewCallback != null) { - mFeedbackVideoView.removeCallbacks(mFeedbackViewCallback); - mFeedbackViewCallback = null; - } - if (mFeedbackVideoViewCallback != null) { - mFeedbackVideoView.removeCallbacks(mFeedbackVideoViewCallback); - mFeedbackVideoViewCallback = null; - } + cancelQueuedGestureAnimation(); if (useGestureAnimationDelay) { mFeedbackViewCallback = onStartRunnable; - mFeedbackVideoViewCallback = () -> { - mFeedbackVideoView.setVisibility(View.VISIBLE); - tutorialAnimation.start(); - }; + mFakeTaskViewCallback = gestureAnimation::start; - mFeedbackVideoView.setVisibility(View.GONE); mFeedbackView.post(mFeedbackViewCallback); - mFeedbackVideoView.postDelayed(mFeedbackVideoViewCallback, GESTURE_ANIMATION_DELAY_MS); + mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS); } else { - mFeedbackVideoView.setVisibility(View.VISIBLE); - tutorialAnimation.start(); + gestureAnimation.start(); } } @@ -364,7 +375,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, @CallSuper void transitToController() { - hideFeedback(false); + hideFeedback(); hideActionButton(); updateSubtext(); updateDrawables(); @@ -419,7 +430,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, if (mContext != null) { mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable( mContext, getMockWallpaperResId())); - mTutorialFragment.updateFeedbackVideo(); + mTutorialFragment.updateFeedbackAnimation(); mFakeLauncherView.setBackgroundColor( mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); mFakeHotseatView.setImageDrawable(AppCompatResources.getDrawable( @@ -497,6 +508,52 @@ abstract class TutorialController implements BackGestureAttemptCallback, return null; } + protected AnimatorSet createFingerDotAppearanceAnimatorSet() { + ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( + mFingerDotView, View.ALPHA, 0f, FINGER_DOT_VISIBLE_ALPHA); + ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( + mFingerDotView, View.SCALE_Y, FINGER_DOT_SMALL_SCALE, 1f); + ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( + mFingerDotView, View.SCALE_X, FINGER_DOT_SMALL_SCALE, 1f); + ArrayList animators = new ArrayList<>(); + + animators.add(alphaAnimator); + animators.add(xScaleAnimator); + animators.add(yScaleAnimator); + + AnimatorSet appearanceAnimatorSet = new AnimatorSet(); + + appearanceAnimatorSet.playTogether(animators); + appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); + + return appearanceAnimatorSet; + } + + protected AnimatorSet createFingerDotDisappearanceAnimatorSet() { + ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( + mFingerDotView, View.ALPHA, FINGER_DOT_VISIBLE_ALPHA, 0f); + ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( + mFingerDotView, View.SCALE_Y, 1f, FINGER_DOT_SMALL_SCALE); + ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( + mFingerDotView, View.SCALE_X, 1f, FINGER_DOT_SMALL_SCALE); + ArrayList animators = new ArrayList<>(); + + animators.add(alphaAnimator); + animators.add(xScaleAnimator); + animators.add(yScaleAnimator); + + AnimatorSet appearanceAnimatorSet = new AnimatorSet(); + + appearanceAnimatorSet.playTogether(animators); + appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); + + return appearanceAnimatorSet; + } + + protected Animator createAnimationPause() { + return ValueAnimator.ofFloat(0f, 1f).setDuration(GESTURE_ANIMATION_PAUSE_DURATION_MILLIS); + } + /** Denotes the type of the tutorial. */ enum TutorialType { BACK_NAVIGATION, diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java index 7637450a1a..52ec9b39f0 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java @@ -15,6 +15,8 @@ */ package com.android.quickstep.interaction; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -29,6 +31,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.widget.ImageView; @@ -38,7 +41,6 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.quickstep.interaction.TutorialController.TutorialType; abstract class TutorialFragment extends Fragment implements OnTouchListener { @@ -49,13 +51,14 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener { TutorialType mTutorialType; @Nullable TutorialController mTutorialController = null; RootSandboxLayout mRootView; + View mFingerDotView; + View mFakePreviousTaskView; EdgeBackGestureHandler mEdgeBackGestureHandler; NavBarGestureHandler mNavBarGestureHandler; - private ImageView mFeedbackVideoView; - private ImageView mGestureVideoView; + private ImageView mEdgeGestureVideoView; - @Nullable private AnimatedVectorDrawable mTutorialAnimation = null; - @Nullable private AnimatedVectorDrawable mGestureAnimation = null; + @Nullable private Animator mGestureAnimation = null; + @Nullable private AnimatedVectorDrawable mEdgeAnimation = null; private boolean mIntroductionShown = false; private boolean mFragmentStopped = false; @@ -96,24 +99,26 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener { return null; } - @Nullable Integer getFeedbackVideoResId(boolean forDarkMode) { - return null; - } - - @Nullable Integer getGestureVideoResId() { + @Nullable Integer getEdgeAnimationResId() { return null; } @Nullable - AnimatedVectorDrawable getTutorialAnimation() { - return mTutorialAnimation; - } - - @Nullable - AnimatedVectorDrawable getGestureAnimation() { + Animator getGestureAnimation() { return mGestureAnimation; } + @Nullable + AnimatedVectorDrawable getEdgeAnimation() { + return mEdgeAnimation; + } + + + @Nullable + protected Animator createGestureAnimation() { + return null; + } + abstract TutorialController createController(TutorialType type); abstract Class getControllerClass(); @@ -147,21 +152,22 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener { return insets; }); mRootView.setOnTouchListener(this); - mFeedbackVideoView = mRootView.findViewById(R.id.gesture_tutorial_feedback_video); - mGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_gesture_video); + mEdgeGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_edge_gesture_video); + mFingerDotView = mRootView.findViewById(R.id.gesture_tutorial_finger_dot); + mFakePreviousTaskView = mRootView.findViewById( + R.id.gesture_tutorial_fake_previous_task_view); return mRootView; } @Override public void onStop() { super.onStop(); - releaseFeedbackVideoView(); - releaseGestureVideoView(); + releaseFeedbackAnimation(); mFragmentStopped = true; } void initializeFeedbackVideoView() { - if (!updateFeedbackVideo()) { + if (!updateFeedbackAnimation()) { return; } @@ -176,87 +182,90 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener { } } - boolean updateFeedbackVideo() { - if (getContext() == null) { + boolean updateFeedbackAnimation() { + if (!updateEdgeAnimation()) { return false; } - Integer feedbackVideoResId = getFeedbackVideoResId(Utilities.isDarkTheme(getContext())); - - if (feedbackVideoResId == null || !updateGestureVideo()) { - return false; - } - mTutorialAnimation = (AnimatedVectorDrawable) getContext().getDrawable(feedbackVideoResId); - - if (mTutorialAnimation != null) { - mTutorialAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() { - - @Override - public void onAnimationStart(Drawable drawable) { - super.onAnimationStart(drawable); - - mFeedbackVideoView.setVisibility(View.VISIBLE); - } - - @Override - public void onAnimationEnd(Drawable drawable) { - super.onAnimationEnd(drawable); - - releaseFeedbackVideoView(); - } - }); - } - mFeedbackVideoView.setImageDrawable(mTutorialAnimation); - - return true; - } - - boolean updateGestureVideo() { - Integer gestureVideoResId = getGestureVideoResId(); - if (gestureVideoResId == null || getContext() == null) { - return false; - } - mGestureAnimation = (AnimatedVectorDrawable) getContext().getDrawable(gestureVideoResId); + mGestureAnimation = createGestureAnimation(); if (mGestureAnimation != null) { - mGestureAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() { + mGestureAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mFingerDotView.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + mFingerDotView.setVisibility(View.GONE); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mFingerDotView.setVisibility(View.GONE); + } + }); + } + + return mGestureAnimation != null; + } + + boolean updateEdgeAnimation() { + Integer edgeAnimationResId = getEdgeAnimationResId(); + if (edgeAnimationResId == null || getContext() == null) { + return false; + } + mEdgeAnimation = (AnimatedVectorDrawable) getContext().getDrawable(edgeAnimationResId); + + if (mEdgeAnimation != null) { + mEdgeAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() { @Override public void onAnimationEnd(Drawable drawable) { super.onAnimationEnd(drawable); - mGestureAnimation.start(); + mEdgeAnimation.start(); } }); } - mGestureVideoView.setImageDrawable(mGestureAnimation); + mEdgeGestureVideoView.setImageDrawable(mEdgeAnimation); - return true; + return mEdgeAnimation != null; } - void releaseFeedbackVideoView() { - if (mTutorialAnimation != null && mTutorialAnimation.isRunning()) { - mTutorialAnimation.stop(); + void releaseFeedbackAnimation() { + if (mTutorialController != null) { + mTutorialController.cancelQueuedGestureAnimation(); } - - mFeedbackVideoView.setVisibility(View.GONE); - } - - void releaseGestureVideoView() { if (mGestureAnimation != null && mGestureAnimation.isRunning()) { - mGestureAnimation.stop(); + mGestureAnimation.cancel(); + } + if (mEdgeAnimation != null && mEdgeAnimation.isRunning()) { + mEdgeAnimation.stop(); } - mGestureVideoView.setVisibility(View.GONE); + mEdgeGestureVideoView.setVisibility(View.GONE); } @Override public void onResume() { super.onResume(); + releaseFeedbackAnimation(); if (mFragmentStopped && mTutorialController != null) { mTutorialController.showFeedback(); mFragmentStopped = false; } else { - changeController(mTutorialType); + mRootView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + changeController(mTutorialType); + mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); } } @@ -292,6 +301,7 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener { mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController); mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController); mTutorialType = tutorialType; + initializeFeedbackVideoView(); } From a8382dc0528588041bb43b964035a95675e6d64c Mon Sep 17 00:00:00 2001 From: Lucas Dupin Date: Mon, 9 Aug 2021 16:11:14 -0700 Subject: [PATCH 03/11] Act on scrim visibility The scrim visibility drives whether the launcher window is opaque or not. We should track it and apply the flag instead of trying to catch it through other Launcher life cycles. Fixes: 195365607 Bug: 196403152 Test: tap on home button while launching app from overview Change-Id: I2a00b86b602b5dd12c901433b92adcf0170be15e (cherry picked from commit e4a9469156c16b19edf156bd7770b81d07674f12) --- .../statehandlers/DepthController.java | 20 ++++++++--- .../android/launcher3/views/ScrimView.java | 36 ++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java index 370fb8ef7c..000d69d400 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java @@ -108,6 +108,13 @@ public class DepthController implements StateHandler, } }; + private final Runnable mOpaquenessListener = new Runnable() { + @Override + public void run() { + dispatchTransactionSurface(mDepth); + } + }; + private final Launcher mLauncher; /** * Blur radius when completely zoomed out, in pixels. @@ -150,23 +157,28 @@ public class DepthController implements StateHandler, if (windowToken != null) { mWallpaperManager.setWallpaperZoomOut(windowToken, mDepth); } - CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), - mCrossWindowBlurListener); + onAttached(); } @Override public void onViewDetachedFromWindow(View view) { CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener); + mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener); } }; mLauncher.getRootView().addOnAttachStateChangeListener(mOnAttachListener); if (mLauncher.getRootView().isAttachedToWindow()) { - CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), - mCrossWindowBlurListener); + onAttached(); } } } + private void onAttached() { + CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), + mCrossWindowBlurListener); + mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); + } + /** * Sets if the underlying activity is started or not */ diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java index 1eb79ad550..4c0bfde682 100644 --- a/src/com/android/launcher3/views/ScrimView.java +++ b/src/com/android/launcher3/views/ScrimView.java @@ -25,23 +25,27 @@ import android.graphics.drawable.ColorDrawable; import android.util.AttributeSet; import android.view.View; +import androidx.annotation.NonNull; import androidx.core.graphics.ColorUtils; import com.android.launcher3.BaseActivity; import com.android.launcher3.Insettable; import com.android.launcher3.util.SystemUiController; +import java.util.ArrayList; + /** * Simple scrim which draws a flat color */ public class ScrimView extends View implements Insettable { private static final float STATUS_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.9f; + private final ArrayList mOpaquenessListeners = new ArrayList<>(1); private SystemUiController mSystemUiController; - private ScrimDrawingController mDrawingController; private int mBackgroundColor; private boolean mIsVisible = true; + private boolean mLastDispatchedOpaqueness; public ScrimView(Context context, AttributeSet attrs) { super(context, attrs); @@ -60,6 +64,7 @@ public class ScrimView extends View implements Insettable { @Override protected boolean onSetAlpha(int alpha) { updateSysUiColors(); + dispatchVisibilityListenersIfNeeded(); return super.onSetAlpha(alpha); } @@ -67,6 +72,7 @@ public class ScrimView extends View implements Insettable { public void setBackgroundColor(int color) { mBackgroundColor = color; updateSysUiColors(); + dispatchVisibilityListenersIfNeeded(); super.setBackgroundColor(color); } @@ -74,6 +80,7 @@ public class ScrimView extends View implements Insettable { public void onVisibilityAggregated(boolean isVisible) { super.onVisibilityAggregated(isVisible); mIsVisible = isVisible; + dispatchVisibilityListenersIfNeeded(); } public boolean isFullyOpaque() { @@ -108,6 +115,17 @@ public class ScrimView extends View implements Insettable { } } + private void dispatchVisibilityListenersIfNeeded() { + boolean fullyOpaque = isFullyOpaque(); + if (mLastDispatchedOpaqueness == fullyOpaque) { + return; + } + mLastDispatchedOpaqueness = fullyOpaque; + for (int i = 0; i < mOpaquenessListeners.size(); i++) { + mOpaquenessListeners.get(i).run(); + } + } + private SystemUiController getSystemUiController() { if (mSystemUiController == null) { mSystemUiController = BaseActivity.fromContext(getContext()).getSystemUiController(); @@ -135,6 +153,22 @@ public class ScrimView extends View implements Insettable { } } + /** + * Registers a listener to be notified of whether the scrim is occluding other UI elements. + * @see #isFullyOpaque() + */ + public void addOpaquenessListener(@NonNull Runnable listener) { + mOpaquenessListeners.add(listener); + } + + /** + * Removes previously registered listener. + * @see #addOpaquenessListener(Runnable) + */ + public void removeOpaquenessListener(@NonNull Runnable listener) { + mOpaquenessListeners.remove(listener); + } + /** * A Utility interface allowing for other surfaces to draw on ScrimView */ From 455ba62b5db7a8415e0f250d62c30e9c24ebbb6b Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 29 Jul 2021 15:48:24 -0700 Subject: [PATCH 04/11] Migrating all model tests to Instrumentation tests Bug: 196825541 Test: Presubmit Change-Id: Iebd46eb41eb46c187d569197f4b97b4fddc0f6f7 --- .../WidgetsPredicationUpdateTaskTest.java | 128 ++++---- .../secondarydisplay/SDWorkModeTest.java | 125 -------- .../launcher3/testing/TestActivity.java | 46 --- .../launcher3/util/LauncherUIHelper.java | 101 ------ .../android/launcher3/LauncherAppState.java | 5 +- src/com/android/launcher3/LauncherModel.java | 7 +- .../graphics/LauncherPreviewRenderer.java | 50 +-- .../android/launcher3/icons/IconCache.java | 3 + .../launcher3/util/DisplayController.java | 17 +- .../util/MainThreadInitializedObject.java | 87 ++++- .../android/launcher3/util/SettingsCache.java | 8 +- .../launcher3/util/UiThreadHelper.java | 4 +- .../widget/custom/CustomWidgetManager.java | 6 +- .../res/raw}/cache_data_updated_task_data.txt | 0 .../res/raw}/db_schema_v10.json | 0 ...package_install_state_change_task_data.txt | 0 .../widgets_predication_update_task_data.txt | 0 .../model/AddWorkspaceItemsTaskTest.java | 24 +- .../launcher3/model/BackupRestoreTest.java | 111 ++++--- .../model/CacheDataUpdatedTaskTest.java | 20 +- .../model/DbDowngradeHelperTest.java | 20 +- .../model/DefaultLayoutProviderTest.java | 45 ++- .../model/GridSizeMigrationTaskV2Test.java | 69 ++-- .../launcher3/model/LoaderCursorTest.java | 32 +- .../model/ModelMultiCallbacksTest.java | 119 ++++--- .../PackageInstallStateChangedTaskTest.java | 20 +- .../launcher3/provider/RestoreDbTaskTest.java | 11 +- .../secondarydisplay/SDLauncherTest.java | 53 ++++ .../launcher3/util/LauncherLayoutBuilder.java | 0 .../launcher3/util/LauncherModelHelper.java | 299 +++++++++++++----- .../launcher3/util/ReflectionHelpers.java | 58 ++++ 31 files changed, 784 insertions(+), 684 deletions(-) rename quickstep/{robolectric_tests => tests}/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java (65%) delete mode 100644 robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java delete mode 100644 robolectric_tests/src/com/android/launcher3/testing/TestActivity.java delete mode 100644 robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java rename {robolectric_tests/resources => tests/res/raw}/cache_data_updated_task_data.txt (100%) rename {robolectric_tests/resources => tests/res/raw}/db_schema_v10.json (100%) rename {robolectric_tests/resources => tests/res/raw}/package_install_state_change_task_data.txt (100%) rename {robolectric_tests/resources => tests/res/raw}/widgets_predication_update_task_data.txt (100%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java (93%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/BackupRestoreTest.java (62%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java (92%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/DbDowngradeHelperTest.java (91%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/DefaultLayoutProviderTest.java (77%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java (89%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/LoaderCursorTest.java (93%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/ModelMultiCallbacksTest.java (65%) rename {robolectric_tests => tests}/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java (87%) rename {robolectric_tests => tests}/src/com/android/launcher3/provider/RestoreDbTaskTest.java (93%) create mode 100644 tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java rename {robolectric_tests => tests}/src/com/android/launcher3/util/LauncherLayoutBuilder.java (100%) rename {robolectric_tests => tests}/src/com/android/launcher3/util/LauncherModelHelper.java (60%) create mode 100644 tests/src/com/android/launcher3/util/ReflectionHelpers.java diff --git a/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java similarity index 65% rename from quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java rename to quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java index f82fbcc15b..c1b3beb475 100644 --- a/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -15,24 +15,31 @@ */ package com.android.launcher3.model; +import static android.os.Process.myUserHandle; + import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; -import static org.robolectric.Shadows.shadowOf; +import static org.mockito.Mockito.doReturn; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetId; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; -import android.content.Context; -import android.os.Process; import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; import com.android.launcher3.LauncherAppState; import com.android.launcher3.config.FeatureFlags; @@ -40,38 +47,34 @@ import com.android.launcher3.icons.ComponentWithLabel; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; -import com.android.launcher3.shadows.ShadowDeviceFlag; import com.android.launcher3.util.LauncherModelHelper; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.PendingAddWidgetInfo; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowAppWidgetManager; -import org.robolectric.shadows.ShadowPackageManager; -import org.robolectric.util.ReflectionHelpers; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; -@RunWith(RobolectricTestRunner.class) +@SmallTest +@RunWith(AndroidJUnit4.class) public final class WidgetsPredicationUpdateTaskTest { - private AppWidgetProviderInfo mApp1Provider1 = new AppWidgetProviderInfo(); - private AppWidgetProviderInfo mApp1Provider2 = new AppWidgetProviderInfo(); - private AppWidgetProviderInfo mApp2Provider1 = new AppWidgetProviderInfo(); - private AppWidgetProviderInfo mApp4Provider1 = new AppWidgetProviderInfo(); - private AppWidgetProviderInfo mApp4Provider2 = new AppWidgetProviderInfo(); - private AppWidgetProviderInfo mApp5Provider1 = new AppWidgetProviderInfo(); + private AppWidgetProviderInfo mApp1Provider1; + private AppWidgetProviderInfo mApp1Provider2; + private AppWidgetProviderInfo mApp2Provider1; + private AppWidgetProviderInfo mApp4Provider1; + private AppWidgetProviderInfo mApp4Provider2; + private AppWidgetProviderInfo mApp5Provider1; + private List allWidgets; private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback(); - private Context mContext; private LauncherModelHelper mModelHelper; private UserHandle mUserHandle; @@ -80,54 +83,56 @@ public final class WidgetsPredicationUpdateTaskTest { @Before public void setup() throws Exception { + mModelHelper = new LauncherModelHelper(); MockitoAnnotations.initMocks(this); doAnswer(invocation -> { ComponentWithLabel componentWithLabel = invocation.getArgument(0); return componentWithLabel.getComponent().getShortClassName(); }).when(mIconCache).getTitleNoCache(any()); - mContext = RuntimeEnvironment.application; - mModelHelper = new LauncherModelHelper(); - mUserHandle = Process.myUserHandle(); + mUserHandle = myUserHandle(); + mApp1Provider1 = createAppWidgetProviderInfo( + ComponentName.createRelative("app1", "provider1")); + mApp1Provider2 = createAppWidgetProviderInfo( + ComponentName.createRelative("app1", "provider2")); + mApp2Provider1 = createAppWidgetProviderInfo( + ComponentName.createRelative("app2", "provider1")); + mApp4Provider1 = createAppWidgetProviderInfo( + ComponentName.createRelative("app4", "provider1")); + mApp4Provider2 = createAppWidgetProviderInfo( + ComponentName.createRelative("app4", ".provider2")); + mApp5Provider1 = createAppWidgetProviderInfo( + ComponentName.createRelative("app5", "provider1")); + allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, + mApp4Provider1, mApp4Provider2, mApp5Provider1); + + AppWidgetManager manager = mModelHelper.sandboxContext.spyService(AppWidgetManager.class); + doReturn(allWidgets).when(manager).getInstalledProviders(); + doReturn(allWidgets).when(manager).getInstalledProvidersForProfile(eq(myUserHandle())); + doAnswer(i -> { + String pkg = i.getArgument(0); + Log.e("Hello", "Getting v " + pkg); + return TextUtils.isEmpty(pkg) ? allWidgets : allWidgets.stream() + .filter(a -> pkg.equals(a.provider.getPackageName())) + .collect(Collectors.toList()); + }).when(manager).getInstalledProvidersForPackage(any(), eq(myUserHandle())); + // 2 widgets, app4/provider1 & app5/provider1, have already been added to the workspace. - mModelHelper.initializeData("/widgets_predication_update_task_data.txt"); - - ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager()); - mApp1Provider1.provider = ComponentName.createRelative("app1", "provider1"); - ReflectionHelpers.setField(mApp1Provider1, "providerInfo", - packageManager.addReceiverIfNotPresent(mApp1Provider1.provider)); - mApp1Provider2.provider = ComponentName.createRelative("app1", "provider2"); - ReflectionHelpers.setField(mApp1Provider2, "providerInfo", - packageManager.addReceiverIfNotPresent(mApp1Provider2.provider)); - mApp2Provider1.provider = ComponentName.createRelative("app2", "provider1"); - ReflectionHelpers.setField(mApp2Provider1, "providerInfo", - packageManager.addReceiverIfNotPresent(mApp2Provider1.provider)); - mApp4Provider1.provider = ComponentName.createRelative("app4", "provider1"); - ReflectionHelpers.setField(mApp4Provider1, "providerInfo", - packageManager.addReceiverIfNotPresent(mApp4Provider1.provider)); - mApp4Provider2.provider = ComponentName.createRelative("app4", ".provider2"); - ReflectionHelpers.setField(mApp4Provider2, "providerInfo", - packageManager.addReceiverIfNotPresent(mApp4Provider2.provider)); - mApp5Provider1.provider = ComponentName.createRelative("app5", "provider1"); - ReflectionHelpers.setField(mApp5Provider1, "providerInfo", - packageManager.addReceiverIfNotPresent(mApp5Provider1.provider)); - - ShadowAppWidgetManager shadowAppWidgetManager = - shadowOf(mContext.getSystemService(AppWidgetManager.class)); - shadowAppWidgetManager.addInstalledProvider(mApp1Provider1); - shadowAppWidgetManager.addInstalledProvider(mApp1Provider2); - shadowAppWidgetManager.addInstalledProvider(mApp2Provider1); - shadowAppWidgetManager.addInstalledProvider(mApp4Provider1); - shadowAppWidgetManager.addInstalledProvider(mApp4Provider2); - shadowAppWidgetManager.addInstalledProvider(mApp5Provider1); - - mModelHelper.getModel().addCallbacks(mCallback); + mModelHelper.initializeData("widgets_predication_update_task_data"); + MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(mCallback)).get(); MODEL_EXECUTOR.post(() -> mModelHelper.getBgDataModel().widgetsModel.update( - LauncherAppState.getInstance(mContext), /* packageUser= */ null)); - waitUntilIdle(); + LauncherAppState.getInstance(mModelHelper.sandboxContext), + /* packageUser= */ null)); + + MODEL_EXECUTOR.submit(() -> { }).get(); + MAIN_EXECUTOR.submit(() -> { }).get(); } + @After + public void tearDown() { + mModelHelper.destroy(); + } @Test public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() @@ -165,9 +170,9 @@ public final class WidgetsPredicationUpdateTaskTest { @Test public void widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder() throws Exception { - ShadowDeviceFlag shadowDeviceFlag = Shadow.extract( - FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER); - shadowDeviceFlag.setValue(false); + if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) { + return; + } // WHEN newPredicationTask is executed with 5 predicated widgets. AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", @@ -203,13 +208,8 @@ public final class WidgetsPredicationUpdateTaskTest { assertThat(actual.getUser()).isEqualTo(expected.getProfile()); } - private void waitUntilIdle() { - shadowOf(MODEL_EXECUTOR.getLooper()).idle(); - shadowOf(MAIN_EXECUTOR.getLooper()).idle(); - } - private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List appTargets) { - return new WidgetsPredictionUpdateTask( + return new WidgetsPredictionUpdateTask( new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"), appTargets); } diff --git a/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java deleted file mode 100644 index e3694aeddc..0000000000 --- a/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.secondarydisplay; - -import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; -import static com.android.launcher3.util.LauncherUIHelper.doLayout; -import static com.android.launcher3.util.Preconditions.assertNotNull; - -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertTrue; - -import android.content.Context; -import android.os.UserManager; -import android.provider.Settings; - -import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.allapps.AllAppsPagedView; -import com.android.launcher3.allapps.AllAppsRecyclerView; -import com.android.launcher3.util.LauncherLayoutBuilder; -import com.android.launcher3.util.LauncherModelHelper; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.LooperMode; -import org.robolectric.annotation.LooperMode.Mode; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowUserManager; - -/** - * Tests for {@link SecondaryDisplayLauncher} with work profile - */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(Mode.PAUSED) -public class SDWorkModeTest { - - private static final int SYSTEM_USER = 0; - private static final int FLAG_SYSTEM = 0x00000800; - private static final int WORK_PROFILE_ID = 10; - private static final int FLAG_PROFILE = 0x00001000; - - private Context mTargetContext; - private InvariantDeviceProfile mIdp; - private LauncherModelHelper mModelHelper; - - @Before - public void setup() throws Exception { - mModelHelper = new LauncherModelHelper(); - mTargetContext = RuntimeEnvironment.application; - mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext); - Settings.Global.putFloat(mTargetContext.getContentResolver(), - Settings.Global.WINDOW_ANIMATION_SCALE, 0); - - mModelHelper.installApp(TEST_PACKAGE); - } - - @Test - public void testAllAppsList_noWorkProfile() throws Exception { - SecondaryDisplayLauncher launcher = loadLauncher(); - launcher.showAppDrawer(true); - doLayout(launcher); - - verifyRecyclerViewCount(launcher.getAppsView().getActiveRecyclerView()); - } - - @Test - public void testAllAppsList_workProfile() throws Exception { - ShadowUserManager sum = Shadow.extract(mTargetContext.getSystemService(UserManager.class)); - sum.addUser(SYSTEM_USER, "me", FLAG_SYSTEM); - sum.addProfile(SYSTEM_USER, WORK_PROFILE_ID, "work", FLAG_PROFILE); - - SecondaryDisplayLauncher launcher = loadLauncher(); - launcher.showAppDrawer(true); - doLayout(launcher); - - AllAppsRecyclerView rv1 = launcher.getAppsView().getActiveRecyclerView(); - verifyRecyclerViewCount(rv1); - - assertNotNull(launcher.getAppsView().getWorkModeSwitch()); - assertTrue(launcher.getAppsView().getRecyclerViewContainer() instanceof AllAppsPagedView); - - AllAppsPagedView pagedView = - (AllAppsPagedView) launcher.getAppsView().getRecyclerViewContainer(); - pagedView.snapToPageImmediately(1); - doLayout(launcher); - - AllAppsRecyclerView rv2 = launcher.getAppsView().getActiveRecyclerView(); - verifyRecyclerViewCount(rv2); - assertNotSame(rv1, rv2); - } - - private SecondaryDisplayLauncher loadLauncher() throws Exception { - // Install 100 apps - for (int i = 0; i < 100; i++) { - mModelHelper.installApp(TEST_PACKAGE + i); - } - mModelHelper.setupDefaultLayoutProvider(new LauncherLayoutBuilder()).loadModelSync(); - SecondaryDisplayLauncher launcher = - Robolectric.buildActivity(SecondaryDisplayLauncher.class).setup().get(); - doLayout(launcher); - return launcher; - } - - private void verifyRecyclerViewCount(AllAppsRecyclerView rv) { - int childCount = rv.getChildCount(); - assertTrue(childCount > 0); - assertTrue(childCount < 100); - } -} diff --git a/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java deleted file mode 100644 index 17d0ac1a9f..0000000000 --- a/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2021 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.launcher3.testing; - -import com.android.launcher3.BaseActivity; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.views.BaseDragLayer; - -/** An empty activity for {@link android.app.Fragment}s, {@link android.view.View}s testing. */ -public class TestActivity extends BaseActivity implements ActivityContext { - - private DeviceProfile mDeviceProfile; - - @Override - public BaseDragLayer getDragLayer() { - return new BaseDragLayer(this, /* attrs= */ null, /* alphaChannelCount= */ 1) { - @Override - public void recreateControllers() { - // Do nothing. - } - }; - } - - @Override - public DeviceProfile getDeviceProfile() { - return mDeviceProfile; - } - - public void setDeviceProfile(DeviceProfile deviceProfile) { - mDeviceProfile = deviceProfile; - } -} diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java deleted file mode 100644 index caad40e260..0000000000 --- a/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.util; - -import static android.view.View.MeasureSpec.EXACTLY; -import static android.view.View.MeasureSpec.makeMeasureSpec; - -import static com.android.launcher3.Utilities.createHomeIntent; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.graphics.Point; -import android.view.View; -import android.view.WindowManager; - -import com.android.launcher3.Launcher; - -import org.robolectric.Robolectric; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.shadows.ShadowLooper; -import org.robolectric.util.ReflectionHelpers; - -import java.util.List; - -/** - * Utility class to help manage Launcher UI and related objects for test. - */ -public class LauncherUIHelper { - - /** - * Returns the class name for the Launcher activity as defined in the manifest - */ - public static String getLauncherClassName() { - Context context = RuntimeEnvironment.application; - Intent homeIntent = createHomeIntent().setPackage(context.getPackageName()); - - List launchers = context.getPackageManager() - .queryIntentActivities(homeIntent, 0); - if (launchers.size() != 1) { - return null; - } - return launchers.get(0).activityInfo.name; - } - - /** - * Returns an activity controller for Launcher activity defined in the manifest - */ - public static ActivityController buildLauncher() { - try { - Class tClass = (Class) Class.forName(getLauncherClassName()); - return Robolectric.buildActivity(tClass); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Creates and binds a Launcher activity defined in the manifest. - * Note that the model must be bound before calling this - */ - public static T buildAndBindLauncher() { - ActivityController controller = buildLauncher(); - - T launcher = controller.setup().get(); - doLayout(launcher); - ViewOnDrawExecutor executor = ReflectionHelpers.getField(launcher, "mPendingExecutor"); - if (executor != null) { - executor.markCompleted(); - } - return launcher; - } - - /** - * Performs a measure and layout pass for the given activity - */ - public static void doLayout(Activity activity) { - Point size = new Point(); - RuntimeEnvironment.application.getSystemService(WindowManager.class) - .getDefaultDisplay().getSize(size); - View view = activity.getWindow().getDecorView(); - view.measure(makeMeasureSpec(size.x, EXACTLY), makeMeasureSpec(size.y, EXACTLY)); - view.layout(0, 0, size.x, size.y); - ShadowLooper.idleMainLooper(); - } -} diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 702b73afba..50808244ad 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -50,7 +50,7 @@ import com.android.launcher3.util.SimpleBroadcastReceiver; import com.android.launcher3.util.Themes; import com.android.launcher3.widget.custom.CustomWidgetManager; -public class LauncherAppState { +public class LauncherAppState implements SafeCloseable { public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher"; private static final String KEY_ICON_STATE = "pref_icon_shape_path"; @@ -158,7 +158,8 @@ public class LauncherAppState { /** * Call from Application.onTerminate(), which is not guaranteed to ever be called. */ - public void onTerminate() { + @Override + public void close() { mModel.destroy(); mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel); CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null); diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 7b6a5bfeb8..9ebec0a2ea 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -96,9 +96,10 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi // our monitoring of the package manager provides all updates and we never // need to do a requery. This is only ever touched from the loader thread. private boolean mModelLoaded; + private boolean mModelDestroyed = false; public boolean isModelLoaded() { synchronized (mLock) { - return mModelLoaded && mLoaderTask == null; + return mModelLoaded && mLoaderTask == null && !mModelDestroyed; } } @@ -245,6 +246,7 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi * Called when the model is destroyed */ public void destroy() { + mModelDestroyed = true; MODEL_EXECUTOR.execute(mModelDelegate::destroy); } @@ -557,6 +559,9 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi } public void enqueueModelUpdateTask(ModelUpdateTask task) { + if (mModelDestroyed) { + return; + } task.init(mApp, this, mBgDataModel, mBgAllAppsList, MAIN_EXECUTOR); MODEL_EXECUTOR.execute(task); } diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index a96de31241..1c8954defa 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -86,7 +86,7 @@ import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; -import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.widget.BaseLauncherAppWidgetHostView; @@ -97,13 +97,10 @@ import com.android.launcher3.widget.NavigableAppWidgetHostView; import com.android.launcher3.widget.custom.CustomWidgetManager; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; /** @@ -122,22 +119,16 @@ public class LauncherPreviewRenderer extends ContextWrapper * Context used just for preview. It also provides a few objects (e.g. UserCache) just for * preview purposes. */ - public static class PreviewContext extends ContextWrapper { - - private final Set mAllowedObjects = new HashSet<>( - Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE, - LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, - CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE)); + public static class PreviewContext extends SandboxContext { private final InvariantDeviceProfile mIdp; - private final Map mObjectMap = new HashMap<>(); private final ConcurrentLinkedQueue mIconPool = new ConcurrentLinkedQueue<>(); - private boolean mDestroyed = false; - public PreviewContext(Context base, InvariantDeviceProfile idp) { - super(base); + super(base, UserCache.INSTANCE, InstallSessionHelper.INSTANCE, + LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, + CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE); mIdp = idp; mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp); mObjectMap.put(LauncherAppState.INSTANCE, @@ -145,37 +136,6 @@ public class LauncherPreviewRenderer extends ContextWrapper } - @Override - public Context getApplicationContext() { - return this; - } - - public void onDestroy() { - CustomWidgetManager.INSTANCE.get(this).onDestroy(); - LauncherAppState.INSTANCE.get(this).onTerminate(); - mDestroyed = true; - } - - /** - * Find a cached object from mObjectMap if we have already created one. If not, generate - * an object using the provider. - */ - public T getObject(MainThreadInitializedObject mainThreadInitializedObject, - MainThreadInitializedObject.ObjectProvider provider) { - if (FeatureFlags.IS_STUDIO_BUILD && mDestroyed) { - throw new RuntimeException("Context already destroyed"); - } - if (!mAllowedObjects.contains(mainThreadInitializedObject)) { - throw new IllegalStateException("Leaking unknown objects"); - } - if (mObjectMap.containsKey(mainThreadInitializedObject)) { - return (T) mObjectMap.get(mainThreadInitializedObject); - } - T t = provider.get(this); - mObjectMap.put(mainThreadInitializedObject, t); - return t; - } - public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) { LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll(); if (launcherIconsForPreview != null) { diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index cd13cd0ec5..1a468aeb88 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -134,6 +134,9 @@ public class IconCache extends BaseIconCache { * Closes the cache DB. This will clear any in-memory cache. */ public void close() { + // This will clear all pending updates + getUpdateHandler(); + mIconDb.close(); } diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java index e2c0a32bb0..94f29dbbf7 100644 --- a/src/com/android/launcher3/util/DisplayController.java +++ b/src/com/android/launcher3/util/DisplayController.java @@ -55,7 +55,7 @@ import java.util.Set; * Utility class to cache properties of default display to avoid a system RPC on every call. */ @SuppressLint("NewApi") -public class DisplayController implements DisplayListener, ComponentCallbacks { +public class DisplayController implements DisplayListener, ComponentCallbacks, SafeCloseable { private static final String TAG = "DisplayController"; @@ -79,6 +79,7 @@ public class DisplayController implements DisplayListener, ComponentCallbacks { private final ArrayList mListeners = new ArrayList<>(); private Info mInfo; + private boolean mDestroyed = false; private DisplayController(Context context) { mContext = context; @@ -110,6 +111,17 @@ public class DisplayController implements DisplayListener, ComponentCallbacks { mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler()); } + @Override + public void close() { + mDestroyed = true; + if (mWindowContext != null) { + mWindowContext.unregisterComponentCallbacks(this); + } else { + // TODO: unregister broadcast receiver + } + mDM.unregisterDisplayListener(this); + } + @Override public final void onDisplayAdded(int displayId) { } @@ -157,6 +169,9 @@ public class DisplayController implements DisplayListener, ComponentCallbacks { * Only used for pre-S */ private void onConfigChanged(Intent intent) { + if (mDestroyed) { + return; + } Configuration config = mContext.getResources().getConfiguration(); if (mInfo.fontScale != config.fontScale || mInfo.densityDpi != config.densityDpi) { Log.d(TAG, "Configuration changed, notifying listeners"); diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java index f6003dd7bf..ef160b199a 100644 --- a/src/com/android/launcher3/util/MainThreadInitializedObject.java +++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java @@ -18,13 +18,20 @@ package com.android.launcher3.util; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.content.Context; +import android.content.ContextWrapper; import android.os.Looper; +import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; -import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext; import com.android.launcher3.util.ResourceBasedOverride.Overrides; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; /** @@ -40,8 +47,8 @@ public class MainThreadInitializedObject { } public T get(Context context) { - if (context instanceof PreviewContext) { - return ((PreviewContext) context).getObject(this, mProvider); + if (context instanceof SandboxContext) { + return ((SandboxContext) context).getObject(this, mProvider); } if (mValue == null) { @@ -80,4 +87,78 @@ public class MainThreadInitializedObject { T get(Context context); } + + /** + * Abstract Context which allows custom implementations for + * {@link MainThreadInitializedObject} providers + */ + public static abstract class SandboxContext extends ContextWrapper { + + protected final Set mAllowedObjects; + protected final Map mObjectMap = new HashMap<>(); + protected final ArrayList mOrderedObjects = new ArrayList<>(); + + private final Object mDestroyLock = new Object(); + private boolean mDestroyed = false; + + public SandboxContext(Context base, MainThreadInitializedObject... allowedObjects) { + super(base); + mAllowedObjects = new HashSet<>(Arrays.asList(allowedObjects)); + } + + @Override + public Context getApplicationContext() { + return this; + } + + public void onDestroy() { + synchronized (mDestroyLock) { + // Destroy in reverse order + for (int i = mOrderedObjects.size() - 1; i >= 0; i--) { + Object o = mOrderedObjects.get(i); + if (o instanceof SafeCloseable) { + ((SafeCloseable) o).close(); + } + } + mDestroyed = true; + } + } + + /** + * Find a cached object from mObjectMap if we have already created one. If not, generate + * an object using the provider. + */ + private T getObject(MainThreadInitializedObject object, ObjectProvider provider) { + synchronized (mDestroyLock) { + if (mDestroyed) { + throw new RuntimeException("Context already destroyed"); + } + if (!mAllowedObjects.contains(object)) { + throw new IllegalStateException( + "Leaking unknown objects " + object + " " + provider); + } + T t = (T) mObjectMap.get(object); + if (t != null) { + return t; + } + if (Looper.myLooper() == Looper.getMainLooper()) { + t = createObject(provider); + mObjectMap.put(object, t); + mOrderedObjects.add(t); + return t; + } + } + + try { + return MAIN_EXECUTOR.submit(() -> getObject(object, provider)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + @UiThread + protected T createObject(ObjectProvider provider) { + return provider.get(this); + } + } } diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java index 10611c7211..0c5b7225d3 100644 --- a/src/com/android/launcher3/util/SettingsCache.java +++ b/src/com/android/launcher3/util/SettingsCache.java @@ -47,7 +47,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * * Cache will also be updated if a key queried is missing (even if it has no listeners registered). */ -public class SettingsCache extends ContentObserver { +public class SettingsCache extends ContentObserver implements SafeCloseable { /** Hidden field Settings.Secure.NOTIFICATION_BADGING */ public static final Uri NOTIFICATION_BADGING_URI = @@ -69,7 +69,6 @@ public class SettingsCache extends ContentObserver { private final Map> mListenerMap = new HashMap<>(); protected final ContentResolver mResolver; - /** * Singleton instance */ @@ -81,6 +80,11 @@ public class SettingsCache extends ContentObserver { mResolver = context.getContentResolver(); } + @Override + public void close() { + mResolver.unregisterContentObserver(this); + } + @Override public void onChange(boolean selfChange, Uri uri) { // We use default of 1, but if we're getting an onChange call, can assume a non-default diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java index 0f40179f79..ac5368c8b4 100644 --- a/src/com/android/launcher3/util/UiThreadHelper.java +++ b/src/com/android/launcher3/util/UiThreadHelper.java @@ -28,7 +28,7 @@ import android.os.Message; import android.view.View; import android.view.inputmethod.InputMethodManager; -import com.android.launcher3.Launcher; +import com.android.launcher3.BaseActivity; import com.android.launcher3.views.ActivityContext; /** @@ -56,7 +56,7 @@ public class UiThreadHelper { STATS_LOGGER_KEY, Message.obtain( HANDLER.get(root.getContext()), - () -> Launcher.cast(activityContext) + () -> BaseActivity.fromContext(root.getContext()) .getStatsLogManager() .logger() .log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED) diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java index 329a44452b..2e2a968a44 100644 --- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java +++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java @@ -33,6 +33,7 @@ import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.systemui.plugins.CustomWidgetPlugin; @@ -46,7 +47,7 @@ import java.util.stream.Stream; /** * CustomWidgetManager handles custom widgets implemented as a plugin. */ -public class CustomWidgetManager implements PluginListener { +public class CustomWidgetManager implements PluginListener, SafeCloseable { public static final MainThreadInitializedObject INSTANCE = new MainThreadInitializedObject<>(CustomWidgetManager::new); @@ -71,7 +72,8 @@ public class CustomWidgetManager implements PluginListener { .addPluginListener(this, CustomWidgetPlugin.class, true); } - public void onDestroy() { + @Override + public void close() { PluginManagerWrapper.INSTANCE.get(mContext).removePluginListener(this); } diff --git a/robolectric_tests/resources/cache_data_updated_task_data.txt b/tests/res/raw/cache_data_updated_task_data.txt similarity index 100% rename from robolectric_tests/resources/cache_data_updated_task_data.txt rename to tests/res/raw/cache_data_updated_task_data.txt diff --git a/robolectric_tests/resources/db_schema_v10.json b/tests/res/raw/db_schema_v10.json similarity index 100% rename from robolectric_tests/resources/db_schema_v10.json rename to tests/res/raw/db_schema_v10.json diff --git a/robolectric_tests/resources/package_install_state_change_task_data.txt b/tests/res/raw/package_install_state_change_task_data.txt similarity index 100% rename from robolectric_tests/resources/package_install_state_change_task_data.txt rename to tests/res/raw/package_install_state_change_task_data.txt diff --git a/robolectric_tests/resources/widgets_predication_update_task_data.txt b/tests/res/raw/widgets_predication_update_task_data.txt similarity index 100% rename from robolectric_tests/resources/widgets_predication_update_task_data.txt rename to tests/res/raw/widgets_predication_update_task_data.txt diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java similarity index 93% rename from robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java rename to tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java index 8aa6f374aa..8a4590a388 100644 --- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java +++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java @@ -13,6 +13,9 @@ import android.content.Intent; import android.graphics.Rect; import android.util.Pair; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings; @@ -21,19 +24,17 @@ import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.ContentWriter; +import com.android.launcher3.util.Executors; import com.android.launcher3.util.GridOccupancy; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSparseArrayMap; import com.android.launcher3.util.LauncherModelHelper; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.LooperMode; -import org.robolectric.annotation.LooperMode.Mode; import java.util.ArrayList; import java.util.List; @@ -41,8 +42,8 @@ import java.util.List; /** * Tests for {@link AddWorkspaceItemsTask} */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(Mode.PAUSED) +@SmallTest +@RunWith(AndroidJUnit4.class) public class AddWorkspaceItemsTaskTest { private final ComponentName mComponent1 = new ComponentName("a", "b"); @@ -60,7 +61,7 @@ public class AddWorkspaceItemsTaskTest { @Before public void setup() { mModelHelper = new LauncherModelHelper(); - mTargetContext = RuntimeEnvironment.application; + mTargetContext = mModelHelper.sandboxContext; mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext); mIdp.numColumns = mIdp.numRows = 5; mAppState = LauncherAppState.getInstance(mTargetContext); @@ -70,6 +71,11 @@ public class AddWorkspaceItemsTaskTest { mNewScreens = new IntArray(); } + @After + public void tearDown() { + mModelHelper.destroy(); + } + private AddWorkspaceItemsTask newTask(ItemInfo... items) { List> list = new ArrayList<>(); for (ItemInfo item : items) { @@ -88,7 +94,7 @@ public class AddWorkspaceItemsTaskTest { int[] spaceFound = newTask().findSpaceForItem( mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1); - assertEquals(2, spaceFound[0]); + assertEquals(1, spaceFound[0]); assertTrue(mScreenOccupancy.get(spaceFound[0]) .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1)); @@ -127,7 +133,7 @@ public class AddWorkspaceItemsTaskTest { @Test public void testAddItem_some_items_added() throws Exception { Callbacks callbacks = mock(Callbacks.class); - mModelHelper.getModel().addCallbacks(callbacks); + Executors.MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(callbacks)).get(); WorkspaceItemInfo info = new WorkspaceItemInfo(); info.intent = new Intent().setComponent(mComponent1); diff --git a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java b/tests/src/com/android/launcher3/model/BackupRestoreTest.java similarity index 62% rename from robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java rename to tests/src/com/android/launcher3/model/BackupRestoreTest.java index a397db565d..41914de1ab 100644 --- a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java +++ b/tests/src/com/android/launcher3/model/BackupRestoreTest.java @@ -17,6 +17,7 @@ package com.android.launcher3.model; import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_RESTORE; +import static android.os.Process.myUserHandle; import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME; import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; @@ -26,74 +27,110 @@ import static com.android.launcher3.provider.LauncherDbUtils.tableExists; import static com.android.launcher3.util.LauncherModelHelper.APP_ICON; import static com.android.launcher3.util.LauncherModelHelper.NO__ICON; import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT; +import static com.android.launcher3.util.ReflectionHelpers.getField; +import static com.android.launcher3.util.ReflectionHelpers.setField; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.robolectric.util.ReflectionHelpers.setField; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; import android.app.backup.BackupManager; import android.content.pm.PackageInstaller; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.os.Process; import android.os.UserHandle; -import android.os.UserManager; +import android.util.ArrayMap; +import android.util.LongSparseArray; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.pm.UserCache; import com.android.launcher3.provider.RestoreDbTask; -import com.android.launcher3.shadows.LShadowBackupManager; import com.android.launcher3.util.LauncherModelHelper; +import com.android.launcher3.util.SafeCloseable; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.LooperMode; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowUserManager; /** * Tests to verify backup and restore flow. */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(LooperMode.Mode.PAUSED) +@SmallTest +@RunWith(AndroidJUnit4.class) public class BackupRestoreTest { - private static final long MY_OLD_PROFILE_ID = 1; - private static final long MY_PROFILE_ID = 0; - private static final long OLD_WORK_PROFILE_ID = 11; - private static final int WORK_PROFILE_ID = 10; + private static final int PER_USER_RANGE = 200000; + + + private long mCurrentMyProfileId; + private long mOldMyProfileId; + + private long mCurrentWorkProfileId; + private long mOldWorkProfileId; - private ShadowUserManager mUserManager; private BackupManager mBackupManager; private LauncherModelHelper mModelHelper; private SQLiteDatabase mDb; private InvariantDeviceProfile mIdp; + private UserHandle mWorkUserHandle; + + private SafeCloseable mUserChangeListener; + @Before public void setUp() { + mModelHelper = new LauncherModelHelper(); + + mCurrentMyProfileId = mModelHelper.defaultProfileId; + mOldMyProfileId = mCurrentMyProfileId + 1; + mCurrentWorkProfileId = mOldMyProfileId + 1; + mOldWorkProfileId = mCurrentWorkProfileId + 1; + + mWorkUserHandle = UserHandle.getUserHandleForUid(PER_USER_RANGE); + mUserChangeListener = UserCache.INSTANCE.get(mModelHelper.sandboxContext) + .addUserChangeListener(() -> { }); + setupUserManager(); setupBackupManager(); - mModelHelper = new LauncherModelHelper(); - RestoreDbTask.setPending(RuntimeEnvironment.application); + RestoreDbTask.setPending(mModelHelper.sandboxContext); mDb = mModelHelper.provider.getDb(); - mIdp = InvariantDeviceProfile.INSTANCE.get(RuntimeEnvironment.application); + mIdp = InvariantDeviceProfile.INSTANCE.get(mModelHelper.sandboxContext); + + } + + @After + public void tearDown() { + mUserChangeListener.close(); + mModelHelper.destroy(); } private void setupUserManager() { - final UserManager userManager = RuntimeEnvironment.application.getSystemService( - UserManager.class); - mUserManager = Shadow.extract(userManager); - // sign in to work profile - mUserManager.addUser(WORK_PROFILE_ID, "work", ShadowUserManager.FLAG_MANAGED_PROFILE); + UserCache cache = UserCache.INSTANCE.get(mModelHelper.sandboxContext); + synchronized (cache) { + LongSparseArray users = getField(cache, "mUsers"); + users.clear(); + users.put(mCurrentMyProfileId, myUserHandle()); + users.put(mCurrentWorkProfileId, mWorkUserHandle); + + ArrayMap userMap = getField(cache, "mUserToSerialMap"); + userMap.clear(); + userMap.put(myUserHandle(), mCurrentMyProfileId); + userMap.put(mWorkUserHandle, mCurrentWorkProfileId); + } } private void setupBackupManager() { - mBackupManager = new BackupManager(RuntimeEnvironment.application); - final LShadowBackupManager bm = Shadow.extract(mBackupManager); - bm.addProfile(MY_OLD_PROFILE_ID, Process.myUserHandle()); - bm.addProfile(OLD_WORK_PROFILE_ID, UserHandle.of(WORK_PROFILE_ID)); + mBackupManager = spy(new BackupManager(mModelHelper.sandboxContext)); + doReturn(myUserHandle()).when(mBackupManager) + .getUserForAncestralSerialNumber(eq(mOldMyProfileId)); + doReturn(mWorkUserHandle).when(mBackupManager) + .getUserForAncestralSerialNumber(eq(mOldWorkProfileId)); } @Test @@ -118,18 +155,18 @@ public class BackupRestoreTest { { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON}, { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT}, { APP_ICON, SHORTCUT, SHORTCUT, APP_ICON}, - }}, 1, MY_OLD_PROFILE_ID); + }}, 1, mOldMyProfileId); // setup grid for work profile on second screen mModelHelper.createGrid(new int[][][]{{ { NO__ICON, APP_ICON, SHORTCUT, SHORTCUT}, { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON}, { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT}, { APP_ICON, SHORTCUT, SHORTCUT, NO__ICON}, - }}, 2, OLD_WORK_PROFILE_ID); + }}, 2, mOldWorkProfileId); // simulates the creation of backup upon restore - new GridBackupTable(RuntimeEnvironment.application, mDb, mIdp.numDatabaseHotseatIcons, + new GridBackupTable(mModelHelper.sandboxContext, mDb, mIdp.numDatabaseHotseatIcons, mIdp.numColumns, mIdp.numRows).doBackup( - MY_OLD_PROFILE_ID, GridBackupTable.OPTION_REQUIRES_SANITIZATION); + mOldMyProfileId, GridBackupTable.OPTION_REQUIRES_SANITIZATION); // reset favorites table createTableUsingOldProfileId(); } @@ -141,28 +178,28 @@ public class BackupRestoreTest { private void verifyTableIsFilled(String tableName, boolean sanitized) { assertEquals(sanitized ? 12 : 13, getCount(mDb, "SELECT * FROM " + tableName + " WHERE profileId = " - + (sanitized ? MY_PROFILE_ID : MY_OLD_PROFILE_ID))); + + (sanitized ? mCurrentMyProfileId : mOldMyProfileId))); assertEquals(10, getCount(mDb, "SELECT * FROM " + tableName + " WHERE profileId = " - + (sanitized ? WORK_PROFILE_ID : OLD_WORK_PROFILE_ID))); + + (sanitized ? mCurrentWorkProfileId : mOldWorkProfileId))); } private void createTableUsingOldProfileId() { // simulates the creation of favorites table on old device dropTable(mDb, TABLE_NAME); - addTableToDb(mDb, MY_OLD_PROFILE_ID, false); + addTableToDb(mDb, mOldMyProfileId, false); } private void createRestoreSession() throws Exception { final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); - final PackageInstaller installer = RuntimeEnvironment.application.getPackageManager() + final PackageInstaller installer = mModelHelper.sandboxContext.getPackageManager() .getPackageInstaller(); final int sessionId = installer.createSession(params); final PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId); setField(info, "installReason", INSTALL_REASON_DEVICE_RESTORE); // TODO: (b/148410677) we should verify the following call instead // InstallSessionHelper.INSTANCE.get(getContext()).restoreDbIfApplicable(info); - RestoreDbTask.restoreIfPossible(RuntimeEnvironment.application, + RestoreDbTask.restoreIfPossible(mModelHelper.sandboxContext, mModelHelper.provider.getHelper(), mBackupManager); } diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java similarity index 92% rename from robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java rename to tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java index 9ac3fe761e..dba0a4063f 100644 --- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java +++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -16,6 +16,8 @@ import android.os.UserHandle; import android.os.UserManager; import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; import com.android.launcher3.LauncherAppState; import com.android.launcher3.icons.BitmapInfo; @@ -26,13 +28,10 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.LauncherModelHelper; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.LooperMode; -import org.robolectric.annotation.LooperMode.Mode; import java.util.Arrays; import java.util.HashSet; @@ -40,8 +39,8 @@ import java.util.HashSet; /** * Tests for {@link CacheDataUpdatedTask} */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(Mode.PAUSED) +@SmallTest +@RunWith(AndroidJUnit4.class) public class CacheDataUpdatedTaskTest { private static final String NEW_LABEL_PREFIX = "new-label-"; @@ -51,10 +50,10 @@ public class CacheDataUpdatedTaskTest { @Before public void setup() throws Exception { mModelHelper = new LauncherModelHelper(); - mModelHelper.initializeData("/cache_data_updated_task_data.txt"); + mModelHelper.initializeData("cache_data_updated_task_data"); // Add placeholder entries in the cache to simulate update - Context context = RuntimeEnvironment.application; + Context context = mModelHelper.sandboxContext; IconCache iconCache = LauncherAppState.getInstance(context).getIconCache(); CachingLogic placeholderLogic = new CachingLogic() { @Override @@ -86,6 +85,11 @@ public class CacheDataUpdatedTaskTest { } } + @After + public void tearDown() { + mModelHelper.destroy(); + } + private CacheDataUpdatedTask newTask(int op, String... pkg) { return new CacheDataUpdatedTask(op, Process.myUserHandle(), new HashSet<>(Arrays.asList(pkg))); diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java similarity index 91% rename from robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java rename to tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java index be03c7dd4b..d849c8fff9 100644 --- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java +++ b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java @@ -15,12 +15,13 @@ */ package com.android.launcher3.model; +import static androidx.test.InstrumentationRegistry.getContext; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertTrue; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -32,6 +33,10 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + import com.android.launcher3.LauncherProvider; import com.android.launcher3.LauncherProvider.DatabaseHelper; import com.android.launcher3.LauncherSettings.Favorites; @@ -40,15 +45,14 @@ import com.android.launcher3.R; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import java.io.File; /** * Tests for {@link DbDowngradeHelper} */ -@RunWith(RobolectricTestRunner.class) +@SmallTest +@RunWith(AndroidJUnit4.class) public class DbDowngradeHelperTest { private static final String SCHEMA_FILE = "test_schema.json"; @@ -60,7 +64,7 @@ public class DbDowngradeHelperTest { @Before public void setup() { - mContext = RuntimeEnvironment.application; + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); mSchemaFile = mContext.getFileStreamPath(SCHEMA_FILE); mDbFile = mContext.getDatabasePath(DB_FILE); } @@ -77,8 +81,10 @@ public class DbDowngradeHelperTest { public void testUpdateSchemaFile() throws Exception { // Setup mock resources Resources res = spy(mContext.getResources()); - doAnswer(i ->this.getClass().getResourceAsStream("/db_schema_v10.json")) - .when(res).openRawResource(eq(R.raw.downgrade_schema)); + Resources myRes = getContext().getResources(); + doAnswer(i -> myRes.openRawResource( + myRes.getIdentifier("db_schema_v10", "raw", getContext().getPackageName()))) + .when(res).openRawResource(R.raw.downgrade_schema); Context context = spy(mContext); when(context.getResources()).thenReturn(res); diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java similarity index 77% rename from robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java rename to tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java index 655237d08c..004ed06b32 100644 --- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java +++ b/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java @@ -16,18 +16,18 @@ package com.android.launcher3.model; +import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; import static org.junit.Assert.assertEquals; -import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.util.ReflectionHelpers.setField; -import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInstaller; -import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + import com.android.launcher3.LauncherSettings; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.model.data.FolderInfo; @@ -35,19 +35,16 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.LauncherModelHelper; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.LooperMode; -import org.robolectric.annotation.LooperMode.Mode; /** * Tests for layout parser for remote layout */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(Mode.PAUSED) +@SmallTest +@RunWith(AndroidJUnit4.class) public class DefaultLayoutProviderTest { private LauncherModelHelper mModelHelper; @@ -56,16 +53,18 @@ public class DefaultLayoutProviderTest { @Before public void setUp() { mModelHelper = new LauncherModelHelper(); - mTargetContext = RuntimeEnvironment.application; + mTargetContext = mModelHelper.sandboxContext; + } - shadowOf(mTargetContext.getPackageManager()) - .addActivityIfNotPresent(new ComponentName(TEST_PACKAGE, TEST_PACKAGE)); + @After + public void tearDown() { + mModelHelper.destroy(); } @Test public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception { writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0) - .putApp(TEST_PACKAGE, TEST_PACKAGE)); + .putApp(TEST_PACKAGE, TEST_ACTIVITY)); // Verify one item in hotseat assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size()); @@ -77,9 +76,9 @@ public class DefaultLayoutProviderTest { @Test public void testCustomProfileLoaded_with_folder() throws Exception { writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy) - .addApp(TEST_PACKAGE, TEST_PACKAGE) - .addApp(TEST_PACKAGE, TEST_PACKAGE) - .addApp(TEST_PACKAGE, TEST_PACKAGE) + .addApp(TEST_PACKAGE, TEST_ACTIVITY) + .addApp(TEST_PACKAGE, TEST_ACTIVITY) + .addApp(TEST_PACKAGE, TEST_ACTIVITY) .build()); // Verify folder @@ -92,9 +91,9 @@ public class DefaultLayoutProviderTest { @Test public void testCustomProfileLoaded_with_folder_custom_title() throws Exception { writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder("CustomFolder") - .addApp(TEST_PACKAGE, TEST_PACKAGE) - .addApp(TEST_PACKAGE, TEST_PACKAGE) - .addApp(TEST_PACKAGE, TEST_PACKAGE) + .addApp(TEST_PACKAGE, TEST_ACTIVITY) + .addApp(TEST_PACKAGE, TEST_ACTIVITY) + .addApp(TEST_PACKAGE, TEST_ACTIVITY) .build()); // Verify folder @@ -112,12 +111,10 @@ public class DefaultLayoutProviderTest { // Add a placeholder session info so that the widget exists SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL); params.setAppPackageName(pendingAppPkg); + params.setAppIcon(BitmapInfo.LOW_RES_ICON); PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller(); - int sessionId = installer.createSession(params); - SessionInfo sessionInfo = installer.getSessionInfo(sessionId); - setField(sessionInfo, "installerPackageName", "com.test"); - setField(sessionInfo, "appIcon", BitmapInfo.LOW_RES_ICON); + installer.createSession(params); writeLayoutAndLoad(new LauncherLayoutBuilder().atWorkspace(0, 1, 0) .putWidget(pendingAppPkg, "PlaceholderWidget", 2, 2)); diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java similarity index 89% rename from robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java rename to tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java index 87b08870c8..005389e5bb 100644 --- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java +++ b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java @@ -30,26 +30,31 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Point; import android.os.Process; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherSettings; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.LauncherModelHelper; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; +import java.util.HashMap; import java.util.HashSet; /** Unit tests for {@link GridSizeMigrationTaskV2} */ -@RunWith(RobolectricTestRunner.class) +@SmallTest +@RunWith(AndroidJUnit4.class) public class GridSizeMigrationTaskV2Test { private LauncherModelHelper mModelHelper; @@ -73,7 +78,7 @@ public class GridSizeMigrationTaskV2Test { @Before public void setUp() { mModelHelper = new LauncherModelHelper(); - mContext = RuntimeEnvironment.application; + mContext = mModelHelper.sandboxContext; mDb = mModelHelper.provider.getDb(); mValidPackages = new HashSet<>(); @@ -98,8 +103,13 @@ public class GridSizeMigrationTaskV2Test { LauncherSettings.Favorites.TMP_TABLE); } + @After + public void tearDown() { + mModelHelper.destroy(); + } + @Test - public void testMigration() { + public void testMigration() throws Exception { int[] srcHotseatItems = { mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI), mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI), @@ -134,17 +144,17 @@ public class GridSizeMigrationTaskV2Test { // Check hotseat items Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT}, - "container=" + CONTAINER_HOTSEAT, null, null, null); + "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null); assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons); int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN); int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); c.moveToNext(); - assertEquals(c.getInt(screenIndex), 1); - assertTrue(c.getString(intentIndex).contains(testPackage2)); - c.moveToNext(); assertEquals(c.getInt(screenIndex), 0); assertTrue(c.getString(intentIndex).contains(testPackage1)); c.moveToNext(); + assertEquals(c.getInt(screenIndex), 1); + assertTrue(c.getString(intentIndex).contains(testPackage2)); + c.moveToNext(); assertEquals(c.getInt(screenIndex), 2); assertTrue(c.getString(intentIndex).contains(testPackage3)); c.moveToNext(); @@ -157,35 +167,24 @@ public class GridSizeMigrationTaskV2Test { new String[]{LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, LauncherSettings.Favorites.INTENT}, "container=" + CONTAINER_DESKTOP, null, null, null); - assertEquals(c.getCount(), 6); intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); int cellXIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLX); int cellYIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLY); - c.moveToNext(); - assertTrue(c.getString(intentIndex).contains(testPackage7)); - c.moveToNext(); - assertTrue(c.getString(intentIndex).contains(testPackage6)); - assertEquals(c.getInt(cellXIndex), 0); - assertEquals(c.getInt(cellYIndex), 3); - c.moveToNext(); - assertTrue(c.getString(intentIndex).contains(testPackage10)); - assertEquals(c.getInt(cellXIndex), 1); - assertEquals(c.getInt(cellYIndex), 3); - c.moveToNext(); - assertTrue(c.getString(intentIndex).contains(testPackage5)); - assertEquals(c.getInt(cellXIndex), 2); - assertEquals(c.getInt(cellYIndex), 3); - c.moveToNext(); - assertTrue(c.getString(intentIndex).contains(testPackage9)); - assertEquals(c.getInt(cellXIndex), 3); - assertEquals(c.getInt(cellYIndex), 3); - c.moveToNext(); - assertTrue(c.getString(intentIndex).contains(testPackage8)); - assertEquals(c.getInt(cellXIndex), 0); - assertEquals(c.getInt(cellYIndex), 2); - + HashMap locMap = new HashMap<>(); + while (c.moveToNext()) { + locMap.put( + Intent.parseUri(c.getString(intentIndex), 0).getPackage(), + new Point(c.getInt(cellXIndex), c.getInt(cellYIndex))); + } c.close(); + + assertEquals(locMap.size(), 6); + assertEquals(new Point(0, 2), locMap.get(testPackage8)); + assertEquals(new Point(0, 3), locMap.get(testPackage6)); + assertEquals(new Point(1, 3), locMap.get(testPackage10)); + assertEquals(new Point(2, 3), locMap.get(testPackage5)); + assertEquals(new Point(3, 3), locMap.get(testPackage9)); } @Test @@ -212,7 +211,7 @@ public class GridSizeMigrationTaskV2Test { // Check hotseat items Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT}, - "container=" + CONTAINER_HOTSEAT, null, null, null); + "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null); assertEquals(c.getCount(), numSrcDatabaseHotseatIcons); int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN); int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); @@ -257,7 +256,7 @@ public class GridSizeMigrationTaskV2Test { // Check hotseat items Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT}, - "container=" + CONTAINER_HOTSEAT, null, null, null); + "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null); assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons); int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN); int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java similarity index 93% rename from robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java rename to tests/src/com/android/launcher3/model/LoaderCursorTest.java index 800311afb6..6444ef6927 100644 --- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java +++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java @@ -16,6 +16,8 @@ package com.android.launcher3.model; +import static androidx.test.InstrumentationRegistry.getContext; + import static com.android.launcher3.LauncherSettings.Favorites.CELLX; import static com.android.launcher3.LauncherSettings.Favorites.CELLY; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER; @@ -33,7 +35,7 @@ import static com.android.launcher3.LauncherSettings.Favorites.RESTORED; import static com.android.launcher3.LauncherSettings.Favorites.SCREEN; import static com.android.launcher3.LauncherSettings.Favorites.TITLE; import static com.android.launcher3.LauncherSettings.Favorites._ID; -import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; +import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -41,37 +43,37 @@ import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; -import static org.robolectric.Shadows.shadowOf; - import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.database.MatrixCursor; import android.os.Process; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.Executors; +import com.android.launcher3.util.LauncherModelHelper; import com.android.launcher3.util.PackageManagerHelper; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.LooperMode; -import org.robolectric.annotation.LooperMode.Mode; /** * Tests for {@link LoaderCursor} */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(Mode.PAUSED) +@SmallTest +@RunWith(AndroidJUnit4.class) public class LoaderCursorTest { + private LauncherModelHelper mModelHelper; private LauncherAppState mApp; private MatrixCursor mCursor; @@ -82,7 +84,8 @@ public class LoaderCursorTest { @Before public void setup() { - mContext = RuntimeEnvironment.application; + mModelHelper = new LauncherModelHelper(); + mContext = mModelHelper.sandboxContext; mIDP = InvariantDeviceProfile.INSTANCE.get(mContext); mApp = LauncherAppState.getInstance(mContext); @@ -97,6 +100,11 @@ public class LoaderCursorTest { ums.allUsers.put(0, Process.myUserHandle()); } + @After + public void tearDown() { + mModelHelper.destroy(); + } + private void initCursor(int itemType, String title) { mCursor.newRow() .add(_ID, 1) @@ -117,9 +125,7 @@ public class LoaderCursorTest { @Test public void getAppShortcutInfo_dontAllowMissing_validComponent() throws Exception { - ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_PACKAGE); - shadowOf(mContext.getPackageManager()).addActivityIfNotPresent(cn); - + ComponentName cn = new ComponentName(getContext(), TEST_ACTIVITY); initCursor(ITEM_TYPE_APPLICATION, ""); assertTrue(mLoaderCursor.moveToNext()); diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java similarity index 65% rename from robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java rename to tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java index 43193553fe..42c9f11a52 100644 --- a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java +++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java @@ -15,69 +15,60 @@ */ package com.android.launcher3.model; -import static com.android.launcher3.util.Executors.createAndStartNewLooper; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.spy; -import static org.robolectric.Shadows.shadowOf; import android.os.Process; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.shadows.ShadowLooperExecutor; import com.android.launcher3.util.Executors; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.LauncherModelHelper; -import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.RunnableList; +import com.android.launcher3.util.TestUtil; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.LooperMode; -import org.robolectric.annotation.LooperMode.Mode; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowPackageManager; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** * Tests to verify multiple callbacks in Loader */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(Mode.PAUSED) +@SmallTest +@RunWith(AndroidJUnit4.class) public class ModelMultiCallbacksTest { private LauncherModelHelper mModelHelper; - private ShadowPackageManager mSpm; - private LooperExecutor mTempMainExecutor; - @Before - public void setUp() throws Exception { + public void setUp() { mModelHelper = new LauncherModelHelper(); - mModelHelper.installApp(TEST_PACKAGE); + } - mSpm = shadowOf(RuntimeEnvironment.application.getPackageManager()); - - // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread, - // so that we can wait appropriately for the loader to complete. - mTempMainExecutor = new LooperExecutor(createAndStartNewLooper("tempMain")); - ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR); - sle.setHandler(mTempMainExecutor.getHandler()); + @After + public void tearDown() throws Exception { + mModelHelper.destroy(); + TestUtil.uninstallDummyApp(); } @Test @@ -85,7 +76,7 @@ public class ModelMultiCallbacksTest { setupWorkspacePages(3); MyCallbacks cb1 = spy(MyCallbacks.class); - mModelHelper.getModel().addCallbacksAndLoad(cb1); + Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb1)); waitForLoaderAndTempMainThread(); cb1.verifySynchronouslyBound(3); @@ -94,10 +85,10 @@ public class ModelMultiCallbacksTest { cb1.reset(); MyCallbacks cb2 = spy(MyCallbacks.class); cb2.mPageToBindSync = IntSet.wrap(2); - mModelHelper.getModel().addCallbacksAndLoad(cb2); + Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb2)); waitForLoaderAndTempMainThread(); - cb1.verifySynchronouslyBound(3); + assertFalse(cb1.bindStarted); cb2.verifySynchronouslyBound(3); // Remove callbacks @@ -105,7 +96,7 @@ public class ModelMultiCallbacksTest { cb2.reset(); // No effect on callbacks when removing an callback - mModelHelper.getModel().removeCallbacks(cb2); + Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2)); waitForLoaderAndTempMainThread(); assertNull(cb1.mPendingTasks); assertNull(cb2.mPendingTasks); @@ -119,52 +110,48 @@ public class ModelMultiCallbacksTest { @Test public void testTwoCallbacks_receiveUpdates() throws Exception { + TestUtil.uninstallDummyApp(); + setupWorkspacePages(1); MyCallbacks cb1 = spy(MyCallbacks.class); MyCallbacks cb2 = spy(MyCallbacks.class); - mModelHelper.getModel().addCallbacksAndLoad(cb1); - mModelHelper.getModel().addCallbacksAndLoad(cb2); + Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb1)); + Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb2)); waitForLoaderAndTempMainThread(); - cb1.verifyApps(TEST_PACKAGE); - cb2.verifyApps(TEST_PACKAGE); + assertTrue(cb1.allApps().contains(TEST_PACKAGE)); + assertTrue(cb2.allApps().contains(TEST_PACKAGE)); // Install package 1 - String pkg1 = "com.test.pkg1"; - mModelHelper.installApp(pkg1); - mModelHelper.getModel().onPackageAdded(pkg1, Process.myUserHandle()); + TestUtil.installDummyApp(); + mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle()); waitForLoaderAndTempMainThread(); - cb1.verifyApps(TEST_PACKAGE, pkg1); - cb2.verifyApps(TEST_PACKAGE, pkg1); - - // Install package 2 - String pkg2 = "com.test.pkg2"; - mModelHelper.installApp(pkg2); - mModelHelper.getModel().onPackageAdded(pkg2, Process.myUserHandle()); - waitForLoaderAndTempMainThread(); - cb1.verifyApps(TEST_PACKAGE, pkg1, pkg2); - cb2.verifyApps(TEST_PACKAGE, pkg1, pkg2); + assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE)); + assertTrue(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE)); // Uninstall package 2 - mSpm.removePackage(pkg1); - mModelHelper.getModel().onPackageRemoved(pkg1, Process.myUserHandle()); + TestUtil.uninstallDummyApp(); + mModelHelper.getModel().onPackageRemoved(TestUtil.DUMMY_PACKAGE, Process.myUserHandle()); waitForLoaderAndTempMainThread(); - cb1.verifyApps(TEST_PACKAGE, pkg2); - cb2.verifyApps(TEST_PACKAGE, pkg2); + assertFalse(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE)); + assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE)); // Unregister a callback and verify updates no longer received - mModelHelper.getModel().removeCallbacks(cb2); - mSpm.removePackage(pkg2); - mModelHelper.getModel().onPackageRemoved(pkg2, Process.myUserHandle()); + Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2)); + TestUtil.installDummyApp(); + mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle()); waitForLoaderAndTempMainThread(); - cb1.verifyApps(TEST_PACKAGE); - cb2.verifyApps(TEST_PACKAGE, pkg2); + + // cb2 didn't get the update + assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE)); + assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE)); } private void waitForLoaderAndTempMainThread() throws Exception { + Executors.MAIN_EXECUTOR.submit(() -> { }).get(); Executors.MODEL_EXECUTOR.submit(() -> { }).get(); - mTempMainExecutor.submit(() -> { }).get(); + Executors.MAIN_EXECUTOR.submit(() -> { }).get(); } private void setupWorkspacePages(int pageCount) throws Exception { @@ -183,9 +170,15 @@ public class ModelMultiCallbacksTest { IntSet mPageBoundSync = new IntSet(); RunnableList mPendingTasks; AppInfo[] mAppInfos; + boolean bindStarted; MyCallbacks() { } + @Override + public void startBinding() { + bindStarted = true; + } + @Override public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) { mPageBoundSync = boundPages; @@ -212,10 +205,12 @@ public class ModelMultiCallbacksTest { mPageBoundSync = new IntSet(); mPendingTasks = null; mAppInfos = null; + bindStarted = false; } public void verifySynchronouslyBound(int totalItems) { // Verify that the requested page is bound synchronously + assertTrue(bindStarted); assertEquals(mPageToBindSync, mPageBoundSync); assertEquals(mItems.size(), 1); assertEquals(IntSet.wrap(mItems.get(0).screenId), mPageBoundSync); @@ -226,12 +221,14 @@ public class ModelMultiCallbacksTest { assertEquals(mItems.size(), totalItems); } - public void verifyApps(String... apps) { - assertEquals(apps.length, mAppInfos.length); - assertEquals(Arrays.stream(mAppInfos) + public Set allApps() { + return Arrays.stream(mAppInfos) .map(ai -> ai.getTargetComponent().getPackageName()) - .collect(Collectors.toSet()), - new HashSet<>(Arrays.asList(apps))); + .collect(Collectors.toSet()); + } + + public void verifyApps(String... apps) { + assertTrue(allApps().containsAll(Arrays.asList(apps))); } } } diff --git a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java similarity index 87% rename from robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java rename to tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java index 412ace03ff..519191e251 100644 --- a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java +++ b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java @@ -2,18 +2,19 @@ package com.android.launcher3.model; import static org.junit.Assert.assertEquals; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.PackageInstallInfo; import com.android.launcher3.util.LauncherModelHelper; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.LooperMode; -import org.robolectric.annotation.LooperMode.Mode; import java.util.Arrays; import java.util.HashSet; @@ -21,8 +22,8 @@ import java.util.HashSet; /** * Tests for {@link PackageInstallStateChangedTask} */ -@RunWith(RobolectricTestRunner.class) -@LooperMode(Mode.PAUSED) +@SmallTest +@RunWith(AndroidJUnit4.class) public class PackageInstallStateChangedTaskTest { private LauncherModelHelper mModelHelper; @@ -30,7 +31,12 @@ public class PackageInstallStateChangedTaskTest { @Before public void setup() throws Exception { mModelHelper = new LauncherModelHelper(); - mModelHelper.initializeData("/package_install_state_change_task_data.txt"); + mModelHelper.initializeData("package_install_state_change_task_data"); + } + + @After + public void tearDown() { + mModelHelper.destroy(); } private PackageInstallStateChangedTask newTask(String pkg, int progress) { @@ -66,7 +72,7 @@ public class PackageInstallStateChangedTaskTest { HashSet updates = new HashSet<>(Arrays.asList(idsUpdated)); for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) { if (info instanceof WorkspaceItemInfo) { - assertEquals(updates.contains(info.id) ? progress: 0, + assertEquals(updates.contains(info.id) ? progress: 100, ((WorkspaceItemInfo) info).getProgressLevel()); } else { assertEquals(updates.contains(info.id) ? progress: -1, diff --git a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java similarity index 93% rename from robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java rename to tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java index 4184d33a68..48305eebdf 100644 --- a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java +++ b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java @@ -21,18 +21,21 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import com.android.launcher3.LauncherProvider.DatabaseHelper; import com.android.launcher3.LauncherSettings.Favorites; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; /** * Tests for {@link RestoreDbTask} */ -@RunWith(RobolectricTestRunner.class) +@SmallTest +@RunWith(AndroidJUnit4.class) public class RestoreDbTaskTest { @Test @@ -95,7 +98,7 @@ public class RestoreDbTaskTest { private final long mProfileId; MyDatabaseHelper(long profileId) { - super(RuntimeEnvironment.application, null, false); + super(InstrumentationRegistry.getInstrumentation().getTargetContext(), null, false); mProfileId = profileId; } diff --git a/tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java b/tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java new file mode 100644 index 0000000000..fd86cf1144 --- /dev/null +++ b/tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2021 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.launcher3.secondarydisplay; + +import static androidx.test.core.app.ActivityScenario.launch; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.espresso.intent.Intents; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.MediumTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link SecondaryDisplayLauncher} + */ +@MediumTest +@RunWith(AndroidJUnit4.class) +public class SDLauncherTest { + + @Before + public void setUp() { + Intents.init(); + } + + @After + public void tearDown() { + Intents.release(); + } + + @Test + public void testAllAppsListOpens() { + ActivityScenario launcher = + launch(SecondaryDisplayLauncher.class); + launcher.onActivity(l -> l.showAppDrawer(true)); + } +} diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java similarity index 100% rename from robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java rename to tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java similarity index 60% rename from robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java rename to tests/src/com/android/launcher3/util/LauncherModelHelper.java index 846e2019a0..c9b63aeb11 100644 --- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java +++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java @@ -15,26 +15,40 @@ */ package com.android.launcher3.util; -import static android.content.Intent.ACTION_CREATE_SHORTCUT; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.launcher3.LauncherSettings.Favorites.CONTENT_URI; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static org.robolectric.Shadows.shadowOf; import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.Resources; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.AutoCloseOutputStream; import android.os.Process; import android.provider.Settings; +import android.test.mock.MockContentResolver; +import android.util.ArrayMap; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.uiautomator.UiDevice; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; @@ -45,27 +59,30 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.model.AllAppsList; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.Callbacks; +import com.android.launcher3.model.ItemInstallQueue; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.UserCache; -import com.android.launcher3.shadows.ShadowLooperExecutor; +import com.android.launcher3.testing.TestInformationProvider; +import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; +import com.android.launcher3.util.MainThreadInitializedObject.ObjectProvider; +import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.widget.custom.CustomWidgetManager; import org.mockito.ArgumentCaptor; -import org.robolectric.Robolectric; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowContentResolver; -import org.robolectric.shadows.ShadowPackageManager; -import org.robolectric.util.ReflectionHelpers; import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.function.Function; @@ -81,7 +98,9 @@ public class LauncherModelHelper { public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; public static final int NO__ICON = -1; - public static final String TEST_PACKAGE = "com.android.launcher3.validpackage"; + + public static final String TEST_PACKAGE = testContext().getPackageName(); + public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2"; // Authority for providing a test default-workspace-layout data. private static final String TEST_PROVIDER_AUTHORITY = @@ -90,21 +109,42 @@ public class LauncherModelHelper { private static final int DEFAULT_GRID_SIZE = 4; private final HashMap> mFieldCache = new HashMap<>(); + private final MockContentResolver mMockResolver = new MockContentResolver(); public final TestLauncherProvider provider; - private final long mDefaultProfileId; + public final SanboxModelContext sandboxContext; + + public final long defaultProfileId; private BgDataModel mDataModel; private AllAppsList mAllAppsList; public LauncherModelHelper() { - provider = Robolectric.setupContentProvider(TestLauncherProvider.class); - mDefaultProfileId = UserCache.INSTANCE.get(RuntimeEnvironment.application) + Context context = getApplicationContext(); + // System settings cache content provider. Ensure that they are statically initialized + Settings.Secure.getString(context.getContentResolver(), "test"); + Settings.System.getString(context.getContentResolver(), "test"); + Settings.Global.getString(context.getContentResolver(), "test"); + + provider = new TestLauncherProvider(); + sandboxContext = new SanboxModelContext(); + defaultProfileId = UserCache.INSTANCE.get(sandboxContext) .getSerialNumberForUser(Process.myUserHandle()); - ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider); + setupProvider(LauncherProvider.AUTHORITY, provider); + } + + protected void setupProvider(String authority, ContentProvider provider) { + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.authority = authority; + providerInfo.applicationInfo = sandboxContext.getApplicationInfo(); + provider.attachInfo(sandboxContext, providerInfo); + mMockResolver.addProvider(providerInfo.authority, provider); + doReturn(providerInfo) + .when(sandboxContext.mPm) + .resolveContentProvider(eq(authority), anyInt()); } public LauncherModel getModel() { - return LauncherAppState.getInstance(RuntimeEnvironment.application).getModel(); + return LauncherAppState.getInstance(sandboxContext).getModel(); } public synchronized BgDataModel getBgDataModel() { @@ -121,6 +161,28 @@ public class LauncherModelHelper { return mAllAppsList; } + public void destroy() { + // When destroying the context, make sure that the model thread is blocked, so that no + // new jobs get posted while we are cleaning up + CountDownLatch l1 = new CountDownLatch(1); + CountDownLatch l2 = new CountDownLatch(1); + MODEL_EXECUTOR.execute(() -> { + l1.countDown(); + waitOrThrow(l2); + }); + waitOrThrow(l1); + sandboxContext.onDestroy(); + l2.countDown(); + } + + private void waitOrThrow(CountDownLatch latch) { + try { + latch.await(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + /** * Synchronously executes the task and returns all the UI callbacks posted. */ @@ -161,13 +223,16 @@ public class LauncherModelHelper { * Initializes mock data for the test. */ public void initializeData(String resourceName) throws Exception { - Context targetContext = RuntimeEnvironment.application; BgDataModel bgDataModel = getBgDataModel(); AllAppsList allAppsList = getAllAppsList(); MODEL_EXECUTOR.submit(() -> { + // Copy apk from resources to a local file and install from there. + Resources resources = testContext().getResources(); + int resId = resources.getIdentifier( + resourceName, "raw", testContext().getPackageName()); try (BufferedReader reader = new BufferedReader(new InputStreamReader( - this.getClass().getResourceAsStream(resourceName)))) { + resources.openRawResource(resId)))) { String line; HashMap classMap = new HashMap<>(); while ((line = reader.readLine()) != null) { @@ -181,7 +246,7 @@ public class LauncherModelHelper { classMap.put(commands[1], Class.forName(commands[2])); break; case "bgItem": - bgDataModel.addItem(targetContext, + bgDataModel.addItem(sandboxContext, (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false); break; @@ -236,7 +301,7 @@ public class LauncherModelHelper { } public int addItem(int type, int screen, int container, int x, int y) { - return addItem(type, screen, container, x, y, mDefaultProfileId, TEST_PACKAGE); + return addItem(type, screen, container, x, y, defaultProfileId, TEST_PACKAGE); } public int addItem(int type, int screen, int container, int x, int y, long profileId) { @@ -244,12 +309,12 @@ public class LauncherModelHelper { } public int addItem(int type, int screen, int container, int x, int y, String packageName) { - return addItem(type, screen, container, x, y, mDefaultProfileId, packageName); + return addItem(type, screen, container, x, y, defaultProfileId, packageName); } public int addItem(int type, int screen, int container, int x, int y, String packageName, int id, Uri contentUri) { - addItem(type, screen, container, x, y, mDefaultProfileId, packageName, id, contentUri); + addItem(type, screen, container, x, y, defaultProfileId, packageName, id, contentUri); return id; } @@ -260,8 +325,7 @@ public class LauncherModelHelper { */ public int addItem(int type, int screen, int container, int x, int y, long profileId, String packageName) { - Context context = RuntimeEnvironment.application; - int id = LauncherSettings.Settings.call(context.getContentResolver(), + int id = LauncherSettings.Settings.call(sandboxContext.getContentResolver(), LauncherSettings.Settings.METHOD_NEW_ITEM_ID) .getInt(LauncherSettings.Settings.EXTRA_VALUE); addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI); @@ -270,8 +334,6 @@ public class LauncherModelHelper { public void addItem(int type, int screen, int container, int x, int y, long profileId, String packageName, int id, Uri contentUri) { - Context context = RuntimeEnvironment.application; - ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites._ID, id); values.put(LauncherSettings.Favorites.CONTAINER, container); @@ -295,7 +357,7 @@ public class LauncherModelHelper { } } - context.getContentResolver().insert(contentUri, values); + sandboxContext.getContentResolver().insert(contentUri, values); } public int[][][] createGrid(int[][][] typeArray) { @@ -303,12 +365,11 @@ public class LauncherModelHelper { } public int[][][] createGrid(int[][][] typeArray, int startScreen) { - final Context context = RuntimeEnvironment.application; - LauncherSettings.Settings.call(context.getContentResolver(), + LauncherSettings.Settings.call(sandboxContext.getContentResolver(), LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); - LauncherSettings.Settings.call(context.getContentResolver(), + LauncherSettings.Settings.call(sandboxContext.getContentResolver(), LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); - return createGrid(typeArray, startScreen, mDefaultProfileId); + return createGrid(typeArray, startScreen, defaultProfileId); } /** @@ -320,14 +381,13 @@ public class LauncherModelHelper { * @return the same grid representation where each entry is the corresponding item id. */ public int[][][] createGrid(int[][][] typeArray, int startScreen, long profileId) { - Context context = RuntimeEnvironment.application; int[][][] ids = new int[typeArray.length][][]; for (int i = 0; i < typeArray.length; i++) { // Add screen to DB int screenId = startScreen + i; // Keep the screen id counter up to date - LauncherSettings.Settings.call(context.getContentResolver(), + LauncherSettings.Settings.call(sandboxContext.getContentResolver(), LauncherSettings.Settings.METHOD_NEW_SCREEN_ID); ids[i] = new int[typeArray[i].length][]; @@ -353,69 +413,45 @@ public class LauncherModelHelper { */ public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder) throws Exception { - Context context = RuntimeEnvironment.application; - InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); + InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext); idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE; idp.iconBitmapSize = DEFAULT_BITMAP_SIZE; - Settings.Secure.putString(context.getContentResolver(), - "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY); + UiDevice.getInstance(getInstrumentation()).executeShellCommand( + "settings put secure launcher3.layout.provider " + TEST_PROVIDER_AUTHORITY); + ContentProvider cp = new TestInformationProvider() { - shadowOf(context.getPackageManager()) - .addProviderIfNotPresent(new ComponentName("com.test", "Mock")).authority = - TEST_PROVIDER_AUTHORITY; - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - builder.build(new OutputStreamWriter(bos)); - Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context); - shadowOf(context.getContentResolver()).registerInputStream(layoutUri, - new ByteArrayInputStream(bos.toByteArray())); + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + builder.build(new OutputStreamWriter(bos)); + outputStream.write(bos.toByteArray()); + outputStream.flush(); + outputStream.close(); + return pipe[0]; + } catch (Exception e) { + throw new FileNotFoundException(e.getMessage()); + } + } + }; + setupProvider(TEST_PROVIDER_AUTHORITY, cp); return this; } - /** - * Simulates an apk install with a default main activity with same class and package name - */ - public void installApp(String component) throws NameNotFoundException { - IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN); - filter.addCategory(Intent.CATEGORY_LAUNCHER); - installApp(component, component, filter); - } - - /** - * Simulates a custom shortcut install - */ - public void installCustomShortcut(String pkg, String clazz) throws NameNotFoundException { - installApp(pkg, clazz, new IntentFilter(ACTION_CREATE_SHORTCUT)); - } - - private void installApp(String pkg, String clazz, IntentFilter filter) - throws NameNotFoundException { - ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager()); - ComponentName cn = new ComponentName(pkg, clazz); - spm.addActivityIfNotPresent(cn); - - filter.addCategory(Intent.CATEGORY_DEFAULT); - spm.addIntentFilterForActivity(cn, filter); - } - /** * Loads the model in memory synchronously */ public void loadModelSync() throws ExecutionException, InterruptedException { - // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread, - // so that we can wait appropriately for the loader to complete. - ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR); - sle.setHandler(Executors.UI_HELPER_EXECUTOR.getHandler()); - - Callbacks mockCb = mock(Callbacks.class); - getModel().addCallbacksAndLoad(mockCb); + Callbacks mockCb = new Callbacks() { }; + Executors.MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get(); Executors.MODEL_EXECUTOR.submit(() -> { }).get(); - Executors.UI_HELPER_EXECUTOR.submit(() -> { }).get(); - - sle.setHandler(null); - getModel().removeCallbacks(mockCb); + Executors.MAIN_EXECUTOR.submit(() -> { }).get(); + Executors.MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get(); } /** @@ -437,4 +473,97 @@ public class LauncherModelHelper { return mOpenHelper; } } + + public static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + success = false; + } + } + } + return success; + } + + public class SanboxModelContext extends SandboxContext { + + private final ArrayMap mSpiedServices = new ArrayMap<>(); + private final PackageManager mPm; + private final File mDbDir; + + SanboxModelContext() { + super(ApplicationProvider.getApplicationContext(), + UserCache.INSTANCE, InstallSessionHelper.INSTANCE, + LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, + DisplayController.INSTANCE, CustomWidgetManager.INSTANCE, + SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, + ItemInstallQueue.INSTANCE); + mPm = spy(getBaseContext().getPackageManager()); + mDbDir = new File(getCacheDir(), UUID.randomUUID().toString()); + } + + public SanboxModelContext allow(MainThreadInitializedObject object) { + mAllowedObjects.add(object); + return this; + } + + @Override + public File getDatabasePath(String name) { + if (!mDbDir.exists()) { + mDbDir.mkdirs(); + } + return new File(mDbDir, name); + } + + @Override + public ContentResolver getContentResolver() { + return mMockResolver; + } + + @Override + public void onDestroy() { + if (deleteContents(mDbDir)) { + mDbDir.delete(); + } + super.onDestroy(); + } + + + @Override + protected T createObject(ObjectProvider provider) { + return spy(provider.get(this)); + } + + @Override + public PackageManager getPackageManager() { + return mPm; + } + + @Override + public Object getSystemService(String name) { + Object service = mSpiedServices.get(name); + return service != null ? service : super.getSystemService(name); + } + + public T spyService(Class tClass) { + String name = getSystemServiceName(tClass); + Object service = mSpiedServices.get(name); + if (service != null) { + return (T) service; + } + + T result = spy(getSystemService(tClass)); + mSpiedServices.put(name, result); + return result; + } + } + + private static Context testContext() { + return getInstrumentation().getContext(); + } } diff --git a/tests/src/com/android/launcher3/util/ReflectionHelpers.java b/tests/src/com/android/launcher3/util/ReflectionHelpers.java new file mode 100644 index 0000000000..d89975de8a --- /dev/null +++ b/tests/src/com/android/launcher3/util/ReflectionHelpers.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 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.launcher3.util; + +import java.lang.reflect.Field; + +public class ReflectionHelpers { + + /** + * Reflectively get the value of a field. + * + * @param object Target object. + * @param fieldName The field name. + * @param The return type. + * @return Value of the field on the object. + */ + public static R getField(Object object, String fieldName) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (R) field.get(object); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Reflectively set the value of a field. + * + * @param object Target object. + * @param fieldName The field name. + * @param fieldNewValue New value. + */ + public static void setField(Object object, String fieldName, Object fieldNewValue) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(object, fieldNewValue); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} From bea7e2ee6e7a7c457f83260015d439c0e1631b4f Mon Sep 17 00:00:00 2001 From: vadimt Date: Mon, 23 Aug 2021 13:57:51 -0700 Subject: [PATCH 05/11] More logging for for Nexus home activity appearing after to-home gesture in L3 Bug: 192018189 Test: presubmit Change-Id: I92cfa9e40f71773e636c700ab7f84df1d9feac19 --- .../android/quickstep/AbsSwipeUpHandler.java | 17 +++++++++++++++++ .../OtherActivityInputConsumer.java | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index fe16b33069..46d58577cd 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -65,6 +65,7 @@ import android.graphics.RectF; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; +import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnApplyWindowInsetsListener; @@ -87,6 +88,7 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.tracing.InputConsumerProto; import com.android.launcher3.tracing.SwipeHandlerProto; import com.android.launcher3.util.TraceHelper; @@ -876,6 +878,9 @@ public abstract class AbsSwipeUpHandler, */ @UiThread public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) { + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TestProtocol.L3_SWIPE_TO_HOME, "3"); + } float flingThreshold = mContext.getResources() .getDimension(R.dimen.quickstep_fling_threshold_speed); boolean isFling = mGestureStarted && !mIsMotionPaused @@ -1037,6 +1042,9 @@ public abstract class AbsSwipeUpHandler, @UiThread private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity, boolean isCancel) { + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TestProtocol.L3_SWIPE_TO_HOME, "4"); + } long duration = MAX_SWIPE_DURATION; float currentShift = mCurrentShift.value; final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity, @@ -1155,6 +1163,9 @@ public abstract class AbsSwipeUpHandler, @UiThread private void animateToProgress(float start, float end, long duration, Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TestProtocol.L3_SWIPE_TO_HOME, "5"); + } runOnRecentsAnimationStart(() -> animateToProgressInternal(start, end, duration, interpolator, target, velocityPxPerMs)); } @@ -1183,6 +1194,9 @@ public abstract class AbsSwipeUpHandler, @UiThread private void animateToProgressInternal(float start, float end, long duration, Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TestProtocol.L3_SWIPE_TO_HOME, "7"); + } maybeUpdateRecentsAttachedState(); // If we are transitioning to launcher, then listen for the activity to be restarted while @@ -1812,6 +1826,9 @@ public abstract class AbsSwipeUpHandler, * be run when it is next started. */ protected void runOnRecentsAnimationStart(Runnable action) { + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TestProtocol.L3_SWIPE_TO_HOME, "6"); + } if (mRecentsAnimationTargets == null) { mRecentsAnimationStartCallbacks.add(action); } else { diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java index 725c7c45a5..e6285f2902 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java @@ -50,6 +50,7 @@ import android.view.ViewConfiguration; import androidx.annotation.UiThread; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.tracing.InputConsumerProto; @@ -356,6 +357,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC } case ACTION_CANCEL: case ACTION_UP: { + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TestProtocol.L3_SWIPE_TO_HOME, "1"); + } if (DEBUG_FAILED_QUICKSWITCH && !mPassedWindowMoveSlop) { float displacementX = mLastPos.x - mDownPos.x; float displacementY = mLastPos.y - mDownPos.y; @@ -409,6 +413,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC * the animation can still be running. */ private void finishTouchTracking(MotionEvent ev) { + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TestProtocol.L3_SWIPE_TO_HOME, "2"); + } Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT, FLAG_CHECK_FOR_RACE_CONDITIONS); From dd7a418335c791b5fa2d615dad14ddc22dbdaa09 Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Fri, 13 Aug 2021 17:03:55 -0700 Subject: [PATCH 06/11] Draw rounded corners above the taskbar - Increase height of taskbar window, but not contentInsets - Draw the inverted corners as part of TaskbarDragLayer#dispatchDraw() (which handles the background already) Test: visual Bug: 196257194 Fixes: 197129604 Change-Id: I517949a4c2b97c92e3bb43408776db6deb1379b4 --- .../taskbar/TaskbarActivityContext.java | 28 ++++++++++++- .../launcher3/taskbar/TaskbarDragLayer.java | 42 ++++++++++++++++++- .../taskbar/TaskbarDragLayerController.java | 6 ++- .../taskbar/TaskbarViewController.java | 5 ++- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index ca574e6a8c..8d7a6dc602 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -36,12 +36,14 @@ import android.view.ContextThemeWrapper; import android.view.Display; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.RoundedCorner; import android.view.View; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; @@ -83,6 +85,7 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ private final TaskbarControllers mControllers; private final WindowManager mWindowManager; + private final @Nullable RoundedCorner mLeftCorner, mRightCorner; private WindowManager.LayoutParams mWindowLayoutParams; private boolean mIsFullscreen; // The size we should return to when we call setTaskbarWindowFullscreen(false) @@ -136,10 +139,12 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ ? windowContext.getApplicationContext() : windowContext.getApplicationContext().createDisplayContext(display); mWindowManager = c.getSystemService(WindowManager.class); + mLeftCorner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); + mRightCorner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); } public void init() { - mLastRequestedNonFullscreenHeight = mDeviceProfile.taskbarSize; + mLastRequestedNonFullscreenHeight = getDefaultTaskbarWindowHeight(); mWindowLayoutParams = new WindowManager.LayoutParams( MATCH_PARENT, mLastRequestedNonFullscreenHeight, @@ -169,6 +174,14 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ return mNavMode == Mode.THREE_BUTTONS; } + public int getLeftCornerRadius() { + return mLeftCorner == null ? 0 : mLeftCorner.getRadius(); + } + + public int getRightCornerRadius() { + return mRightCorner == null ? 0 : mRightCorner.getRadius(); + } + @Override public LayoutInflater getLayoutInflater() { return mLayoutInflater; @@ -249,8 +262,12 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ setTaskbarWindowHeight(fullscreen ? MATCH_PARENT : mLastRequestedNonFullscreenHeight); } + public boolean isTaskbarWindowFullscreen() { + return mIsFullscreen; + } + /** - * Updates the TaskbarContainer height (pass deviceProfile.taskbarSize to reset). + * Updates the TaskbarContainer height (pass {@link #getDefaultTaskbarWindowHeight()} to reset). */ public void setTaskbarWindowHeight(int height) { if (mWindowLayoutParams.height == height || mIsDestroyed) { @@ -270,6 +287,13 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); } + /** + * Returns the default height of the window, including the static corner radii above taskbar. + */ + public int getDefaultTaskbarWindowHeight() { + return mDeviceProfile.taskbarSize + Math.max(getLeftCornerRadius(), getRightCornerRadius()); + } + protected void onTaskbarIconClicked(View view) { Object tag = view.getTag(); if (tag instanceof Task) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java index cd1baf726d..b42a60ceea 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java @@ -18,6 +18,7 @@ package com.android.launcher3.taskbar; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Path; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -40,9 +41,12 @@ import com.android.systemui.shared.system.ViewTreeObserverWrapper.OnComputeInset public class TaskbarDragLayer extends BaseDragLayer { private final Paint mTaskbarBackgroundPaint; + private final Path mInvertedLeftCornerPath, mInvertedRightCornerPath; private final OnComputeInsetsListener mTaskbarInsetsComputer = this::onComputeTaskbarInsets; + // Initialized in init. private TaskbarDragLayerController.TaskbarDragLayerCallbacks mControllerCallbacks; + private float mLeftCornerRadius, mRightCornerRadius; private float mTaskbarBackgroundOffset; @@ -65,10 +69,32 @@ public class TaskbarDragLayer extends BaseDragLayer { mTaskbarBackgroundPaint = new Paint(); mTaskbarBackgroundPaint.setColor(getResources().getColor(R.color.taskbar_background)); mTaskbarBackgroundPaint.setAlpha(0); + mTaskbarBackgroundPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + mTaskbarBackgroundPaint.setStyle(Paint.Style.FILL); + + // Will be set in init(), but this ensures they are always non-null. + mInvertedLeftCornerPath = new Path(); + mInvertedRightCornerPath = new Path(); } public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) { mControllerCallbacks = callbacks; + + // Create the paths for the inverted rounded corners above the taskbar. Start with a filled + // square, and then subtracting out a circle from the appropriate corner. + mLeftCornerRadius = mActivity.getLeftCornerRadius(); + mRightCornerRadius = mActivity.getRightCornerRadius(); + Path square = new Path(); + square.addRect(0, 0, mLeftCornerRadius, mLeftCornerRadius, Path.Direction.CW); + Path circle = new Path(); + circle.addCircle(mLeftCornerRadius, 0, mLeftCornerRadius, Path.Direction.CW); + mInvertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE); + square.reset(); + square.addRect(0, 0, mRightCornerRadius, mRightCornerRadius, Path.Direction.CW); + circle.reset(); + circle.addCircle(0, 0, mRightCornerRadius, Path.Direction.CW); + mInvertedRightCornerPath.op(square, circle, Path.Op.DIFFERENCE); + recreateControllers(); } @@ -121,8 +147,20 @@ public class TaskbarDragLayer extends BaseDragLayer { protected void dispatchDraw(Canvas canvas) { float backgroundHeight = mControllerCallbacks.getTaskbarBackgroundHeight() * (1f - mTaskbarBackgroundOffset); - canvas.drawRect(0, canvas.getHeight() - backgroundHeight, canvas.getWidth(), - canvas.getHeight(), mTaskbarBackgroundPaint); + canvas.save(); + canvas.translate(0, canvas.getHeight() - backgroundHeight); + + // Draw the background behind taskbar content. + canvas.drawRect(0, 0, canvas.getWidth(), backgroundHeight, mTaskbarBackgroundPaint); + + // Draw the inverted rounded corners above the taskbar. + canvas.translate(0, -mLeftCornerRadius); + canvas.drawPath(mInvertedLeftCornerPath, mTaskbarBackgroundPaint); + canvas.translate(0, mLeftCornerRadius); + canvas.translate(canvas.getWidth() - mRightCornerRadius, -mRightCornerRadius); + canvas.drawPath(mInvertedRightCornerPath, mTaskbarBackgroundPaint); + + canvas.restore(); super.dispatchDraw(canvas); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java index 14150b9e25..0afa480667 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java @@ -16,6 +16,7 @@ package com.android.launcher3.taskbar; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_CONTENT; import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_FRAME; import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION; @@ -132,13 +133,14 @@ public class TaskbarDragLayerController { // Let touches pass through us. insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); } else if (mControllers.navbarButtonsViewController.isImeVisible()) { - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME); + insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_CONTENT); } else if (!mControllers.uiController.isTaskbarTouchable()) { // Let touches pass through us. insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); } else if (mControllers.taskbarViewController.areIconsVisible()) { // Buttons are visible, take over the full taskbar area - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME); + insetsInfo.setTouchableInsets(mActivity.isTaskbarWindowFullscreen() + ? TOUCHABLE_INSETS_FRAME : TOUCHABLE_INSETS_CONTENT); } else { insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 6b95f08090..1882762a67 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -165,8 +165,9 @@ public class TaskbarViewController { int offsetY = launcherDp.getTaskbarOffsetY(); setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, LINEAR); - int collapsedHeight = mActivity.getDeviceProfile().taskbarSize; - int expandedHeight = collapsedHeight + offsetY; + int collapsedHeight = mActivity.getDefaultTaskbarWindowHeight(); + int expandedHeight = Math.max(collapsedHeight, + mActivity.getDeviceProfile().taskbarSize + offsetY); setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight( anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight)); From a0cc0903e8295a8febebd771503b74c9d302b14e Mon Sep 17 00:00:00 2001 From: Andy Wickham Date: Wed, 18 Aug 2021 15:17:54 -1000 Subject: [PATCH 07/11] Adds additional pages to Taskbar Edu. Currently there are pages for the following: - Splitscreen - Customize (add apps/predicted apps) - Docking (long press to hide) At the moment they all use the same placeholder image from before. Button states: Page 1: Close and Next Page 2: Back and Next Page 3: Back and Done You can also swipe left and right between the pages. Demo: https://drive.google.com/file/d/1_D3i-jZxCRRVHV92p6hG5cm3VGcJ_bhK/view?usp=sharing&resourcekey=0-KHLHTTx67JlmVv-UZoAUAw Bug: 180605356 Test: Manual Change-Id: Ibbb81610a611f6ca412e53ed90dc1c67764f417e --- quickstep/res/layout/taskbar_edu.xml | 120 ++++++++++++++---- quickstep/res/values/strings.xml | 24 +++- quickstep/res/values/styles.xml | 2 + .../taskbar/TaskbarEduController.java | 28 +++- .../taskbar/TaskbarEduPagedView.java | 79 ++++++++++++ .../launcher3/taskbar/TaskbarEduView.java | 38 +++++- 6 files changed, 260 insertions(+), 31 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/taskbar/TaskbarEduPagedView.java diff --git a/quickstep/res/layout/taskbar_edu.xml b/quickstep/res/layout/taskbar_edu.xml index b7717b71be..ef57a53690 100644 --- a/quickstep/res/layout/taskbar_edu.xml +++ b/quickstep/res/layout/taskbar_edu.xml @@ -17,6 +17,7 @@ - + launcher:pageIndicator="@+id/content_page_indicator"> - + + + + + + + + + + + + + + + + + + + + +