From 5edd864c8ddbd3b05ae049cc40a84d8599535820 Mon Sep 17 00:00:00 2001 From: Jeremy Sim Date: Tue, 7 Feb 2023 13:47:47 +0800 Subject: [PATCH 1/6] Improve resilience of testSplitFromOverview() This patch makes it so that TaplTestsQuickstep#testSplitFromOverview() is less flaky. Previously, this test worked by tapping the center of a Overview tile to confirm split select. However, sometimes the tile would be halfway offscreen, so it would accidentally tap an overlapping UI element (the staged first app) instead, causing the test to fail. Fixed by using getCurrentTask() to always select an Overview tile that is fully onscreen. Fixes: 267794149 Test: Manual on phone, confirmed test passing Change-Id: I8b89509bb53a16e45aea4545562989e2e964de2d --- .../tests/src/com/android/quickstep/TaplTestsQuickstep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java index 9f34775761..bc5fa19ce1 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -191,7 +191,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { mLauncher.goHome().switchToOverview().getCurrentTask() .tapMenu() .tapSplitMenuItem() - .getTestActivityTask(2) + .getCurrentTask() .open(); } From 12c193e6c5130df304ad9dcb68a1c5fec0cac18a Mon Sep 17 00:00:00 2001 From: Brandon Dayauon Date: Mon, 31 Oct 2022 14:28:38 -0700 Subject: [PATCH 2/6] Implement diff haptics going into all apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notes: * the reason why 0.6 -> 1 never happened was because of AllAppsSwipeController where it had a clampToProgress. By changing lowerbound to 0 the progress actually shows fully 0->1 - composed the haptics in the constructor - added new listener class in AATransitionController Added featureflag bug: 233751149 test: Manually - presubmit, ran “make -j7 Launcher3” from master branch photo: https://screenshot.googleplex.com/8r5FZh6buzkQMjk Change-Id: I5e1a24170fdbfdd35b8d8f24af0ec5e8586641a2 --- AndroidManifest-common.xml | 1 + ...ButtonNavbarToOverviewTouchController.java | 8 ++ .../allapps/AllAppsTransitionController.java | 71 +++++++++++- .../launcher3/config/FeatureFlags.java | 2 + .../touch/AllAppsSwipeController.java | 5 +- .../launcher3/util/VibratorWrapper.java | 103 +++++++++++++++++- 6 files changed, 181 insertions(+), 9 deletions(-) diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 951be4e54e..0c7b48fe66 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -40,6 +40,7 @@ + diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java index b5afda388a..df95dc1275 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java @@ -39,6 +39,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.uioverrides.QuickstepLauncher; @@ -62,6 +63,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80; private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f; + private final VibratorWrapper mVibratorWrapper; private final RecentsView mRecentsView; private final MotionPauseDetector mMotionPauseDetector; private final float mMotionPauseMinDisplacement; @@ -82,6 +84,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch mRecentsView = l.getOverviewPanel(); mMotionPauseDetector = new MotionPauseDetector(l); mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop(); + mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext()); } @Override @@ -188,6 +191,11 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch // need to manually set the duration to a reasonable value. animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */)); } + if (FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() && + ((mFromState == NORMAL && mToState == ALL_APPS) + || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) { + mVibratorWrapper.vibrateForDragBump(); + } } private void onMotionPauseDetected() { diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 394a7d7c35..0e143c8d2a 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -32,6 +32,7 @@ import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.util.FloatProperty; import android.view.HapticFeedbackConstants; import android.view.View; @@ -47,17 +48,21 @@ import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.PropertySetter; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.touch.AllAppsSwipeController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.Themes; +import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.views.ScrimView; /** @@ -78,6 +83,8 @@ public class AllAppsTransitionController private static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200; private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f; + private static final float SWIPE_DRAG_COMMIT_THRESHOLD = + 1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL; public static final FloatProperty ALL_APPS_PROGRESS = new FloatProperty("allAppsProgress") { @@ -181,6 +188,7 @@ public class AllAppsTransitionController private boolean mIsTablet; private boolean mHasScaleEffect; + private final VibratorWrapper mVibratorWrapper; public AllAppsTransitionController(Launcher l) { mLauncher = l; @@ -193,6 +201,7 @@ public class AllAppsTransitionController setShiftRange(dp.allAppsShiftRange); mLauncher.addOnDeviceProfileChangeListener(this); + mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext()); } public float getShiftRange() { @@ -304,6 +313,11 @@ public class AllAppsTransitionController /** * Creates an animation which updates the vertical transition progress and updates all the * dependent UI using various animation events + * + * This method also dictates where along the progress the haptics should be played. As the user + * scrolls up from workspace or down from AllApps, a drag haptic is being played until the + * commit point where it plays a commit haptic. Where we play the haptics differs when going + * from workspace -> allApps and vice versa. */ @Override public void setStateWithAnimation(LauncherState toState, @@ -332,6 +346,20 @@ public class AllAppsTransitionController }); } + if(FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() && config.userControlled + && Utilities.ATLEAST_S) { + if (toState == ALL_APPS) { + builder.addOnFrameListener( + new VibrationAnimatorUpdateListener(this, mVibratorWrapper, + SWIPE_DRAG_COMMIT_THRESHOLD, 1)); + } else { + builder.addOnFrameListener( + new VibrationAnimatorUpdateListener(this, mVibratorWrapper, + 0, SWIPE_DRAG_COMMIT_THRESHOLD)); + } + builder.addEndListener(mVibratorWrapper::cancelVibrate); + } + float targetProgress = toState.getVerticalProgress(mLauncher); if (Float.compare(mProgress, targetProgress) == 0) { setAlphas(toState, config, builder); @@ -349,7 +377,7 @@ public class AllAppsTransitionController setAlphas(toState, config, builder); - if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) { + if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) && !(Utilities.ATLEAST_S)) { mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); } @@ -487,4 +515,45 @@ public class AllAppsTransitionController } } } + + /** + * This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start + * threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller + * as it gives an accurate progress that dictates where the vibrator should vibrate. + * Note: once the user begins a gesture and does the commit haptic, there should not be anymore + * haptics played for that gesture. + */ + private static class VibrationAnimatorUpdateListener implements + ValueAnimator.AnimatorUpdateListener { + private final VibratorWrapper mVibratorWrapper; + private final AllAppsTransitionController mController; + private final float mStartThreshold; + private final float mEndThreshold; + private boolean mHasCommitted; + + VibrationAnimatorUpdateListener(AllAppsTransitionController controller, + VibratorWrapper vibratorWrapper, float startThreshold, + float endThreshold) { + mController = controller; + mVibratorWrapper = vibratorWrapper; + mStartThreshold = startThreshold; + mEndThreshold = endThreshold; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (mHasCommitted) { + return; + } + float currentProgress = + AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController); + if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) { + mVibratorWrapper.vibrateForDragTexture(); + } else if (!(currentProgress == 0 || currentProgress == 1)) { + // This check guards against committing at the location of the start of the gesture + mVibratorWrapper.vibrateForDragCommit(); + mHasCommitted = true; + } + } + } } diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 4f4e65ca2b..f78326dac5 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -375,6 +375,8 @@ public final class FeatureFlags { "ENABLE_LAUNCH_FROM_STAGED_APP", true, "Enable the ability to tap a staged app during split select to launch it in full screen" ); + public static final BooleanFlag ENABLE_HAPTICS_ALL_APPS = getDebugFlag( + "ENABLE_HAPTICS_ALL_APPS", false, "Enables haptics opening/closing All apps"); public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag( "ENABLE_FORCED_MONO_ICON", false, diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java index bfd0e1b74b..a53751fcb8 100644 --- a/src/com/android/launcher3/touch/AllAppsSwipeController.java +++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java @@ -129,10 +129,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { Interpolators.clampToProgress( Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1f), ALL_APPS_STATE_TRANSITION_ATOMIC, 1f); - public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = - Interpolators.clampToProgress( - Interpolators.mapToProgress(LINEAR, ALL_APPS_STATE_TRANSITION_MANUAL, 1f), - ALL_APPS_STATE_TRANSITION_MANUAL, 1f); + public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = LINEAR; // -------- diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java index 932bcfc292..ceba0db384 100644 --- a/src/com/android/launcher3/util/VibratorWrapper.java +++ b/src/com/android/launcher3/util/VibratorWrapper.java @@ -17,7 +17,6 @@ package com.android.launcher3.util; import static android.os.VibrationEffect.createPredefined; import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED; - import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; @@ -28,12 +27,17 @@ import android.content.Context; import android.database.ContentObserver; import android.media.AudioAttributes; import android.os.Build; +import android.os.SystemClock; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; +import androidx.annotation.Nullable; + import com.android.launcher3.Utilities; -import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.anim.PendingAnimation; + +import java.util.function.Consumer; /** * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. @@ -52,6 +56,21 @@ public class VibratorWrapper { public static final VibrationEffect EFFECT_CLICK = createPredefined(VibrationEffect.EFFECT_CLICK); + private static final float DRAG_TEXTURE_SCALE = 0.03f; + private static final float DRAG_COMMIT_SCALE = 0.5f; + private static final float DRAG_BUMP_SCALE = 0.4f; + private static final int DRAG_TEXTURE_EFFECT_SIZE = 200; + + @Nullable + private final VibrationEffect mDragEffect; + @Nullable + private final VibrationEffect mCommitEffect; + @Nullable + private final VibrationEffect mBumpEffect; + + private long mLastDragTime; + private final int mThresholdUntilNextDragCallMillis; + /** * Haptic when entering overview. */ @@ -62,7 +81,7 @@ public class VibratorWrapper { private boolean mIsHapticFeedbackEnabled; - public VibratorWrapper(Context context) { + private VibratorWrapper(Context context) { mVibrator = context.getSystemService(Vibrator.class); mHasVibrator = mVibrator.hasVibrator(); if (mHasVibrator) { @@ -75,12 +94,88 @@ public class VibratorWrapper { } }; resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED), - false /* notifyForDescendents */, observer); + false /* notifyForDescendants */, observer); } else { mIsHapticFeedbackEnabled = false; } + + if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK)) { + + // Drag texture, Commit, and Bump should only be used for premium phones. + // Before using these haptics make sure check if the device can use it + VibrationEffect.Composition dragEffect = VibrationEffect.startComposition(); + for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) { + dragEffect.addPrimitive( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE); + } + mDragEffect = dragEffect.compose(); + mCommitEffect = VibrationEffect.startComposition().addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose(); + mBumpEffect = VibrationEffect.startComposition().addPrimitive( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose(); + int primitiveDuration = mVibrator.getPrimitiveDurations( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0]; + + mThresholdUntilNextDragCallMillis = + DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100; + } else { + mDragEffect = null; + mCommitEffect = null; + mBumpEffect = null; + mThresholdUntilNextDragCallMillis = 0; + } } + /** + * This is called when the user swipes to/from all apps. This is meant to be used in between + * long animation progresses so that it gives a dragging texture effect. For a better + * experience, this should be used in combination with vibrateForDragCommit(). + */ + public void vibrateForDragTexture() { + if (mDragEffect == null) { + return; + } + long currentTime = SystemClock.elapsedRealtime(); + long elapsedTimeSinceDrag = currentTime - mLastDragTime; + if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) { + vibrate(mDragEffect); + mLastDragTime = currentTime; + } + } + + /** + * This is used when user reaches the commit threshold when swiping to/from from all apps. + */ + public void vibrateForDragCommit() { + if (mCommitEffect != null) { + vibrate(mCommitEffect); + } + // resetting dragTexture timestamp to be able to play dragTexture again + mLastDragTime = 0; + } + + /** + * The bump haptic is used to be called at the end of a swipe and only if it the gesture is a + * FLING going to/from all apps. Client can just call this method elsewhere just for the + * effect. + */ + public void vibrateForDragBump() { + if (mBumpEffect != null) { + vibrate(mBumpEffect); + } + } + + /** + * This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For + * example, when no animation is happening but a vibrator happens to be vibrating still. Need + * boolean parameter for {@link PendingAnimation#addEndListener(Consumer)}. + */ + public void cancelVibrate(boolean unused) { + UI_HELPER_EXECUTOR.execute(mVibrator::cancel); + // reset dragTexture timestamp to be able to play dragTexture again whenever cancelled + mLastDragTime = 0; + } private boolean isHapticFeedbackEnabled(ContentResolver resolver) { return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1; } From 1a2d4bd6f4c466bc1f38fe761f3cea4d0d090e88 Mon Sep 17 00:00:00 2001 From: Thales Lima Date: Mon, 28 Nov 2022 13:08:32 +0000 Subject: [PATCH 3/6] Create an XML parser for WorkspaceSpecs Extract DeviceProfileTest to Launcher3 so it can be used in other tests as well, and change name of previous base test to be more descriptive. Bug: 241386436 Test: WorkspaceSpecsTest Change-Id: I64613bb5a23c374ed15fb6d936192236a541ab9b --- .../quickstep/FullscreenDrawParamsTest.kt | 4 +- .../quickstep/HotseatWidthCalculationTest.kt | 4 +- res/values/attrs.xml | 15 + .../android/launcher3/util/ResourceHelper.kt | 37 +++ .../launcher3/workspace/WorkspaceSpecs.kt | 252 ++++++++++++++++ tests/res/values/attrs.xml | 35 +++ .../res/xml/invalid_workspace_file_case_1.xml | 56 ++++ .../res/xml/invalid_workspace_file_case_2.xml | 59 ++++ .../res/xml/invalid_workspace_file_case_3.xml | 58 ++++ tests/res/xml/valid_workspace_file.xml | 57 ++++ .../launcher3/AbstractDeviceProfileTest.kt | 272 ++++++++++++++++++ ...t.kt => FakeInvariantDeviceProfileTest.kt} | 8 +- .../HotseatWidthCalculationTest.kt | 4 +- .../launcher3/util/TestResourceHelper.kt | 34 +++ .../launcher3/workspace/WorkspaceSpecsTest.kt | 120 ++++++++ 15 files changed, 1008 insertions(+), 7 deletions(-) create mode 100644 src/com/android/launcher3/util/ResourceHelper.kt create mode 100644 src/com/android/launcher3/workspace/WorkspaceSpecs.kt create mode 100644 tests/res/values/attrs.xml create mode 100644 tests/res/xml/invalid_workspace_file_case_1.xml create mode 100644 tests/res/xml/invalid_workspace_file_case_2.xml create mode 100644 tests/res/xml/invalid_workspace_file_case_3.xml create mode 100644 tests/res/xml/valid_workspace_file.xml create mode 100644 tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt rename tests/src/com/android/launcher3/{DeviceProfileBaseTest.kt => FakeInvariantDeviceProfileTest.kt} (97%) create mode 100644 tests/src/com/android/launcher3/util/TestResourceHelper.kt create mode 100644 tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt index 9afd893894..bc1b87deb0 100644 --- a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt +++ b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt @@ -19,7 +19,7 @@ import android.graphics.Rect import android.graphics.RectF import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.launcher3.DeviceProfileBaseTest +import com.android.launcher3.FakeInvariantDeviceProfileTest import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT import com.android.quickstep.views.TaskView.FullscreenDrawParams @@ -36,7 +36,7 @@ import org.mockito.Mockito.mock /** Test for FullscreenDrawParams class. */ @SmallTest @RunWith(AndroidJUnit4::class) -class FullscreenDrawParamsTest : DeviceProfileBaseTest() { +class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { private val TASK_SCALE = 0.7f private var mThumbnailData: ThumbnailData = mock(ThumbnailData::class.java) diff --git a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt index adbca32fba..a347156769 100644 --- a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt +++ b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt @@ -18,7 +18,7 @@ package com.android.quickstep import android.graphics.Rect import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.launcher3.DeviceProfileBaseTest +import com.android.launcher3.FakeInvariantDeviceProfileTest import com.android.launcher3.util.WindowBounds import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -26,7 +26,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -class HotseatWidthCalculationTest : DeviceProfileBaseTest() { +class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { /** * This is a case when after setting the hotseat, the space needs to be recalculated but it diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 26180f345f..c3bd90e529 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -224,6 +224,21 @@ + + + + + + + + + + + + + + + diff --git a/src/com/android/launcher3/util/ResourceHelper.kt b/src/com/android/launcher3/util/ResourceHelper.kt new file mode 100644 index 0000000000..0ca788840f --- /dev/null +++ b/src/com/android/launcher3/util/ResourceHelper.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 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 android.content.Context +import android.content.res.TypedArray +import android.content.res.XmlResourceParser +import android.util.AttributeSet +import kotlin.IntArray + +/** + * This class is a helper that can be subclassed in tests to provide a way to parse attributes + * correctly. + */ +open class ResourceHelper(private val context: Context, private val specsFileId: Int) { + open fun getXml(): XmlResourceParser { + return context.resources.getXml(specsFileId) + } + + open fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray { + return context.obtainStyledAttributes(attrs, styleId) + } +} diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt new file mode 100644 index 0000000000..0f6e1b0af1 --- /dev/null +++ b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2023 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.workspace + +import android.content.res.TypedArray +import android.content.res.XmlResourceParser +import android.util.AttributeSet +import android.util.Log +import android.util.TypedValue +import android.util.Xml +import com.android.launcher3.R +import com.android.launcher3.util.ResourceHelper +import java.io.IOException +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException + +private const val TAG = "WorkspaceSpecs" + +class WorkspaceSpecs(resourceHelper: ResourceHelper) { + object XmlTags { + const val WORKSPACE_SPECS = "workspaceSpecs" + + const val WORKSPACE_SPEC = "workspaceSpec" + const val START_PADDING = "startPadding" + const val END_PADDING = "endPadding" + const val GUTTER = "gutter" + const val CELL_SIZE = "cellSize" + } + + val workspaceHeightSpecList = mutableListOf() + val workspaceWidthSpecList = mutableListOf() + + init { + try { + val parser: XmlResourceParser = resourceHelper.getXml() + val depth = parser.depth + var type: Int + while ( + (parser.next().also { type = it } != XmlPullParser.END_TAG || + parser.depth > depth) && type != XmlPullParser.END_DOCUMENT + ) { + if (type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPECS == parser.name) { + val displayDepth = parser.depth + while ( + (parser.next().also { type = it } != XmlPullParser.END_TAG || + parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT + ) { + if ( + type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPEC == parser.name + ) { + val attrs = + resourceHelper.obtainStyledAttributes( + Xml.asAttributeSet(parser), + R.styleable.WorkspaceSpec + ) + val maxAvailableSize = + attrs.getDimensionPixelSize( + R.styleable.WorkspaceSpec_maxAvailableSize, + 0 + ) + val specType = + WorkspaceSpec.SpecType.values()[ + attrs.getInt( + R.styleable.WorkspaceSpec_specType, + WorkspaceSpec.SpecType.HEIGHT.ordinal + ) + ] + attrs.recycle() + + var startPadding: SizeSpec? = null + var endPadding: SizeSpec? = null + var gutter: SizeSpec? = null + var cellSize: SizeSpec? = null + + val limitDepth = parser.depth + while ( + (parser.next().also { type = it } != XmlPullParser.END_TAG || + parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT + ) { + val attr: AttributeSet = Xml.asAttributeSet(parser) + if (type == XmlPullParser.START_TAG) { + when (parser.name) { + XmlTags.START_PADDING -> { + startPadding = SizeSpec(resourceHelper, attr) + } + XmlTags.END_PADDING -> { + endPadding = SizeSpec(resourceHelper, attr) + } + XmlTags.GUTTER -> { + gutter = SizeSpec(resourceHelper, attr) + } + XmlTags.CELL_SIZE -> { + cellSize = SizeSpec(resourceHelper, attr) + } + } + } + } + + if ( + startPadding == null || + endPadding == null || + gutter == null || + cellSize == null + ) { + throw IllegalStateException( + "All attributes in workspaceSpec must be defined" + ) + } + + val workspaceSpec = + WorkspaceSpec( + maxAvailableSize, + specType, + startPadding, + endPadding, + gutter, + cellSize + ) + if (workspaceSpec.isValid()) { + if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) + workspaceHeightSpecList.add(workspaceSpec) + else workspaceWidthSpecList.add(workspaceSpec) + } else { + throw IllegalStateException("Invalid workspaceSpec found.") + } + } + } + + if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) { + throw IllegalStateException( + "WorkspaceSpecs is incomplete - " + + "height list size = ${workspaceHeightSpecList.size}; " + + "width list size = ${workspaceWidthSpecList.size}." + ) + } + } + } + parser.close() + } catch (e: Exception) { + when (e) { + is IOException, + is XmlPullParserException -> { + throw RuntimeException("Failure parsing workspaces specs file.", e) + } + else -> throw e + } + } + } +} + +data class WorkspaceSpec( + val maxAvailableSize: Int, + val specType: SpecType, + val startPadding: SizeSpec, + val endPadding: SizeSpec, + val gutter: SizeSpec, + val cellSize: SizeSpec +) { + + enum class SpecType { + HEIGHT, + WIDTH + } + + fun isValid(): Boolean { + if (maxAvailableSize <= 0) { + Log.e(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0") + return false + } + + // All specs need to be individually valid + if (!allSpecsAreValid()) { + Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()") + return false + } + + return true + } + + private fun allSpecsAreValid(): Boolean = + startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid() +} + +class SizeSpec(resourceHelper: ResourceHelper, attrs: AttributeSet) { + val fixedSize: Float + val ofAvailableSpace: Float + val ofRemainderSpace: Float + + init { + val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SpecSize) + + fixedSize = getValue(styledAttrs, R.styleable.SpecSize_fixedSize) + ofAvailableSpace = getValue(styledAttrs, R.styleable.SpecSize_ofAvailableSpace) + ofRemainderSpace = getValue(styledAttrs, R.styleable.SpecSize_ofRemainderSpace) + + styledAttrs.recycle() + } + + private fun getValue(a: TypedArray, index: Int): Float { + if (a.getType(index) == TypedValue.TYPE_DIMENSION) { + return a.getDimensionPixelSize(index, 0).toFloat() + } else if (a.getType(index) == TypedValue.TYPE_FLOAT) { + return a.getFloat(index, 0f) + } + return 0f + } + + fun isValid(): Boolean { + // All attributes are empty + if (fixedSize <= 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f) { + Log.e(TAG, "SizeSpec#isValid - all attributes are empty") + return false + } + + // More than one attribute is filled + val attrCount = + (if (fixedSize > 0) 1 else 0) + + (if (ofAvailableSpace > 0) 1 else 0) + + (if (ofRemainderSpace > 0) 1 else 0) + if (attrCount > 1) { + Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled") + return false + } + + // Values should be between 0 and 1 + if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) { + Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1") + return false + } + + return true + } + + override fun toString(): String { + return "SizeSpec(fixedSize=$fixedSize, ofAvailableSpace=$ofAvailableSpace, " + + "ofRemainderSpace=$ofRemainderSpace)" + } +} diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml new file mode 100644 index 0000000000..2310d9ef66 --- /dev/null +++ b/tests/res/values/attrs.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/invalid_workspace_file_case_1.xml b/tests/res/xml/invalid_workspace_file_case_1.xml new file mode 100644 index 0000000000..0be704bd8f --- /dev/null +++ b/tests/res/xml/invalid_workspace_file_case_1.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/invalid_workspace_file_case_2.xml b/tests/res/xml/invalid_workspace_file_case_2.xml new file mode 100644 index 0000000000..5a37d97ba2 --- /dev/null +++ b/tests/res/xml/invalid_workspace_file_case_2.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/invalid_workspace_file_case_3.xml b/tests/res/xml/invalid_workspace_file_case_3.xml new file mode 100644 index 0000000000..3e68edb15d --- /dev/null +++ b/tests/res/xml/invalid_workspace_file_case_3.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/valid_workspace_file.xml b/tests/res/xml/valid_workspace_file.xml new file mode 100644 index 0000000000..91a3e48d21 --- /dev/null +++ b/tests/res/xml/valid_workspace_file.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt new file mode 100644 index 0000000000..dcc669bc38 --- /dev/null +++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2023 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 + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Point +import android.graphics.Rect +import android.util.DisplayMetrics +import android.view.Surface +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.NavigationMode +import com.android.launcher3.util.WindowBounds +import com.android.launcher3.util.window.CachedDisplayInfo +import com.android.launcher3.util.window.WindowManagerProxy +import kotlin.math.max +import kotlin.math.min +import org.junit.After +import org.junit.Before +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` as whenever + +/** + * This is an abstract class for DeviceProfile tests that create an InvariantDeviceProfile based on + * a real device spec. + * + * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest] + */ +abstract class AbstractDeviceProfileTest { + protected var context: Context? = null + protected open val runningContext: Context = ApplicationProvider.getApplicationContext() + private var displayController: DisplayController = mock(DisplayController::class.java) + private var windowManagerProxy: WindowManagerProxy = mock(WindowManagerProxy::class.java) + private lateinit var originalDisplayController: DisplayController + private lateinit var originalWindowManagerProxy: WindowManagerProxy + + @Before + fun setUp() { + val appContext: Context = ApplicationProvider.getApplicationContext() + originalWindowManagerProxy = WindowManagerProxy.INSTANCE.get(appContext) + originalDisplayController = DisplayController.INSTANCE.get(appContext) + WindowManagerProxy.INSTANCE.initializeForTesting(windowManagerProxy) + DisplayController.INSTANCE.initializeForTesting(displayController) + } + + @After + fun tearDown() { + WindowManagerProxy.INSTANCE.initializeForTesting(originalWindowManagerProxy) + DisplayController.INSTANCE.initializeForTesting(originalDisplayController) + } + + class DeviceSpec( + val naturalSize: Pair, + val densityDpi: Int, + val statusBarNaturalPx: Int, + val statusBarRotatedPx: Int, + val gesturePx: Int, + val cutoutPx: Int + ) + + open val deviceSpecs = + mapOf( + "phone" to + DeviceSpec( + Pair(1080, 2400), + densityDpi = 420, + statusBarNaturalPx = 118, + statusBarRotatedPx = 74, + gesturePx = 63, + cutoutPx = 118 + ), + "tablet" to + DeviceSpec( + Pair(2560, 1600), + densityDpi = 320, + statusBarNaturalPx = 104, + statusBarRotatedPx = 104, + gesturePx = 0, + cutoutPx = 0 + ), + ) + + protected fun initializeVarsForPhone( + deviceSpec: DeviceSpec, + isGestureMode: Boolean = true, + isVerticalBar: Boolean = false + ) { + val (naturalX, naturalY) = deviceSpec.naturalSize + val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY) + val displayInfo = + CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0)) + val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) + + initializeCommonVars( + perDisplayBoundsCache, + displayInfo, + rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0, + isGestureMode, + densityDpi = deviceSpec.densityDpi + ) + } + + protected fun initializeVarsForTablet( + deviceSpec: DeviceSpec, + isLandscape: Boolean = false, + isGestureMode: Boolean = true + ) { + val (naturalX, naturalY) = deviceSpec.naturalSize + val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY) + val displayInfo = + CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0)) + val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) + + initializeCommonVars( + perDisplayBoundsCache, + displayInfo, + rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, + isGestureMode, + densityDpi = deviceSpec.densityDpi + ) + } + + protected fun initializeVarsForTwoPanel( + deviceTabletSpec: DeviceSpec, + deviceSpec: DeviceSpec, + isLandscape: Boolean = false, + isGestureMode: Boolean = true + ) { + val (tabletNaturalX, tabletNaturalY) = deviceTabletSpec.naturalSize + val tabletWindowsBounds = + tabletWindowsBounds(deviceTabletSpec, tabletNaturalX, tabletNaturalY) + val tabletDisplayInfo = + CachedDisplayInfo( + Point(tabletNaturalX, tabletNaturalY), + Surface.ROTATION_0, + Rect(0, 0, 0, 0) + ) + + val (phoneNaturalX, phoneNaturalY) = deviceSpec.naturalSize + val phoneWindowsBounds = + phoneWindowsBounds(deviceSpec, isGestureMode, phoneNaturalX, phoneNaturalY) + val phoneDisplayInfo = + CachedDisplayInfo( + Point(phoneNaturalX, phoneNaturalY), + Surface.ROTATION_0, + Rect(0, 0, 0, 0) + ) + + val perDisplayBoundsCache = + mapOf(tabletDisplayInfo to tabletWindowsBounds, phoneDisplayInfo to phoneWindowsBounds) + + initializeCommonVars( + perDisplayBoundsCache, + tabletDisplayInfo, + rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, + isGestureMode, + densityDpi = deviceTabletSpec.densityDpi + ) + } + + private fun phoneWindowsBounds( + deviceSpec: DeviceSpec, + isGestureMode: Boolean, + naturalX: Int, + naturalY: Int + ): Array { + val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi) + + val rotation0Insets = + Rect( + 0, + max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx), + 0, + if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight + ) + val rotation90Insets = + Rect( + deviceSpec.cutoutPx, + deviceSpec.statusBarRotatedPx, + if (isGestureMode) 0 else buttonsNavHeight, + if (isGestureMode) deviceSpec.gesturePx else 0 + ) + val rotation180Insets = + Rect( + 0, + deviceSpec.statusBarNaturalPx, + 0, + max( + if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight, + deviceSpec.cutoutPx + ) + ) + val rotation270Insets = + Rect( + if (isGestureMode) 0 else buttonsNavHeight, + deviceSpec.statusBarRotatedPx, + deviceSpec.cutoutPx, + if (isGestureMode) deviceSpec.gesturePx else 0 + ) + + return arrayOf( + WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0), + WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90), + WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180), + WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270) + ) + } + + private fun tabletWindowsBounds( + deviceSpec: DeviceSpec, + naturalX: Int, + naturalY: Int + ): Array { + val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0) + val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0) + + return arrayOf( + WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0), + WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90), + WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180), + WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270) + ) + } + + private fun initializeCommonVars( + perDisplayBoundsCache: Map>, + displayInfo: CachedDisplayInfo, + rotation: Int, + isGestureMode: Boolean = true, + densityDpi: Int + ) { + val windowsBounds = perDisplayBoundsCache[displayInfo]!! + val realBounds = windowsBounds[rotation] + whenever(windowManagerProxy.getDisplayInfo(ArgumentMatchers.any())).thenReturn(displayInfo) + whenever(windowManagerProxy.getRealBounds(ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(realBounds) + whenever(windowManagerProxy.getRotation(ArgumentMatchers.any())).thenReturn(rotation) + whenever(windowManagerProxy.getNavigationMode(ArgumentMatchers.any())) + .thenReturn( + if (isGestureMode) NavigationMode.NO_BUTTON else NavigationMode.THREE_BUTTONS + ) + + val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat() + val config = + Configuration(runningContext.resources.configuration).apply { + this.densityDpi = densityDpi + screenWidthDp = (realBounds.bounds.width() / density).toInt() + screenHeightDp = (realBounds.bounds.height() / density).toInt() + smallestScreenWidthDp = min(screenWidthDp, screenHeightDp) + } + context = runningContext.createConfigurationContext(config) + + val info = DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache) + whenever(displayController.info).thenReturn(info) + whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode) + } +} diff --git a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt similarity index 97% rename from tests/src/com/android/launcher3/DeviceProfileBaseTest.kt rename to tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt index daf4608df7..a5f33c0338 100644 --- a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt +++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt @@ -31,7 +31,13 @@ import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever -abstract class DeviceProfileBaseTest { +/** + * This is an abstract class for DeviceProfile tests that don't need the real Context and mock an + * InvariantDeviceProfile instead of creating one based on real values. + * + * For an implementation that creates InvariantDeviceProfile, use [AbstractDeviceProfileTest] + */ +abstract class FakeInvariantDeviceProfileTest { protected var context: Context? = null protected var inv: InvariantDeviceProfile? = null diff --git a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt index 951f5f86fa..2a27487e3b 100644 --- a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt +++ b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt @@ -18,7 +18,7 @@ package com.android.launcher3.nonquickstep import android.graphics.Rect import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.launcher3.DeviceProfileBaseTest +import com.android.launcher3.FakeInvariantDeviceProfileTest import com.android.launcher3.util.WindowBounds import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -26,7 +26,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -class HotseatWidthCalculationTest : DeviceProfileBaseTest() { +class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { /** * This is a case when after setting the hotseat, the space needs to be recalculated but it diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt new file mode 100644 index 0000000000..fb03fe1b53 --- /dev/null +++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 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 android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import com.android.launcher3.R +import com.android.launcher3.tests.R as TestR +import kotlin.IntArray + +class TestResourceHelper(private val context: Context, private val specsFileId: Int) : + ResourceHelper(context, specsFileId) { + override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray { + var clone = styleId.clone() + if (styleId == R.styleable.SpecSize) clone = TestR.styleable.SpecSize + else if (styleId == R.styleable.WorkspaceSpec) clone = TestR.styleable.WorkspaceSpec + return context.obtainStyledAttributes(attrs, clone) + } +} diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt new file mode 100644 index 0000000000..0fd8a5460e --- /dev/null +++ b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 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.workspace + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.AbstractDeviceProfileTest +import com.android.launcher3.tests.R as TestR +import com.android.launcher3.util.TestResourceHelper +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class WorkspaceSpecsTest : AbstractDeviceProfileTest() { + override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context + + @Before + fun setup() { + initializeVarsForPhone(deviceSpecs["phone"]!!) + } + + @Test + fun parseValidFile() { + val workspaceSpecs = + WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file)) + assertThat(workspaceSpecs.workspaceHeightSpecList.size).isEqualTo(2) + assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString()) + .isEqualTo( + "WorkspaceSpec(" + + "maxAvailableSize=1701, " + + "specType=HEIGHT, " + + "startPadding=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.0125, " + + "ofRemainderSpace=0.0), " + + "endPadding=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.05, " + + "ofRemainderSpace=0.0), " + + "gutter=SizeSpec(fixedSize=42.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.0), " + + "cellSize=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.2)" + + ")" + ) + assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString()) + .isEqualTo( + "WorkspaceSpec(" + + "maxAvailableSize=26247, " + + "specType=HEIGHT, " + + "startPadding=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.0306, " + + "ofRemainderSpace=0.0), " + + "endPadding=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.068, " + + "ofRemainderSpace=0.0), " + + "gutter=SizeSpec(fixedSize=42.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.0), " + + "cellSize=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.2)" + + ")" + ) + assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1) + assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString()) + .isEqualTo( + "WorkspaceSpec(" + + "maxAvailableSize=26247, " + + "specType=WIDTH, " + + "startPadding=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.21436226), " + + "endPadding=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.21436226), " + + "gutter=SizeSpec(fixedSize=0.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.11425509), " + + "cellSize=SizeSpec(fixedSize=315.0, " + + "ofAvailableSpace=0.0, " + + "ofRemainderSpace=0.0)" + + ")" + ) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_missingTag_throwsError() { + WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1)) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_moreThanOneValuePerTag_throwsError() { + WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2)) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_valueBiggerThan1_throwsError() { + WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3)) + } +} From 97ef7f005273d83b8fe52bfad94e0396b639aaaa Mon Sep 17 00:00:00 2001 From: Pat Manning Date: Tue, 7 Feb 2023 15:49:08 +0000 Subject: [PATCH 4/6] Refresh thumbnail splash when task icon changes. This is usually a result of TaskIconCache updating in the background. Fix: 267744363 Test: manual. Change-Id: I83620f3774def7ffb07906a6d45b64e9aad6de71 --- .../android/quickstep/views/GroupedTaskView.java | 6 ++++++ .../quickstep/views/LauncherRecentsView.java | 1 + .../com/android/quickstep/views/RecentsView.java | 8 ++++++++ .../android/quickstep/views/TaskThumbnailView.java | 14 +++++++++++--- .../src/com/android/quickstep/views/TaskView.java | 4 ++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java index 3f7d677970..5cf79ea8e2 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java @@ -355,6 +355,12 @@ public class GroupedTaskView extends TaskView { mSnapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha); } + @Override + protected void refreshTaskThumbnailSplash() { + super.refreshTaskThumbnailSplash(); + mSnapshotView2.refreshSplashView(); + } + /** * Sets visibility for thumbnails and associated elements (DWB banners). * IconView is unaffected. diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index 886fc80a22..c6dc15a9fd 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -92,6 +92,7 @@ public class LauncherRecentsView extends RecentsView Date: Tue, 7 Feb 2023 17:44:25 -0800 Subject: [PATCH 5/6] Fix foldable single page bug I did a last minute change and uploaded the wrong branch. Bug: 268356439 Change-Id: Ia11ce681d57b7a637c01b2155b16486cf5fd648f --- src/com/android/launcher3/Workspace.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index fa54dcfc59..cfb8ca4df5 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -501,7 +501,7 @@ public class Workspace extends PagedView } private boolean isTwoPanelEnabled() { - return mLauncher.mDeviceProfile.isTwoPanels; + return !FOLDABLE_SINGLE_PAGE.get() && mLauncher.mDeviceProfile.isTwoPanels; } @Override @@ -663,8 +663,9 @@ public class Workspace extends PagedView // Inflate the cell layout, but do not add it automatically so that we can get the newly // created CellLayout. + DeviceProfile dp = mLauncher.getDeviceProfile(); CellLayout newScreen; - if (FOLDABLE_SINGLE_PAGE.get() && isTwoPanelEnabled()) { + if (FOLDABLE_SINGLE_PAGE.get() && dp.isTwoPanels) { newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate( R.layout.workspace_screen_foldable, this, false /* attachToRoot */); } else { From c65f37e7c6b7cb7df8911f6c97dcd68cef98513e Mon Sep 17 00:00:00 2001 From: Saumya Prakash Date: Wed, 8 Feb 2023 00:18:13 +0000 Subject: [PATCH 6/6] Migrate from using CardViews to Views in gesture nav tutorial layouts. Use Views in layouts for the gesture navigation tutorial instead of CardViews. We only migrate the classes that will end up being used in the redesigned gesture tutorial. Fix: 268259319 Test: Manual Change-Id: I510c8b3f840d0267dd6dfca573cc69c4996d525c --- quickstep/res/drawable/hotseat_icon.xml | 21 +++++++++ quickstep/res/drawable/hotseat_icon_home.xml | 21 +++++++++ quickstep/res/drawable/hotseat_search_bar.xml | 21 +++++++++ quickstep/res/drawable/top_task_view.xml | 21 +++++++++ .../gesture_tutorial_mock_hotseat.xml | 28 ++++++------ .../gesture_tutorial_tablet_mock_hotseat.xml | 43 ++++++++----------- .../res/layout/gesture_tutorial_fragment.xml | 23 +++++----- .../layout/gesture_tutorial_mock_hotseat.xml | 35 +++++++-------- .../gesture_tutorial_tablet_mock_hotseat.xml | 42 ++++++++---------- .../redesigned_gesture_tutorial_fragment.xml | 26 +++++------ ...designed_gesture_tutorial_mock_hotseat.xml | 38 +++++++--------- .../interaction/AnimatedTaskView.java | 7 +-- .../HomeGestureTutorialController.java | 8 ++-- .../interaction/TutorialController.java | 1 - 14 files changed, 194 insertions(+), 141 deletions(-) create mode 100644 quickstep/res/drawable/hotseat_icon.xml create mode 100644 quickstep/res/drawable/hotseat_icon_home.xml create mode 100644 quickstep/res/drawable/hotseat_search_bar.xml create mode 100644 quickstep/res/drawable/top_task_view.xml diff --git a/quickstep/res/drawable/hotseat_icon.xml b/quickstep/res/drawable/hotseat_icon.xml new file mode 100644 index 0000000000..b849fe90ce --- /dev/null +++ b/quickstep/res/drawable/hotseat_icon.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/hotseat_icon_home.xml b/quickstep/res/drawable/hotseat_icon_home.xml new file mode 100644 index 0000000000..d59dd4a88e --- /dev/null +++ b/quickstep/res/drawable/hotseat_icon_home.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/hotseat_search_bar.xml b/quickstep/res/drawable/hotseat_search_bar.xml new file mode 100644 index 0000000000..ea332e9462 --- /dev/null +++ b/quickstep/res/drawable/hotseat_search_bar.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/top_task_view.xml b/quickstep/res/drawable/top_task_view.xml new file mode 100644 index 0000000000..d2176c3cb4 --- /dev/null +++ b/quickstep/res/drawable/top_task_view.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml b/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml index 1e2e014111..c7e176ac2e 100644 --- a/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml +++ b/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml @@ -24,54 +24,50 @@ android:paddingStart="56dp" android:paddingEnd="56dp"> - - - - - - - - - - - + app:layout_constraintTop_toTopOf="@id/full_task_view" /> - + app:layout_constraintTop_toBottomOf="@id/top_task_view" /> diff --git a/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml b/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml index 8513dcf3d7..8ee0339e9c 100644 --- a/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml +++ b/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml @@ -8,67 +8,62 @@ android:paddingStart="26dp" android:paddingEnd="26dp"> - - - - - diff --git a/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml b/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml index 363f14e481..63c51e8040 100644 --- a/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml +++ b/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml @@ -22,84 +22,78 @@ android:paddingStart="@dimen/gesture_tutorial_hotseat_padding_start_end" android:paddingEnd="@dimen/gesture_tutorial_hotseat_padding_start_end"> - - - - - - - + app:layout_constraintTop_toTopOf="@id/full_task_view" /> - + android:layout_centerHorizontal="true" + android:layout_marginBottom="@dimen/gesture_tutorial_taskbar_margin_bottom" /> + android:paddingEnd="26dp"> - - - - - diff --git a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java index 53ad138b6a..3ccd683eba 100644 --- a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java +++ b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java @@ -30,7 +30,6 @@ import android.view.ViewOutlineProvider; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.cardview.widget.CardView; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.R; @@ -46,8 +45,8 @@ import java.util.ArrayList; public class AnimatedTaskView extends ConstraintLayout { private View mFullTaskView; - private CardView mTopTaskView; - private CardView mBottomTaskView; + private View mTopTaskView; + private View mBottomTaskView; private ViewOutlineProvider mTaskViewOutlineProvider = null; private final Rect mTaskViewAnimatedRect = new Rect(); @@ -185,8 +184,6 @@ public class AnimatedTaskView extends ConstraintLayout { void setFakeTaskViewFillColor(@ColorInt int colorResId) { mFullTaskView.setBackgroundColor(colorResId); - mTopTaskView.setCardBackgroundColor(colorResId); - mBottomTaskView.setCardBackgroundColor(colorResId); } @Override diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java index c89d4b61a8..bce639b200 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java @@ -61,10 +61,10 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll @Override protected int getMockAppTaskLayoutResId() { - return mTutorialFragment.isLargeScreen() - ? R.layout.gesture_tutorial_tablet_mock_webpage - : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.swipe_up_gesture_tutorial_shape + return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() + ? R.layout.swipe_up_gesture_tutorial_shape + : mTutorialFragment.isLargeScreen() + ? R.layout.gesture_tutorial_tablet_mock_webpage : R.layout.gesture_tutorial_mock_webpage; } diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index ccdb2660bd..6fcb840349 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -154,7 +154,6 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFeedbackTitleView.setText(getIntroductionTitle()); mFeedbackSubtitleView.setText(getIntroductionSubtitle()); - mSkipButton.setVisibility(GONE); } mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(