From 4ddd20c53d8b058141f49185896c6f1bc2057239 Mon Sep 17 00:00:00 2001 From: Jagrut Desai Date: Fri, 3 Mar 2023 16:09:02 -0800 Subject: [PATCH 01/11] Make TaskbarStashController aware of accesseibilty time to action settings. Test: Manual Bug: 265594904 Bug: 265755827 Change-Id: Id71a6343a0a014933c4468c971ae3e8c4dd9f806 --- .../launcher3/taskbar/TaskbarStashController.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index 150d3db453..f816d6f6df 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -16,6 +16,7 @@ package com.android.launcher3.taskbar; import static android.view.HapticFeedbackConstants.LONG_PRESS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; import static com.android.launcher3.anim.Interpolators.EMPHASIZED; import static com.android.launcher3.anim.Interpolators.FINAL_FRAME; @@ -43,6 +44,7 @@ import android.util.Log; import android.view.InsetsController; import android.view.View; import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityManager; import android.view.animation.Interpolator; import androidx.annotation.NonNull; @@ -189,6 +191,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba // Stashed handle properties. private MultiProperty mTaskbarStashedHandleAlpha; private AnimatedFloat mTaskbarStashedHandleHintScale; + private final AccessibilityManager mAccessibilityManager; /** Whether we are currently visually stashed (might change based on launcher state). */ private boolean mIsStashed = false; @@ -221,6 +224,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba mActivity = activity; mPrefs = LauncherPrefs.getPrefs(mActivity); mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity); + mAccessibilityManager = mActivity.getSystemService(AccessibilityManager.class); if (isPhoneMode()) { // DeviceProfile's taskbar vars aren't initialized w/ the flag off Resources resources = mActivity.getResources(); @@ -1018,7 +1022,15 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba cancelTimeoutIfExists(); mTimeoutAlarm.setOnAlarmListener(this::onTaskbarTimeout); - mTimeoutAlarm.setAlarm(NO_TOUCH_TIMEOUT_TO_STASH_MS); + mTimeoutAlarm.setAlarm(getTaskbarAutoHideTimeout()); + } + + /** + * returns appropriate timeout for taskbar to stash depending on accessibility being on/off. + */ + private long getTaskbarAutoHideTimeout() { + return mAccessibilityManager.getRecommendedTimeoutMillis((int) NO_TOUCH_TIMEOUT_TO_STASH_MS, + FLAG_CONTENT_CONTROLS); } private void onTaskbarTimeout(Alarm alarm) { From aa7bf8b39444cc4c24c922d8602d84c6df9d5192 Mon Sep 17 00:00:00 2001 From: Vinit Nayak Date: Mon, 27 Feb 2023 11:58:16 -0800 Subject: [PATCH 02/11] Add initial split from GroupedTaskView animation * Created SplitAnimationController to prevent more RecentsView bloat * Extract SplitSelectSource attributes into separate class * http://recall/-/cMb5xTTxhmZtFt04eYnmQj/eACJq1un8Ef91LgmQblpAw Bug: 267370106 Change-Id: I5d430c091ac84bbbc30319e7b78711b610a21252 --- .../util/SplitAnimationController.kt | 163 ++++++++++++++++++ .../util/SplitSelectStateController.java | 24 +++ .../quickstep/views/DesktopTaskView.java | 2 +- .../quickstep/views/GroupedTaskView.java | 61 ++++++- .../android/quickstep/views/RecentsView.java | 78 ++++++--- .../quickstep/views/TaskThumbnailView.java | 94 +++++++++- .../com/android/quickstep/views/TaskView.java | 43 ++++- .../touch/PortraitPagedViewHandler.java | 11 +- 8 files changed, 427 insertions(+), 49 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/util/SplitAnimationController.kt diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt new file mode 100644 index 0000000000..6dd67def47 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -0,0 +1,163 @@ +/* + * 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.quickstep.util + +import android.animation.ObjectAnimator +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.view.View +import com.android.launcher3.DeviceProfile +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource +import com.android.quickstep.views.TaskThumbnailView +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskView.TaskIdAttributeContainer +import java.util.function.Supplier + +/** + * Utils class to help run animations for initiating split screen from launcher. + * Will be expanded with future refactors. Works in conjunction with the state stored in + * [SplitSelectStateController] + */ +class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) { + companion object { + // Break this out into maybe enums? Abstractions into its own classes? Tbd. + data class SplitAnimInitProps( + val originalView: View, + val originalBitmap: Bitmap?, + val iconDrawable: Drawable, + val fadeWithThumbnail: Boolean, + val isStagedTask: Boolean, + val iconView: View? + ) + } + + /** + * Returns different elements to animate for the initial split selection animation + * depending on the state of the surface from which the split was initiated + */ + fun getFirstAnimInitViews(taskViewSupplier: Supplier, + splitSelectSourceSupplier: Supplier) + : SplitAnimInitProps { + if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { + // Initiating from home + val splitSelectSource = splitSelectSourceSupplier.get() + return SplitAnimInitProps(splitSelectSource.view, originalBitmap = null, + splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, + iconView = null) + } else if (splitSelectStateController.isDismissingFromSplitPair) { + // Initiating split from overview, but on a split pair + val taskView = taskViewSupplier.get() + for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) { + if (container.task.key.id == splitSelectStateController.initialTaskId) { + return SplitAnimInitProps(container.thumbnailView, + container.thumbnailView.thumbnail, container.iconView.drawable!!, + fadeWithThumbnail = true, isStagedTask = true, + iconView = container.iconView + ) + } + } + throw IllegalStateException("Attempting to init split from existing split pair " + + "without a valid taskIdAttributeContainer") + } else { + // Initiating split from overview on fullscreen task TaskView + val taskView = taskViewSupplier.get() + return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail, + taskView.iconView.drawable!!, fadeWithThumbnail = true, isStagedTask = true, + taskView.iconView + ) + } + } + + /** + * When selecting first app from split pair, second app's thumbnail remains. This animates + * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying + * it with [TaskThumbnailView]'s splashView. Adds animations to the provided builder. + * Note: The app that **was not** selected as the first split app should be the container that's + * passed through. + * + * @param builder Adds animation to this + * @param taskIdAttributeContainer container of the app that **was not** selected + * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair + * (opposite of that representing [taskIdAttributeContainer]) + */ + fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer, + builder: PendingAnimation, deviceProfile: DeviceProfile, + taskViewWidth: Int, taskViewHeight: Int, + isPrimaryTaskSplitting: Boolean) { + val thumbnail = taskIdAttributeContainer.thumbnailView + val iconView: View = taskIdAttributeContainer.iconView + builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f)) + thumbnail.setShowSplashForSplitSelection(true) + if (deviceProfile.isLandscape) { + // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 + val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f + val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f + val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX)) + // icons are anchored from Gravity.END, so need to use negative translation + builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, + -centerIconTranslationX)) + builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) + + // Reset other dimensions + // TODO(b/271468547), can't set Y translate to 0, need to account for top space + thumbnail.scaleY = 1f + val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else + deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, + translateYResetVal)) + } else { + val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx + // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 + // primary thumbnail has layout margin above it, so secondary thumbnail needs to take + // that into account. We should migrate to only using translations otherwise this + // asymmetry causes problems.. + + // Icon defaults to center | horizontal, we add additional translation for split + val centerIconTranslationX = 0f + var centerThumbnailTranslationY: Float + + // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary + // thumbnail needs to take that into account. We should migrate to only using + // translations otherwise this asymmetry causes problems.. + if (isPrimaryTaskSplitting) { + centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx + .toFloat() + } else { + centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + } + val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY)) + + // icons are anchored from Gravity.END, so need to use negative translation + builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, + centerIconTranslationX)) + builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) + + // Reset other dimensions + thumbnail.scaleX = 1f + builder.add(ObjectAnimator.ofFloat(thumbnail, + TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f)) + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index 5214f7c68c..1f4085f5c5 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -82,6 +82,7 @@ public class SplitSelectStateController { private final Context mContext; private final Handler mHandler; private final RecentsModel mRecentTasksModel; + private final SplitAnimationController mSplitAnimationController; private StatsLogManager mStatsLogManager; private final SystemUiProxy mSystemUiProxy; private final StateManager mStateManager; @@ -96,6 +97,11 @@ public class SplitSelectStateController { private boolean mRecentsAnimationRunning; /** If {@code true}, animates the existing task view split placeholder view */ private boolean mAnimateCurrentTaskDismissal; + /** + * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a + * split pair task view without wanting to animate current task dismissal overall + */ + private boolean mDismissingFromSplitPair; @Nullable private UserHandle mUser; /** If not null, this is the TaskView we want to launch from */ @@ -116,6 +122,7 @@ public class SplitSelectStateController { mStateManager = stateManager; mDepthController = depthController; mRecentTasksModel = recentsModel; + mSplitAnimationController = new SplitAnimationController(this); } /** @@ -399,6 +406,18 @@ public class SplitSelectStateController { mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal; } + public boolean isDismissingFromSplitPair() { + return mDismissingFromSplitPair; + } + + public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) { + mDismissingFromSplitPair = dismissingFromSplitPair; + } + + public SplitAnimationController getSplitAnimationController() { + return mSplitAnimationController; + } + /** * Requires Shell Transitions */ @@ -506,6 +525,7 @@ public class SplitSelectStateController { mItemInfo = null; mSplitEvent = null; mAnimateCurrentTaskDismissal = false; + mDismissingFromSplitPair = false; } /** @@ -532,6 +552,10 @@ public class SplitSelectStateController { return mInitialTaskId; } + public int getSecondTaskId() { + return mSecondTaskId; + } + private boolean isSecondTaskIntentSet() { return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null); } diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java index 14898b1007..cc05779d08 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java @@ -500,7 +500,7 @@ public class DesktopTaskView extends TaskView { } @Override - void setThumbnailVisibility(int visibility) { + void setThumbnailVisibility(int visibility, int taskId) { for (int i = 0; i < mSnapshotViewMap.size(); i++) { mSnapshotViewMap.valueAt(i).setVisibility(visibility); } diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java index e9498fd0f9..0e05032423 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java @@ -1,5 +1,7 @@ package com.android.quickstep.views; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; @@ -25,6 +27,7 @@ import com.android.quickstep.TaskIconCache; import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.util.CancellableTask; import com.android.quickstep.util.RecentsOrientedState; +import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.TaskViewSimulator; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -267,6 +270,19 @@ public class GroupedTaskView extends TaskView { @Override protected int getLastSelectedChildTaskIndex() { + SplitSelectStateController splitSelectController = + getRecentsView().getSplitSelectController(); + if (splitSelectController.isDismissingFromSplitPair()) { + // return the container index of the task that wasn't initially selected to split with + // because that is the only remaining app that can be selected. The coordinate checks + // below aren't reliable since both of those views may be gone/transformed + int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); + if (initSplitTaskId != INVALID_TASK_ID) { + return initSplitTaskId == mTask.key.id ? 1 : 0; + } + } + + // Check which of the two apps was selected if (isCoordInView(mIconView2, mLastTouchDownPosition) || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) { return 1; @@ -296,9 +312,30 @@ public class GroupedTaskView extends TaskView { if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) { return; } - getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, - mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, - mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL); + int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); + if (initSplitTaskId == INVALID_TASK_ID) { + getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, + mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, + mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL); + // Should we be having a separate translation step apart from the measuring above? + // The following only applies to large screen for now, but for future reference + // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary + // translation directions + mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX()); + mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY()); + mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX()); + mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY()); + } else { + // Currently being split with this taskView, let the non-split selected thumbnail + // take up full thumbnail area + TaskIdAttributeContainer container = + mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0]; + container.getThumbnailView().measure(widthMeasureSpec, + View.MeasureSpec.makeMeasureSpec( + heightSize - + mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx, + MeasureSpec.EXACTLY)); + } updateIconPlacement(); } @@ -379,21 +416,27 @@ public class GroupedTaskView extends TaskView { mSnapshotView2.refreshSplashView(); } + @Override + protected void resetViewTransforms() { + super.resetViewTransforms(); + mSnapshotView2.resetViewTransforms(); + } + /** - * Sets visibility for thumbnails and associated elements (DWB banners). - * IconView is unaffected. + * Sets visibility for thumbnails and associated elements (DWB banners). + * IconView is unaffected. * - * When setting INVISIBLE, sets the visibility for the last selected child task. - * When setting VISIBLE (as a reset), sets the visibility for both tasks. + * When setting INVISIBLE, sets the visibility for the last selected child task. + * When setting VISIBLE (as a reset), sets the visibility for both tasks. */ @Override - void setThumbnailVisibility(int visibility) { + void setThumbnailVisibility(int visibility, int taskId) { if (visibility == VISIBLE) { mSnapshotView.setVisibility(visibility); mDigitalWellBeingToast.setBannerVisibility(visibility); mSnapshotView2.setVisibility(visibility); mDigitalWellBeingToast2.setBannerVisibility(visibility); - } else if (getLastSelectedChildTaskIndex() == 0) { + } else if (taskId == getTaskIds()[0]) { mSnapshotView.setVisibility(visibility); mDigitalWellBeingToast.setBannerVisibility(visibility); } else { diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index ac59403216..076fd64ec0 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -186,6 +186,7 @@ import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RecentsOrientedState; +import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps; import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SurfaceTransaction; @@ -194,6 +195,7 @@ import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.VibrationConstants; +import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; import com.android.systemui.plugins.ResourceProvider; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -937,7 +939,7 @@ public abstract class RecentsView mSplitHiddenTaskView, () -> mSplitSelectSource); if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) { // Create the split select animation from Overview - mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE); - anim.setViewAlpha(mSplitHiddenTaskView.getIconView(), 0, clampToProgress(LINEAR, + mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE, + mSplitSelectStateController.getInitialTaskId()); + anim.setViewAlpha(splitAnimInitProps.getIconView(), 0, clampToProgress(LINEAR, timings.getIconFadeStartOffset(), timings.getIconFadeEndOffset())); - mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, - mSplitHiddenTaskView.getThumbnail(), - mSplitHiddenTaskView.getThumbnail().getThumbnail(), - mSplitHiddenTaskView.getIconView().getDrawable(), startingTaskRect); - mFirstFloatingTaskView.setAlpha(1); - mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, - true /* fadeWithThumbnail */, true /* isStagedTask */); - } else { - // Create the split select animation from Home - mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, - mSplitSelectSource.view, null /* thumbnail */, - mSplitSelectSource.drawable, startingTaskRect); - mFirstFloatingTaskView.setAlpha(1); - mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, - false /* fadeWithThumbnail */, true /* isStagedTask */); } + mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, + splitAnimInitProps.getOriginalView(), + splitAnimInitProps.getOriginalBitmap(), + splitAnimInitProps.getIconDrawable(), startingTaskRect); + mFirstFloatingTaskView.setAlpha(1); + mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, + splitAnimInitProps.getFadeWithThumbnail(), splitAnimInitProps.isStagedTask()); + // Allow user to click staged app to launch into fullscreen if (ENABLE_LAUNCH_FROM_STAGED_APP.get()) { mFirstFloatingTaskView.setOnClickListener(this::animateToFullscreen); @@ -4447,7 +4446,7 @@ public abstract class RecentsView{ + thumbnail.refreshSplashView(); + mSplitHiddenTaskView.updateSnapshotRadius(); + }); + } else if (isInitiatingSplitFromTaskView) { + // Splitting from Overview for fullscreen task createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, true /* dismissingForSplitSelection*/); } else { @@ -4606,7 +4633,8 @@ public abstract class RecentsView SPLASH_ALPHA = + new FloatProperty("splashAlpha") { + @Override + public void setValue(TaskThumbnailView thumbnail, float splashAlpha) { + thumbnail.setSplashAlpha(splashAlpha); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mSplashAlpha / 255f; + } + }; + + /** Use to animate thumbnail translationX while first app in split selection is initiated */ + public static final Property SPLIT_SELECT_TRANSLATE_X = + new FloatProperty("splitSelectTranslateX") { + @Override + public void setValue(TaskThumbnailView thumbnail, float splitSelectTranslateX) { + thumbnail.applySplitSelectTranslateX(splitSelectTranslateX); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mSplitSelectTranslateX; + } + }; + + /** Use to animate thumbnail translationY while first app in split selection is initiated */ + public static final Property SPLIT_SELECT_TRANSLATE_Y = + new FloatProperty("splitSelectTranslateY") { + @Override + public void setValue(TaskThumbnailView thumbnail, float splitSelectTranslateY) { + thumbnail.applySplitSelectTranslateY(splitSelectTranslateY); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mSplitSelectTranslateY; + } + }; + private final BaseActivity mActivity; @Nullable private TaskOverlay mOverlay; @@ -111,6 +152,10 @@ public class TaskThumbnailView extends View { private int mSplashAlpha = 0; private boolean mOverlayEnabled; + /** Used as a placeholder when the original thumbnail animates out to. */ + private boolean mShowSplashForSplitSelection; + private float mSplitSelectTranslateX; + private float mSplitSelectTranslateY; public TaskThumbnailView(Context context) { this(context, null); @@ -342,10 +387,17 @@ public class TaskThumbnailView extends View { // Draw splash above thumbnail to hide inconsistencies in rotation and aspect ratios. if (shouldShowSplashView()) { + float cornerRadiusX = cornerRadius; + float cornerRadiusY = cornerRadius; + if (mShowSplashForSplitSelection) { + cornerRadiusX = cornerRadius / getScaleX(); + cornerRadiusY = cornerRadius / getScaleY(); + } + // Always draw background for hiding inconsistencies, even if splash view is not yet // loaded (which can happen as task icons are loaded asynchronously in the background) - canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadius, - cornerRadius, mSplashBackgroundPaint); + canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadiusX, + cornerRadiusY, mSplashBackgroundPaint); if (mSplashView != null) { mSplashView.layout((int) x, (int) (y + 1), (int) width, (int) height - 1); mSplashView.draw(canvas); @@ -353,6 +405,31 @@ public class TaskThumbnailView extends View { } } + /** See {@link #SPLIT_SELECT_TRANSLATE_X} */ + protected void applySplitSelectTranslateX(float splitSelectTranslateX) { + mSplitSelectTranslateX = splitSelectTranslateX; + applyTranslateX(); + } + + /** See {@link #SPLIT_SELECT_TRANSLATE_Y} */ + protected void applySplitSelectTranslateY(float splitSelectTranslateY) { + mSplitSelectTranslateY = splitSelectTranslateY; + applyTranslateY(); + } + + private void applyTranslateX() { + setTranslationX(mSplitSelectTranslateX); + } + + private void applyTranslateY() { + setTranslationY(mSplitSelectTranslateY); + } + + protected void resetViewTransforms() { + mSplitSelectTranslateX = 0; + mSplitSelectTranslateY = 0; + } + public TaskView getTaskView() { return (TaskView) getParent(); } @@ -373,7 +450,12 @@ public class TaskThumbnailView extends View { */ public boolean shouldShowSplashView() { return isThumbnailAspectRatioDifferentFromThumbnailData() - || isThumbnailRotationDifferentFromTask(); + || isThumbnailRotationDifferentFromTask() + || mShowSplashForSplitSelection; + } + + public void setShowSplashForSplitSelection(boolean showSplashForSplitSelection) { + mShowSplashForSplitSelection = showSplashForSplitSelection; } protected void refreshSplashView() { @@ -396,7 +478,6 @@ public class TaskThumbnailView extends View { imageView.setScaleType(ImageView.ScaleType.MATRIX); Matrix matrix = new Matrix(); - float drawableWidth = mSplashViewDrawable.getIntrinsicWidth(); float drawableHeight = mSplashViewDrawable.getIntrinsicHeight(); float viewWidth = getMeasuredWidth(); @@ -408,12 +489,13 @@ public class TaskThumbnailView extends View { float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale(); float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen(); - float scale = nonGridScale * recentsMaxScale; + float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX()); + float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY()); // Center the image in the view. matrix.setTranslate(centeredDrawableLeft, centeredDrawableTop); // Apply scale transformation after translation, pivoting around center of view. - matrix.postScale(scale, scale, viewCenterX, viewCenterY); + matrix.postScale(scaleX, scaleY, viewCenterX, viewCenterY); imageView.setImageMatrix(matrix); mSplashView = imageView; diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index ab72f2d10e..a06e1f8225 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -16,6 +16,7 @@ package com.android.quickstep.views; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.Display.DEFAULT_DISPLAY; import static android.widget.Toast.LENGTH_SHORT; @@ -41,6 +42,7 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.IdRes; import android.app.ActivityOptions; +import android.app.ActivityTaskManager; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; @@ -706,9 +708,12 @@ public class TaskView extends FrameLayout implements Reusable { } SplitSelectStateController splitSelectStateController = recentsView.getSplitSelectController(); - if (splitSelectStateController.isSplitSelectActive() && - splitSelectStateController.getInitialTaskId() == getTask().key.id) { - // Prevent taps on the this taskview if it's being animated into split select state + // Disable taps for split selection animation unless we have multiple tasks + boolean disableTapsForSplitSelect = + splitSelectStateController.isSplitSelectActive() + && splitSelectStateController.getInitialTaskId() == getTask().key.id + && !containsMultipleTasks(); + if (disableTapsForSplitSelect) { return false; } @@ -718,6 +723,25 @@ public class TaskView extends FrameLayout implements Reusable { return super.dispatchTouchEvent(ev); } + /** + * @return taskId that split selection was initiated with, + * {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of + * split selection + */ + protected int getThisTaskCurrentlyInSplitSelection() { + SplitSelectStateController splitSelectController = + getRecentsView().getSplitSelectController(); + int initSplitTaskId = INVALID_TASK_ID; + for (TaskIdAttributeContainer container : getTaskIdAttributeContainers()) { + int taskId = container.getTask().key.id; + if (taskId == splitSelectController.getInitialTaskId()) { + initSplitTaskId = taskId; + break; + } + } + return initSplitTaskId; + } + private void onClick(View view) { if (getTask() == null) { return; @@ -747,6 +771,8 @@ public class TaskView extends FrameLayout implements Reusable { /** * Returns the task index of the last selected child task (0 or 1). + * If we contain multiple tasks and this TaskView is used as part of split selection, the + * selected child task index will be that of the remaining task. */ protected int getLastSelectedChildTaskIndex() { return 0; @@ -1084,6 +1110,8 @@ public class TaskView extends FrameLayout implements Reusable { DeviceProfile deviceProfile = mActivity.getDeviceProfile(); int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; + // TODO(b/271468547), we should default to setting trasnlations only on the snapshot instead + // of a hybrid of both margins and translations LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams(); snapshotParams.topMargin = thumbnailTopMargin; mSnapshotView.setLayoutParams(snapshotParams); @@ -1179,6 +1207,7 @@ public class TaskView extends FrameLayout implements Reusable { setAlpha(mStableAlpha); setIconScaleAndDim(1); setColorTint(0, 0); + mSnapshotView.resetViewTransforms(); } public void setStableAlpha(float parentAlpha) { @@ -1720,10 +1749,12 @@ public class TaskView extends FrameLayout implements Reusable { } /** - * Sets visibility for the thumbnail and associated elements (DWB banners and action chips). - * IconView is unaffected. + * Sets visibility for the thumbnail and associated elements (DWB banners and action chips). + * IconView is unaffected. + * + * @param taskId is only used when setting visibility to a non-{@link View#VISIBLE} value */ - void setThumbnailVisibility(int visibility) { + void setThumbnailVisibility(int visibility, int taskId) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child != mIconView) { diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java index 628aa9a28d..295baa3a88 100644 --- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java +++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java @@ -635,6 +635,9 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { primarySnapshot.setTranslationX(0); } secondarySnapshot.setTranslationY(spaceAboveSnapshot); + + // Reset unused translations + primarySnapshot.setTranslationY(0); } else { int deviceHeightWithoutTaskbar = dp.availableHeightPx - dp.taskbarSize; float scale = (float) totalThumbnailHeight / deviceHeightWithoutTaskbar; @@ -669,6 +672,10 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight, View.MeasureSpec.EXACTLY)); + primarySnapshot.setScaleX(1); + secondarySnapshot.setScaleX(1); + primarySnapshot.setScaleY(1); + secondarySnapshot.setScaleY(1); } @Override @@ -699,13 +706,13 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { : deviceProfile.getInsets().left; int fullscreenMidpointFromBottom = ((deviceProfile.widthPx - fullscreenInsetThickness) / 2); - float midpointFromBottomPct = (float) fullscreenMidpointFromBottom + float midpointFromEndPct = (float) fullscreenMidpointFromBottom / deviceProfile.widthPx; float insetPct = (float) fullscreenInsetThickness / deviceProfile.widthPx; int spaceAboveSnapshots = 0; int overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots; int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness - * midpointFromBottomPct); + * midpointFromEndPct); int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); if (deviceProfile.isSeascape()) { From cdb3c0af1ab664291027bf296fe96bcea7880cdf Mon Sep 17 00:00:00 2001 From: Sihua Ma Date: Mon, 13 Feb 2023 15:12:32 -0800 Subject: [PATCH 03/11] Use app widget host instead of holder to get rid of deleteHost Also putting the calls to destroy() in try-finally blocks. Test: N/A Fix: 272535886 Change-Id: I57ecd90e412b3b8b59b4ed755a5d4191b05fd9b3 --- .../launcher3/AppWidgetsRestoredReceiver.java | 9 +-- .../android/launcher3/LauncherProvider.java | 59 ++++++++++--------- .../launcher3/provider/RestoreDbTask.java | 8 +-- .../widget/LauncherWidgetHolder.java | 7 --- 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java index eb6d096602..9d5b08eab5 100644 --- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -2,6 +2,7 @@ package com.android.launcher3; import static android.os.Process.myUserHandle; +import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; @@ -51,10 +52,10 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { */ @WorkerThread public static void restoreAppWidgetIds(Context context, int[] oldWidgetIds, int[] newWidgetIds, - @NonNull LauncherWidgetHolder holder) { + @NonNull AppWidgetHost host) { if (WidgetsModel.GO_DISABLE_WIDGETS) { Log.e(TAG, "Skipping widget ID remap as widgets not supported"); - holder.deleteHost(); + host.deleteHost(); return; } if (!RestoreDbTask.isPending(context)) { @@ -63,7 +64,7 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { Log.e(TAG, "Skipping widget ID remap as DB already in use"); for (int widgetId : newWidgetIds) { Log.d(TAG, "Deleting widgetId: " + widgetId); - holder.deleteAppWidgetId(widgetId); + host.deleteAppWidgetId(widgetId); } return; } @@ -100,7 +101,7 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { try { if (!cursor.moveToFirst()) { // The widget no long exists. - holder.deleteAppWidgetId(newWidgetIds[i]); + host.deleteAppWidgetId(newWidgetIds[i]); } } finally { cursor.close(); diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 767c3d986e..bccb894343 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -550,39 +550,42 @@ public class LauncherProvider extends ContentProvider { Log.d(TAG, "loading default workspace"); LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder(); - AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder); - if (loader == null) { - loader = AutoInstallsLayout.get(getContext(), widgetHolder, mOpenHelper); - } - if (loader == null) { - final Partner partner = Partner.get(getContext().getPackageManager()); - if (partner != null) { - int workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT); - if (workspaceResId != 0) { - loader = new DefaultLayoutParser(getContext(), widgetHolder, - mOpenHelper, partner.getResources(), workspaceResId); + try { + AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder); + if (loader == null) { + loader = AutoInstallsLayout.get(getContext(), widgetHolder, mOpenHelper); + } + if (loader == null) { + final Partner partner = Partner.get(getContext().getPackageManager()); + if (partner != null) { + int workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT); + if (workspaceResId != 0) { + loader = new DefaultLayoutParser(getContext(), widgetHolder, + mOpenHelper, partner.getResources(), workspaceResId); + } } } - } - final boolean usingExternallyProvidedLayout = loader != null; - if (loader == null) { - loader = getDefaultLayoutParser(widgetHolder); - } + final boolean usingExternallyProvidedLayout = loader != null; + if (loader == null) { + loader = getDefaultLayoutParser(widgetHolder); + } - // There might be some partially restored DB items, due to buggy restore logic in - // previous versions of launcher. - mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); - // Populate favorites table with initial favorites - if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) - && usingExternallyProvidedLayout) { - // Unable to load external layout. Cleanup and load the internal layout. + // There might be some partially restored DB items, due to buggy restore logic in + // previous versions of launcher. mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); - mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), - getDefaultLayoutParser(widgetHolder)); + // Populate favorites table with initial favorites + if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) + && usingExternallyProvidedLayout) { + // Unable to load external layout. Cleanup and load the internal layout. + mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); + mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), + getDefaultLayoutParser(widgetHolder)); + } + clearFlagEmptyDbCreated(); + } finally { + widgetHolder.destroy(); } - clearFlagEmptyDbCreated(); - widgetHolder.destroy(); } } @@ -957,8 +960,6 @@ public class LauncherProvider extends ContentProvider { allWidgets = holder.getAppWidgetIds(); } catch (IncompatibleClassChangeError e) { Log.e(TAG, "getAppWidgetIds not supported", e); - // Necessary to destroy the holder to free up possible activity context - holder.destroy(); return; } final IntSet validWidgets = IntSet.wrap(LauncherDbUtils.queryIntArray(false, db, diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index 2a452be8dd..3a5ef10c59 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -21,8 +21,10 @@ import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS; import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; import static com.android.launcher3.provider.LauncherDbUtils.dropTable; +import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; import android.app.backup.BackupManager; +import android.appwidget.AppWidgetHost; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -49,7 +51,6 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.LogConfig; -import com.android.launcher3.widget.LauncherWidgetHolder; import java.io.InvalidObjectException; import java.util.Arrays; @@ -354,12 +355,11 @@ public class RestoreDbTask { private void restoreAppWidgetIdsIfExists(Context context) { LauncherPrefs lp = LauncherPrefs.get(context); if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) { - LauncherWidgetHolder holder = LauncherWidgetHolder.newInstance(context); + AppWidgetHost host = new AppWidgetHost(context, APPWIDGET_HOST_ID); AppWidgetsRestoredReceiver.restoreAppWidgetIds(context, IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(), IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(), - holder); - holder.destroy(); + host); } else { FileLog.d(TAG, "No app widget ids to restore."); } diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java index bea7517c91..8e67eb110b 100644 --- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java +++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java @@ -321,13 +321,6 @@ public class LauncherWidgetHolder { mFlags &= ~FLAG_LISTENING; } - /** - * Delete the host - */ - public void deleteHost() { - mWidgetHost.deleteHost(); - } - /** * @return The app widget ids */ From 3a6193517d42cde768abafecb3757fbbd375c2e5 Mon Sep 17 00:00:00 2001 From: Thales Lima Date: Thu, 23 Feb 2023 16:33:18 +0000 Subject: [PATCH 04/11] Fix TaskView cropping for transient taskbar TaskView clipping is not necessary anymore because task in Overview will always be the same aspect ratio as the app when in fullscreen. Fix: 268211636 Test: manual trigger of Overview (landscape, portrait, split screen) Change-Id: Ib4faeda6f0cccef3b23fcbacb89c6d2832c0df59 Merged-In: Ib4faeda6f0cccef3b23fcbacb89c6d2832c0df59 --- .../quickstep/BaseActivityInterface.java | 22 +-- .../quickstep/util/RecentsOrientedState.java | 2 +- .../quickstep/util/TaskViewSimulator.java | 12 +- .../views/DigitalWellBeingToast.java | 16 +- .../quickstep/views/GroupedTaskView.java | 4 +- .../quickstep/views/TaskThumbnailView.java | 13 +- .../com/android/quickstep/views/TaskView.java | 42 +---- .../quickstep/FullscreenDrawParamsTest.kt | 165 +++++------------- .../touch/PortraitPagedViewHandler.java | 6 +- 9 files changed, 73 insertions(+), 209 deletions(-) diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java index 3e565b3697..487da9208a 100644 --- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java +++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java @@ -244,6 +244,7 @@ public abstract class BaseActivityInterface 0) { - mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight); - } - } - - /** - * Insets to used for clipping the thumbnail (in case it is drawing outside its own space) - */ - private static RectF getInsetsToDrawInFullscreen(PreviewPositionHelper pph, - DeviceProfile dp, boolean isTaskbarTransient) { - if (dp.isTaskbarPresent && isTaskbarTransient) { - return pph.getClippedInsets(); - } - return dp.isTaskbarPresent && !dp.isTaskbarPresentInApps - ? pph.getClippedInsets() : EMPTY_RECT_F; } } diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt index bc1b87deb0..6c0e7dc8fd 100644 --- a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt +++ b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt @@ -16,16 +16,14 @@ package com.android.quickstep import android.graphics.Rect -import android.graphics.RectF import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest 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.util.TaskCornerRadius import com.android.quickstep.views.TaskView.FullscreenDrawParams import com.android.systemui.shared.recents.model.ThumbnailData import com.android.systemui.shared.recents.utilities.PreviewPositionHelper -import com.android.wm.shell.util.SplitBounds +import com.android.systemui.shared.system.QuickStepContract import com.google.common.truth.Truth.assertThat import kotlin.math.roundToInt import org.junit.Before @@ -50,7 +48,42 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { } @Test - fun setFullProgress_currentDrawnInsets_clipTaskbarSizeFromBottomForTablets() { + fun setStartProgress_correctCornerRadiusForTablet() { + initializeVarsForTablet() + val dp = newDP() + val previewRect = Rect(0, 0, 100, 100) + val canvasWidth = (dp.widthPx * TASK_SCALE).roundToInt() + val canvasHeight = (dp.heightPx * TASK_SCALE).roundToInt() + val currentRotation = 0 + val isRtl = false + + mPreviewPositionHelper.updateThumbnailMatrix( + previewRect, + mThumbnailData, + canvasWidth, + canvasHeight, + dp.widthPx, + dp.heightPx, + dp.taskbarSize, + dp.isTablet, + currentRotation, + isRtl + ) + params.setProgress( + /* fullscreenProgress= */ 0f, + /* parentScale= */ 1.0f, + /* taskViewScale= */ 1.0f, + /* previewWidth= */ 0, + dp, + mPreviewPositionHelper + ) + + val expectedRadius = TaskCornerRadius.get(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + } + + @Test + fun setFullProgress_correctCornerRadiusForTablet() { initializeVarsForTablet() val dp = newDP() val previewRect = Rect(0, 0, 100, 100) @@ -80,122 +113,19 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { mPreviewPositionHelper ) - val expectedClippedInsets = RectF(0f, 0f, 0f, dp.taskbarSize * TASK_SCALE) - assertThat(params.mCurrentDrawnInsets).isEqualTo(expectedClippedInsets) + val expectedRadius = QuickStepContract.getWindowCornerRadius(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) } @Test - fun setFullProgress_currentDrawnInsets_clipTaskbarSizeFromBottomForTablets_splitPortrait() { - initializeVarsForTablet() + fun setStartProgress_correctCornerRadiusForPhone() { + initializeVarsForPhone() val dp = newDP() val previewRect = Rect(0, 0, 100, 100) val canvasWidth = (dp.widthPx * TASK_SCALE).roundToInt() - val canvasHeight = (dp.heightPx * TASK_SCALE / 2).roundToInt() - val currentRotation = 0 - val isRtl = false - // portrait/vertical split apps - val dividerSize = 10 - val splitBounds = - SplitBounds( - Rect(0, 0, dp.widthPx, (dp.heightPx - dividerSize) / 2), - Rect(0, (dp.heightPx + dividerSize) / 2, dp.widthPx, dp.heightPx), - 0 /*lefTopTaskId*/, - 0 /*rightBottomTaskId*/ - ) - mPreviewPositionHelper.setSplitBounds(splitBounds, STAGE_POSITION_BOTTOM_OR_RIGHT) - - mPreviewPositionHelper.updateThumbnailMatrix( - previewRect, - mThumbnailData, - canvasWidth, - canvasHeight, - dp.widthPx, - dp.heightPx, - dp.taskbarSize, - dp.isTablet, - currentRotation, - isRtl - ) - params.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f, - /* previewWidth= */ 0, - dp, - mPreviewPositionHelper - ) - - // Probably unhelpful, but also unclear how to test otherwise ¯\_(ツ)_/¯ - val fullscreenTaskHeight = - dp.heightPx * (1 - (splitBounds.topTaskPercent + splitBounds.dividerHeightPercent)) - val canvasScreenRatio = canvasHeight / fullscreenTaskHeight - val expectedBottomHint = dp.taskbarSize * canvasScreenRatio - assertThat(params.mCurrentDrawnInsets.bottom).isWithin(1f).of(expectedBottomHint) - } - - @Test - fun setFullProgress_currentDrawnInsets_clipTaskbarSizeFromTopForTablets_splitPortrait() { - initializeVarsForTablet() - val dp = newDP() - val previewRect = Rect(0, 0, 100, 100) - val canvasWidth = (dp.widthPx * TASK_SCALE).roundToInt() - val canvasHeight = (dp.heightPx * TASK_SCALE / 2).roundToInt() - val currentRotation = 0 - val isRtl = false - // portrait/vertical split apps - val dividerSize = 10 - val splitBounds = - SplitBounds( - Rect(0, 0, dp.widthPx, (dp.heightPx - dividerSize) / 2), - Rect(0, (dp.heightPx + dividerSize) / 2, dp.widthPx, dp.heightPx), - 0 /*lefTopTaskId*/, - 0 /*rightBottomTaskId*/ - ) - mPreviewPositionHelper.setSplitBounds(splitBounds, STAGE_POSITION_TOP_OR_LEFT) - - mPreviewPositionHelper.updateThumbnailMatrix( - previewRect, - mThumbnailData, - canvasWidth, - canvasHeight, - dp.widthPx, - dp.heightPx, - dp.taskbarSize, - dp.isTablet, - currentRotation, - isRtl - ) - params.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f, - /* previewWidth= */ 0, - dp, - mPreviewPositionHelper - ) - - assertThat(params.mCurrentDrawnInsets.bottom).isWithin(1f).of((0f)) - } - - @Test - fun setFullProgress_currentDrawnInsets_clipTaskbarSizeFromBottomForTablets_splitLandscape() { - initializeVarsForTablet(isLandscape = true) - val dp = newDP() - val previewRect = Rect(0, 0, 100, 100) - val canvasWidth = (dp.widthPx * TASK_SCALE / 2).roundToInt() val canvasHeight = (dp.heightPx * TASK_SCALE).roundToInt() val currentRotation = 0 val isRtl = false - // portrait/vertical split apps - val dividerSize = 10 - val splitBounds = - SplitBounds( - Rect(0, 0, (dp.widthPx - dividerSize) / 2, dp.heightPx), - Rect((dp.widthPx + dividerSize) / 2, 0, dp.widthPx, dp.heightPx), - 0 /*lefTopTaskId*/, - 0 /*rightBottomTaskId*/ - ) - mPreviewPositionHelper.setSplitBounds(splitBounds, STAGE_POSITION_BOTTOM_OR_RIGHT) mPreviewPositionHelper.updateThumbnailMatrix( previewRect, @@ -210,7 +140,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { isRtl ) params.setProgress( - /* fullscreenProgress= */ 1.0f, + /* fullscreenProgress= */ 0f, /* parentScale= */ 1.0f, /* taskViewScale= */ 1.0f, /* previewWidth= */ 0, @@ -218,11 +148,12 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { mPreviewPositionHelper ) - assertThat(params.mCurrentDrawnInsets.bottom).isWithin(1f).of((dp.taskbarSize * TASK_SCALE)) + val expectedRadius = TaskCornerRadius.get(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) } @Test - fun setFullProgress_currentDrawnInsets_doNotClipTaskbarSizeFromBottomForPhones() { + fun setFullProgress_correctCornerRadiusForPhone() { initializeVarsForPhone() val dp = newDP() val previewRect = Rect(0, 0, 100, 100) @@ -252,7 +183,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { mPreviewPositionHelper ) - val expectedClippedInsets = RectF(0f, 0f, 0f, 0f) - assertThat(params.mCurrentDrawnInsets).isEqualTo(expectedClippedInsets) + val expectedRadius = QuickStepContract.getWindowCornerRadius(context) + assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) } } diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java index 628aa9a28d..4b501fef21 100644 --- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java +++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java @@ -582,8 +582,7 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { ? splitInfo.dividerHeightPercent : splitInfo.dividerWidthPercent; - int deviceHeightWithoutTaskbar = dp.availableHeightPx - dp.taskbarSize; - float scale = (float) outRect.height() / deviceHeightWithoutTaskbar; + float scale = (float) outRect.height() / dp.availableHeightPx; float topTaskHeight = dp.availableHeightPx * topLeftTaskPercent; float scaledTopTaskHeight = topTaskHeight * scale; float dividerHeight = dp.availableHeightPx * dividerBarPercent; @@ -636,8 +635,7 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { } secondarySnapshot.setTranslationY(spaceAboveSnapshot); } else { - int deviceHeightWithoutTaskbar = dp.availableHeightPx - dp.taskbarSize; - float scale = (float) totalThumbnailHeight / deviceHeightWithoutTaskbar; + float scale = (float) totalThumbnailHeight / dp.availableHeightPx; float topTaskHeight = dp.availableHeightPx * taskPercent; float finalDividerHeight = dividerBar * scale; float scaledTopTaskHeight = topTaskHeight * scale; From c7257cceec49faa3e9ca5e5ac5c907b240fe273e Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Thu, 9 Mar 2023 14:05:29 -0800 Subject: [PATCH 05/11] Baseline NewApi issues NewApi was recently updated to support linting methods in the system, module_lib, and system_server sdks, and was demoted to a warning due to the new issues it finds. Baseline all the new issues that NewApi can be made an error again. These cls were generated automatically by a script that copied the NewApi issues from the reference baselines. Bug: 268261262 Test: m lint-check Change-Id: Iaaf0504fb29069417c00f2950b9412654b9bdab7 --- lint-baseline.xml | 148 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 lint-baseline.xml diff --git a/lint-baseline.xml b/lint-baseline.xml new file mode 100644 index 0000000000..23a22be31b --- /dev/null +++ b/lint-baseline.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e17a94995015889ece75067fced5e27a0e81316e Mon Sep 17 00:00:00 2001 From: Jon Miranda Date: Wed, 8 Mar 2023 16:42:20 -0800 Subject: [PATCH 06/11] Align taskbar to the left/right in 3 button nav for certain devices. Otherwise we position the taskbar in the center. Bug: 259712417 Fixes: 267997547 Test: transient taskbar unaffected persistent taskbar - test on small tablet - test on large tablet - test LTR and RTL Change-Id: Ieb0a304d9963ebf583bc4ef2deaab747019e8d6d --- quickstep/res/values-sw600dp/config.xml | 21 ++++++++++++++++ quickstep/res/values-sw720dp-land/config.xml | 21 ++++++++++++++++ quickstep/res/values-sw720dp/config.xml | 21 ++++++++++++++++ quickstep/res/values/config.xml | 4 ++++ .../launcher3/taskbar/TaskbarView.java | 24 +++++++++++++++---- src/com/android/launcher3/DeviceProfile.java | 12 +++++----- 6 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 quickstep/res/values-sw600dp/config.xml create mode 100644 quickstep/res/values-sw720dp-land/config.xml create mode 100644 quickstep/res/values-sw720dp/config.xml diff --git a/quickstep/res/values-sw600dp/config.xml b/quickstep/res/values-sw600dp/config.xml new file mode 100644 index 0000000000..e1e442f0a9 --- /dev/null +++ b/quickstep/res/values-sw600dp/config.xml @@ -0,0 +1,21 @@ + + + + + + + true + \ No newline at end of file diff --git a/quickstep/res/values-sw720dp-land/config.xml b/quickstep/res/values-sw720dp-land/config.xml new file mode 100644 index 0000000000..bf0f9adea8 --- /dev/null +++ b/quickstep/res/values-sw720dp-land/config.xml @@ -0,0 +1,21 @@ + + + + + + + false + \ No newline at end of file diff --git a/quickstep/res/values-sw720dp/config.xml b/quickstep/res/values-sw720dp/config.xml new file mode 100644 index 0000000000..e1e442f0a9 --- /dev/null +++ b/quickstep/res/values-sw720dp/config.xml @@ -0,0 +1,21 @@ + + + + + + + true + \ No newline at end of file diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index d58158207d..b61bdfbc20 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -51,4 +51,8 @@ + + + + false diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 3d5089fc07..7ff988ccdd 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -91,6 +91,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private float mTransientTaskbarAllAppsButtonTranslationXOffset; + private final boolean mStartAlignTaskbar; + public TaskbarView(@NonNull Context context) { this(context, null); } @@ -118,6 +120,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar resources.getDimension(isTransientTaskbar ? R.dimen.transient_taskbar_all_apps_button_translation_x_offset : R.dimen.taskbar_all_apps_button_translation_x_offset); + mStartAlignTaskbar = mActivityContext.isThreeButtonNav() + && resources.getBoolean(R.bool.start_align_taskbar); int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing); int actualIconSize = mActivityContext.getDeviceProfile().iconSizePx; @@ -343,10 +347,22 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar boolean needMoreSpaceForNav = layoutRtl ? navSpaceNeeded > (iconEnd - spaceNeeded) : iconEnd > (right - navSpaceNeeded); - if (needMoreSpaceForNav) { - int offset = layoutRtl ? - navSpaceNeeded - (iconEnd - spaceNeeded) : - (right - navSpaceNeeded) - iconEnd; + + if (mStartAlignTaskbar) { + // Taskbar is aligned to the start + int startSpacingPx = deviceProfile.inlineNavButtonsEndSpacingPx; + + if (layoutRtl) { + iconEnd = right - startSpacingPx; + } else { + iconEnd = startSpacingPx + spaceNeeded; + } + } else if (needMoreSpaceForNav) { + // Add offset to account for nav bar when taskbar is centered + int offset = layoutRtl + ? navSpaceNeeded - (iconEnd - spaceNeeded) + : (right - navSpaceNeeded) - iconEnd; + iconEnd += offset; } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 87ee4f65ec..d992ee0085 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -193,7 +193,7 @@ public class DeviceProfile { private final int mMinHotseatIconSpacePx; private final int mMinHotseatQsbWidthPx; private final int mMaxHotseatIconSpacePx; - private final int mInlineNavButtonsEndSpacingPx; + public final int inlineNavButtonsEndSpacingPx; // Bottom sheets public int bottomSheetTopPadding; @@ -490,7 +490,7 @@ public class DeviceProfile { hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0; updateHotseatSizes(pxFromDp(inv.iconSize[INDEX_DEFAULT], mMetrics)); if (areNavButtonsInline && !isPhone) { - mInlineNavButtonsEndSpacingPx = + inlineNavButtonsEndSpacingPx = res.getDimensionPixelSize(inv.inlineNavButtonsEndSpacing); /* * 3 nav buttons + @@ -499,9 +499,9 @@ public class DeviceProfile { */ hotseatBarEndOffset = 3 * res.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) + 2 * res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween) - + mInlineNavButtonsEndSpacingPx; + + inlineNavButtonsEndSpacingPx; } else { - mInlineNavButtonsEndSpacingPx = 0; + inlineNavButtonsEndSpacingPx = 0; hotseatBarEndOffset = 0; } @@ -662,7 +662,7 @@ public class DeviceProfile { } // The side space with inline buttons should be what is defined in InvariantDeviceProfile - int sideSpacePx = mInlineNavButtonsEndSpacingPx; + int sideSpacePx = inlineNavButtonsEndSpacingPx; int maxHotseatWidthPx = availableWidthPx - sideSpacePx - hotseatBarEndOffset; int maxHotseatIconsWidthPx = maxHotseatWidthPx - (isQsbInline ? hotseatQsbWidth : 0); hotseatBorderSpace = calculateHotseatBorderSpace(maxHotseatIconsWidthPx, @@ -1320,7 +1320,7 @@ public class DeviceProfile { int endSpacing; // Hotseat aligns to the left with nav buttons if (hotseatBarEndOffset > 0) { - startSpacing = mInlineNavButtonsEndSpacingPx; + startSpacing = inlineNavButtonsEndSpacingPx; endSpacing = availableWidthPx - hotseatWidth - startSpacing + hotseatBorderSpace; } else { startSpacing = (availableWidthPx - hotseatWidth) / 2; From 1898d16ee0e0172a177fd8c6c06f4352f7b901c4 Mon Sep 17 00:00:00 2001 From: Fengjiang Li Date: Thu, 9 Mar 2023 11:26:55 -0800 Subject: [PATCH 07/11] [Predictive Back] Fix small bottom gap in widget to home animation The bug was caused by a math problem. The extrat bottom offset should be offset = (H / 0.9 - H) / 2 Fix: b/269393854 Test: manual Change-Id: Ia4d6b3f2ecb53b82d6779cd385ce4d7ca3c16113 --- src/com/android/launcher3/views/AbstractSlideInView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java index e2f1c04ced..ebae00d223 100644 --- a/src/com/android/launcher3/views/AbstractSlideInView.java +++ b/src/com/android/launcher3/views/AbstractSlideInView.java @@ -236,8 +236,8 @@ public abstract class AbstractSlideInView /** Return extra space revealed during predictive back animation. */ @Px protected int getBottomOffsetPx() { - return (int) (getMeasuredHeight() - * (1 - PREDICTIVE_BACK_MIN_SCALE) / 2); + final int height = getMeasuredHeight(); + return (int) ((height / PREDICTIVE_BACK_MIN_SCALE - height) / 2); } /** From db599b2d8e7a5ce1b6e092afb1b12c6db05ac18e Mon Sep 17 00:00:00 2001 From: Federico Baron Date: Thu, 9 Mar 2023 14:44:41 -0800 Subject: [PATCH 08/11] Fix bug where homescreen shows installation completed when it's technically still downloading and fix animation that repeats in apps with quick install Fix: 269702659 Test: Download an app that has a lighning next to the install text and verify that the animation plays smoothly and that it doesn't animate until we finish downloading completely. Change-Id: I11147444a85db01c75d63b5cdd09d1d911a28c75 --- src/com/android/launcher3/BubbleTextView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 3eb03edf13..80a8193379 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -717,7 +717,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, || info.hasPromiseIconUi() || (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0 || (ENABLE_DOWNLOAD_APP_UX_V2.get() && icon != null)) { - updateProgressBarUi(icon); + updateProgressBarUi(info.getProgressLevel() == 100 ? icon : null); } } } From 239ae0a995ed72d7e347f98a128a187b76bee894 Mon Sep 17 00:00:00 2001 From: Sebastian Franco Date: Fri, 3 Mar 2023 15:56:52 -0800 Subject: [PATCH 09/11] Making sure there is a propper Chrome icon for the test to long press In some cases, the test was long pressing on a recommendation for google chrome and not a regular chrome icon and the events didn't match. I don't think is good to relly on chrome getting recommended so I'm making sure there is always a chrome app icon on the hotseat Fix: 269163620 Test: atest TaplTestsLauncher3:testWorkspace (run on pixel 7 pro) Change-Id: Ic8ee744a9af18d60de9bbedd42043f5abbbd7356 --- .../testing/DebugTestInformationHandler.java | 6 ++++ res/xml/default_tapl_test_workspace.xml | 28 +++++++++++++++++++ .../android/launcher3/LauncherProvider.java | 4 +++ .../android/launcher3/LauncherSettings.java | 1 + .../testing/shared/TestProtocol.java | 1 + .../launcher3/ui/TaplTestsLauncher3.java | 4 +++ .../tapl/LauncherInstrumentation.java | 9 ++++++ 7 files changed, 53 insertions(+) create mode 100644 res/xml/default_tapl_test_workspace.xml diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java index a645e58ecc..cf58198c2c 100644 --- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java +++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java @@ -221,6 +221,12 @@ public class DebugTestInformationHandler extends TestInformationHandler { return response; } + case TestProtocol.REQUEST_USE_TAPL_WORKSPACE_LAYOUT: { + useTestWorkspaceLayout( + LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TAPL); + return response; + } + case TestProtocol.REQUEST_USE_DEFAULT_WORKSPACE_LAYOUT: { useTestWorkspaceLayout(null); return response; diff --git a/res/xml/default_tapl_test_workspace.xml b/res/xml/default_tapl_test_workspace.xml new file mode 100644 index 0000000000..24d76f3b1a --- /dev/null +++ b/res/xml/default_tapl_test_workspace.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 767c3d986e..1368de9fc5 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -107,6 +107,7 @@ public class LauncherProvider extends ContentProvider { private static final int TEST_WORKSPACE_LAYOUT_RES_XML = R.xml.default_test_workspace; private static final int TEST2_WORKSPACE_LAYOUT_RES_XML = R.xml.default_test2_workspace; + private static final int TAPL_WORKSPACE_LAYOUT_RES_XML = R.xml.default_tapl_test_workspace; static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; @@ -410,6 +411,9 @@ public class LauncherProvider extends ContentProvider { case LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TEST2: mDefaultWorkspaceLayoutOverride = TEST2_WORKSPACE_LAYOUT_RES_XML; break; + case LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TAPL: + mDefaultWorkspaceLayoutOverride = TAPL_WORKSPACE_LAYOUT_RES_XML; + break; default: mDefaultWorkspaceLayoutOverride = 0; break; diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 66ea616ffe..cef00d9e7d 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -389,6 +389,7 @@ public class LauncherSettings { "set_use_test_workspace_layout_flag"; public static final String ARG_DEFAULT_WORKSPACE_LAYOUT_TEST = "default_test_workspace"; public static final String ARG_DEFAULT_WORKSPACE_LAYOUT_TEST2 = "default_test2_workspace"; + public static final String ARG_DEFAULT_WORKSPACE_LAYOUT_TAPL = "default_tapl_workspace"; public static final String METHOD_CLEAR_USE_TEST_WORKSPACE_LAYOUT_FLAG = "clear_use_test_workspace_layout_flag"; diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java index c69ec2cc26..f036b3e397 100644 --- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java +++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java @@ -107,6 +107,7 @@ public final class TestProtocol { public static final String REQUEST_CLEAR_DATA = "clear-data"; public static final String REQUEST_USE_TEST_WORKSPACE_LAYOUT = "use-test-workspace-layout"; public static final String REQUEST_USE_TEST2_WORKSPACE_LAYOUT = "use-test2-workspace-layout"; + public static final String REQUEST_USE_TAPL_WORKSPACE_LAYOUT = "use-tapl-workspace-layout"; public static final String REQUEST_USE_DEFAULT_WORKSPACE_LAYOUT = "use-default-workspace-layout"; public static final String REQUEST_HOTSEAT_ICON_NAMES = "get-hotseat-icon-names"; diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 5f516eb7fc..fef1708cb5 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -225,6 +225,10 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { @Test @ScreenRecord // b/202433017 public void testWorkspace() throws Exception { + // Make sure there is an instance of chrome on the hotseat + mLauncher.useTaplWorkspaceLayoutOnReload(); + clearLauncherData(); + final Workspace workspace = mLauncher.getWorkspace(); // Test that ensureWorkspaceIsScrollable adds a page by dragging an icon there. diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 52994a50b4..5c4b707190 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -1860,6 +1860,15 @@ public final class LauncherInstrumentation { getTestInfo(TestProtocol.REQUEST_USE_TEST2_WORKSPACE_LAYOUT); } + + /** + * Reloads the workspace with a test layout that includes the chrome Activity app icon on the + * hotseat. + */ + public void useTaplWorkspaceLayoutOnReload() { + getTestInfo(TestProtocol.REQUEST_USE_TAPL_WORKSPACE_LAYOUT); + } + /** Reloads the workspace with the default layout defined by the user's grid size selection. */ public void useDefaultWorkspaceLayoutOnReload() { getTestInfo(TestProtocol.REQUEST_USE_DEFAULT_WORKSPACE_LAYOUT); From cf88ea1e62f9948dfd6f6fdd5f675619ae4e1958 Mon Sep 17 00:00:00 2001 From: Brandon Dayauon Date: Fri, 6 Jan 2023 12:38:55 -0800 Subject: [PATCH 10/11] Support two line text in AllApps/OnDeviceSearch w/ feature flag Made separate feature flag for on device search Add unit test to test twoLine string - Unit tests for testing newStringThatShouldSupportTwoLineText() in BubbleTextView.java. This class tests a couple of strings and uses the getLineCount() to determine if the test passes. Verifying with getLineCount() is sufficient since BubbleTextView can only be in one line or two lines, and this is enough to ensure whether the string should be specifically wrapped onto the second line and to ensure truncation. bug: 201388851 test: presubmit, ran locally on big and small device, before: https://screenshot.googleplex.com/3Q6pwveFDZqxDXL (ORIGINAL TWO LINE TEXT) after: https://screenshot.googleplex.com/7pkwUto6HGzMYoT Change-Id: I93e6ed179e1081d5cdffc6db9c7ae34de8021c24 --- res/layout/all_apps_icon_twoline.xml | 1 - src/com/android/launcher3/BubbleTextView.java | 121 +++++++- .../launcher3/allapps/BaseAllAppsAdapter.java | 8 +- .../launcher3/config/FeatureFlags.java | 4 + .../search/StringMatcherUtility.java | 48 +++- .../search/StringMatcherUtilityTest.java | 95 +++++++ .../launcher3/ui/BubbleTextViewTest.java | 264 ++++++++++++++++++ 7 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 tests/src/com/android/launcher3/ui/BubbleTextViewTest.java diff --git a/res/layout/all_apps_icon_twoline.xml b/res/layout/all_apps_icon_twoline.xml index 54c714734c..b0d02c15de 100644 --- a/res/layout/all_apps_icon_twoline.xml +++ b/res/layout/all_apps_icon_twoline.xml @@ -19,7 +19,6 @@ android:id="@+id/icon" android:singleLine="false" android:lines="2" - android:inputType="textMultiLine" launcher:iconDisplay="all_apps" launcher:centerVertically="true" /> diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 3eb03edf13..352163e921 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -52,8 +52,10 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.accessibility.BaseAccessibilityDelegate; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.dragndrop.DragOptions.PreDragCondition; import com.android.launcher3.dragndrop.DraggableView; @@ -71,6 +73,8 @@ import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.util.MultiTranslateDelegate; +import com.android.launcher3.search.StringMatcherUtility; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.views.ActivityContext; @@ -97,11 +101,19 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private static final float MIN_LETTER_SPACING = -0.05f; private static final int MAX_SEARCH_LOOP_COUNT = 20; + private static final Character NEW_LINE = '\n'; + private static final String EMPTY = ""; + private static final StringMatcherUtility.StringMatcher MATCHER = + StringMatcherUtility.StringMatcher.getInstance(); private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed}; private float mScaleForReorderBounce = 1f; + private IntArray mBreakPointsIntArray; + private CharSequence mLastOriginalText; + private CharSequence mLastModifiedText; + private static final Property DOT_SCALE_PROPERTY = new Property(Float.TYPE, "dotScale") { @Override @@ -134,7 +146,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private FastBitmapDrawable mIcon; private boolean mCenterVertically; - protected final int mDisplay; + protected int mDisplay; private final CheckLongPressHelper mLongPressHelper; @@ -255,6 +267,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mDotParams.scale = 0f; mForceHideDot = false; setBackground(null); + setSingleLine(true); + setMaxLines(1); setTag(null); if (mIconLoadRequest != null) { @@ -382,8 +396,15 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } @UiThread - private void applyLabel(ItemInfoWithIcon info) { - setText(info.title); + @VisibleForTesting + public void applyLabel(ItemInfoWithIcon info) { + CharSequence label = info.title; + if (label != null) { + mLastOriginalText = label; + mLastModifiedText = mLastOriginalText; + mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER); + setText(label); + } if (info.contentDescription != null) { setContentDescription(info.isDisabled() ? getContext().getString(R.string.disabled_app_label, info.contentDescription) @@ -391,6 +412,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } } + /** This is used for testing to forcefully set the display to ALL_APPS */ + @VisibleForTesting + public void setDisplayAllApps() { + mDisplay = DISPLAY_ALL_APPS; + } + /** * Overrides the default long press timeout. */ @@ -637,6 +664,27 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), getPaddingBottom()); } + // only apply two line for all_apps + if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() && (mLastOriginalText != null) + && (mDisplay == DISPLAY_ALL_APPS)) { + CharSequence modifiedString = modifyTitleToSupportMultiLine( + MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft() + - getCompoundPaddingRight(), + mLastOriginalText, + getPaint(), mBreakPointsIntArray); + if (!TextUtils.equals(modifiedString, mLastModifiedText)) { + mLastModifiedText = modifiedString; + setText(modifiedString); + // if text contains NEW_LINE, set max lines to 2 + if (TextUtils.indexOf(modifiedString, NEW_LINE) != -1) { + setSingleLine(false); + setMaxLines(2); + } else { + setSingleLine(true); + setMaxLines(1); + } + } + } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @@ -697,6 +745,73 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha); } + /** + * Generate a new string that will support two line text depending on the current string. + * This method calculates the limited width of a text view and creates a string to fit as + * many words as it can until the limit is reached. Once the limit is reached, we decide to + * either return the original title or continue on a new line. How to get the new string is by + * iterating through the list of break points and determining if the strings between the break + * points can fit within the line it is in. + * Example assuming each character takes up one spot: + * title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7 + * We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery, + * now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth + * at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking + * if the first char is a SPACE, we trim to append "Stats". So resulting string would be + * "Battery\nStats" + */ + public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, CharSequence title, + TextPaint paint, IntArray breakPoints) { + // current title is less than the width allowed so we can just skip + if (title == null || paint.measureText(title, 0, title.length()) <= limitedWidth) { + return title; + } + float currentWordWidth, runningWidth = 0; + CharSequence currentWord; + StringBuilder newString = new StringBuilder(); + int stringPtr = 0; + for (int i = 0; i < breakPoints.size()+1; i++) { + if (i < breakPoints.size()) { + currentWord = title.subSequence(stringPtr, breakPoints.get(i)+1); + } else { + // last word from recent breakpoint until the end of the string + currentWord = title.subSequence(stringPtr, title.length()); + } + currentWordWidth = paint.measureText(currentWord,0, currentWord.length()); + runningWidth += currentWordWidth; + if (runningWidth <= limitedWidth) { + newString.append(currentWord); + } else { + // there is no more space + if (i == 0) { + // if the first words exceeds width, just return as the first line will ellipse + return title; + } else { + // If putting word onto a new line, make sure there is no space or new line + // character in the beginning of the current word and just put in the rest of + // the characters. + CharSequence lastCharacters = title.subSequence(stringPtr, title.length()); + int beginningLetterType = + Character.getType(Character.codePointAt(lastCharacters,0)); + if (beginningLetterType == Character.SPACE_SEPARATOR + || beginningLetterType == Character.LINE_SEPARATOR) { + lastCharacters = lastCharacters.length() > 1 + ? lastCharacters.subSequence(1, lastCharacters.length()) + : EMPTY; + } + newString.append(NEW_LINE).append(lastCharacters); + return newString.toString(); + } + } + if (i >= breakPoints.size()) { + // no need to look forward into the string if we've already finished processing + break; + } + stringPtr = breakPoints.get(i)+1; + } + return newString.toString(); + } + @Override public void cancelLongPress() { super.cancelLongPress(); diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 7040de5075..8fa42765b0 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.R; import com.android.launcher3.allapps.search.SearchAdapterProvider; +import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.views.ActivityContext; @@ -140,7 +141,7 @@ public abstract class BaseAllAppsAdapter ex protected final OnClickListener mOnIconClickListener; protected OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS; protected OnFocusChangeListener mIconFocusListener; - private final int mExtraHeight; + private final int mExtraTextHeight; public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater, AlphabeticalAppsList apps, SearchAdapterProvider adapterProvider) { @@ -152,7 +153,8 @@ public abstract class BaseAllAppsAdapter ex mOnIconClickListener = mActivityContext.getItemOnClickListener(); mAdapterProvider = adapterProvider; - mExtraHeight = res.getDimensionPixelSize(R.dimen.all_apps_height_extra); + mExtraTextHeight = Utilities.calculateTextHeight( + mActivityContext.getDeviceProfile().allAppsIconTextSizePx); } /** @@ -197,7 +199,7 @@ public abstract class BaseAllAppsAdapter ex icon.getLayoutParams().height = mActivityContext.getDeviceProfile().allAppsCellHeightPx; if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()) { - icon.getLayoutParams().height += mExtraHeight; + icon.getLayoutParams().height += mExtraTextHeight; } return new ViewHolder(icon); case VIEW_TYPE_EMPTY_SEARCH: diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 6bb2a0f0a8..d1aaef1aca 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -113,6 +113,10 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937, "ENABLE_TWOLINE_ALLAPPS", false, "Enables two line label inside all apps."); + public static final BooleanFlag ENABLE_TWOLINE_DEVICESEARCH = getDebugFlag(201388851, + "ENABLE_TWOLINE_DEVICESEARCH", false, + "Enable two line label for icons with labels on device search."); + public static final BooleanFlag ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING = getReleaseFlag( 270391397, "ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING", false, "Allows on device search in all apps logging"); diff --git a/src/com/android/launcher3/search/StringMatcherUtility.java b/src/com/android/launcher3/search/StringMatcherUtility.java index c66f3a19e4..28fc4f002b 100644 --- a/src/com/android/launcher3/search/StringMatcherUtility.java +++ b/src/com/android/launcher3/search/StringMatcherUtility.java @@ -16,13 +16,20 @@ package com.android.launcher3.search; +import android.text.TextUtils; + +import com.android.launcher3.util.IntArray; + import java.text.Collator; +import java.util.stream.IntStream; /** * Utilities for matching query string to target string. */ public class StringMatcherUtility { + private static final Character SPACE = ' '; + /** * Returns {@code true} if {@code query} is a prefix of a substring in {@code target}. How to * break target to valid substring is defined in the given {@code matcher}. @@ -58,6 +65,41 @@ public class StringMatcherUtility { return false; } + /** + * Returns a list of breakpoints wherever the string contains a break. For example: + * "t-mobile" would have breakpoints at [0, 1] + * "Agar.io" would have breakpoints at [3, 4] + * "LEGO®Builder" would have a breakpoint at [4] + */ + public static IntArray getListOfBreakpoints(CharSequence input, StringMatcher matcher) { + int inputLength = input.length(); + if ((inputLength <= 2) || TextUtils.indexOf(input, SPACE) != -1) { + // when there is a space in the string, return a list where the elements are the + // position of the spaces - 1. This is to make the logic consistent where breakpoints + // are placed + return IntArray.wrap(IntStream.range(0, inputLength) + .filter(i -> input.charAt(i) == SPACE) + .map(i -> i - 1) + .toArray()); + } + IntArray listOfBreakPoints = new IntArray(); + int prevType; + int thisType = Character.getType(Character.codePointAt(input, 0)); + int nextType = Character.getType(Character.codePointAt(input, 1)); + for (int i = 1; i < inputLength; i++) { + prevType = thisType; + thisType = nextType; + nextType = i < (inputLength - 1) + ? Character.getType(Character.codePointAt(input, i + 1)) + : Character.UNASSIGNED; + if (matcher.isBreak(thisType, prevType, nextType)) { + // breakpoint is at previous + listOfBreakPoints.add(i-1); + } + } + return listOfBreakPoints; + } + /** * Performs locale sensitive string comparison using {@link Collator}. */ @@ -118,7 +160,11 @@ public class StringMatcherUtility { } switch (thisType) { case Character.UPPERCASE_LETTER: - if (nextType == Character.UPPERCASE_LETTER) { + // takes care of the case where there are consistent uppercase letters as well + // as a special symbol following the capitalize letters for example: LEGO® + if (nextType != Character.UPPERCASE_LETTER && nextType != Character.OTHER_SYMBOL + && nextType != Character.DECIMAL_DIGIT_NUMBER + && nextType != Character.UNASSIGNED) { return true; } // Follow through diff --git a/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java index 3b53255550..0ba46f4f55 100644 --- a/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java +++ b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java @@ -15,8 +15,10 @@ */ package com.android.launcher3.search; +import static com.android.launcher3.search.StringMatcherUtility.getListOfBreakpoints; import static com.android.launcher3.search.StringMatcherUtility.matches; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -25,6 +27,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.search.StringMatcherUtility.StringMatcher; import com.android.launcher3.search.StringMatcherUtility.StringMatcherSpace; +import com.android.launcher3.util.IntArray; import org.junit.Test; import org.junit.runner.RunWith; @@ -138,4 +141,96 @@ public class StringMatcherUtilityTest { assertFalse(matches("phant", "elephant", MATCHER_SPACE)); assertFalse(matches("elephants", "elephant", MATCHER_SPACE)); } + + @Test + public void testStringWithProperBreaks() { + // empty string + assertEquals(IntArray.wrap(), getListOfBreakpoints("", MATCHER)); + + // should be "D Dz" that's why breakpoint is at 0 + assertEquals(IntArray.wrap(0), getListOfBreakpoints("DDz", MATCHER)); + + // test all caps and all lower-case + assertEquals(IntArray.wrap(), getListOfBreakpoints("SNKRS", MATCHER)); + assertEquals(IntArray.wrap(), getListOfBreakpoints("flutterappflorafy", MATCHER)); + assertEquals(IntArray.wrap(), getListOfBreakpoints("LEGO®", MATCHER)); + + // test camel case + // breakpoint at 9 to be "flutterapp Florafy" + assertEquals(IntArray.wrap(9), getListOfBreakpoints("flutterappFlorafy", MATCHER)); + // breakpoint at 4 to be "Metro Zone" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("MetroZone", MATCHER)); + // breakpoint at 4,5 to be "metro X Zone" + assertEquals(IntArray.wrap(4,5), getListOfBreakpoints("metroXZone", MATCHER)); + // breakpoint at 0 to be "G Pay" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("GPay", MATCHER)); + // breakpoint at 4 to be "Whats App" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("WhatsApp", MATCHER)); + // breakpoint at 2 to be "aaa A" + assertEquals(IntArray.wrap(2), getListOfBreakpoints("aaaA", MATCHER)); + // breakpoint at 4,12,16 to be "Other Launcher Test App" + assertEquals(IntArray.wrap(4,12,16), + getListOfBreakpoints("OtherLauncherTestApp", MATCHER)); + + // test with TITLECASE_LETTER + // should be "DDz" that's why there are no break points + assertEquals(IntArray.wrap(), getListOfBreakpoints("DDz", MATCHER)); + // breakpoint at 0 to be "D DDž" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("DDDž", MATCHER)); + // breakpoint at 0 because there is a space to be "Dž DD" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("Dž DD", MATCHER)); + // breakpoint at 1 to be "Dw Dz" + assertEquals(IntArray.wrap(1), getListOfBreakpoints("DwDz", MATCHER)); + // breakpoint at 0,2 to be "Dw Dz" + assertEquals(IntArray.wrap(0,2), getListOfBreakpoints("wDwDz", MATCHER)); + // breakpoint at 1,3 to be "ᾋw Dw Dz" + assertEquals(IntArray.wrap(1,3), getListOfBreakpoints("ᾋwDwDz", MATCHER)); + // breakpoint at 0,2,4 to be "ᾋ ᾋw Dw Dz" + assertEquals(IntArray.wrap(0,2,4), getListOfBreakpoints("ᾋᾋwDwDz", MATCHER)); + // breakpoint at 0,2,4 to be "ᾋ ᾋw Dw Dz®" + assertEquals(IntArray.wrap(0,2,4), getListOfBreakpoints("ᾋᾋwDwDz®", MATCHER)); + + // test with numbers and symbols + // breakpoint at 3,11 to be "Test Activity 13" + assertEquals(IntArray.wrap(3,11), getListOfBreakpoints("TestActivity13", MATCHER)); + // breakpoint at 3, 4, 12, 13 as the breakpoints are at the dashes + assertEquals(IntArray.wrap(3,4,12,13), + getListOfBreakpoints("Test-Activity-12", MATCHER)); + // breakpoint at 1 to be "AA 2" + assertEquals(IntArray.wrap(1), getListOfBreakpoints("AA2", MATCHER)); + // breakpoint at 1 to be "AAA 2" + assertEquals(IntArray.wrap(2), getListOfBreakpoints("AAA2", MATCHER)); + // breakpoint at 1 to be "ab 2" + assertEquals(IntArray.wrap(1), getListOfBreakpoints("ab2", MATCHER)); + // breakpoint at 1,2 to be "el 3 suhwee" + assertEquals(IntArray.wrap(1,2), getListOfBreakpoints("el3suhwee", MATCHER)); + // breakpoint at 0,1 as the breakpoints are at '-' + assertEquals(IntArray.wrap(0,1), getListOfBreakpoints("t-mobile", MATCHER)); + assertEquals(IntArray.wrap(0,1), getListOfBreakpoints("t-Mobile", MATCHER)); + // breakpoint at 0,1,2 as the breakpoints are at '-' + assertEquals(IntArray.wrap(0,1,2), getListOfBreakpoints("t--Mobile", MATCHER)); + // breakpoint at 1,2,3 as the breakpoints are at '-' + assertEquals(IntArray.wrap(1,2,3), getListOfBreakpoints("tr--Mobile", MATCHER)); + // breakpoint at 3,4 as the breakpoints are at '.' + assertEquals(IntArray.wrap(3,4), getListOfBreakpoints("Agar.io", MATCHER)); + assertEquals(IntArray.wrap(3,4), getListOfBreakpoints("Hole.Io", MATCHER)); + + // breakpoint at 0 to be "µ Torrent®" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("µTorrent®", MATCHER)); + // breakpoint at 4 to be "LEGO® Builder" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("LEGO®Builder", MATCHER)); + // breakpoint at 4 to be "LEGO® builder" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("LEGO®builder", MATCHER)); + // breakpoint at 4 to be "lego® builder" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("lego®builder", MATCHER)); + + // test string with spaces - where the breakpoints are right before where the spaces are at + assertEquals(IntArray.wrap(3,8), getListOfBreakpoints("HEAD BALL 2", MATCHER)); + assertEquals(IntArray.wrap(2,8), + getListOfBreakpoints("OFL Agent Application", MATCHER)); + assertEquals(IntArray.wrap(0,2), getListOfBreakpoints("D D z", MATCHER)); + assertEquals(IntArray.wrap(6), getListOfBreakpoints("Battery Stats", MATCHER)); + assertEquals(IntArray.wrap(5,9,15), + getListOfBreakpoints("System UWB Field Test", MATCHER)); + } } diff --git a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java new file mode 100644 index 0000000000..528f7ac57d --- /dev/null +++ b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java @@ -0,0 +1,264 @@ +/* + * 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.ui; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.config.FeatureFlags.ENABLE_TWOLINE_ALLAPPS; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.graphics.Typeface; +import android.view.ViewGroup; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Utilities; +import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.search.StringMatcherUtility; +import com.android.launcher3.util.ActivityContextWrapper; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.TestUtil; +import com.android.launcher3.views.BaseDragLayer; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for testing modifyTitleToSupportMultiLine() in BubbleTextView.java + * This class tests a couple of strings and uses the getMaxLines() to determine if the test passes. + * Verifying with getMaxLines() is sufficient since BubbleTextView can only be in one line or + * two lines, and this is enough to ensure whether the string should be specifically wrapped onto + * the second line and to ensure truncation. + */ +public class BubbleTextViewTest { + + private static final StringMatcherUtility.StringMatcher + MATCHER = StringMatcherUtility.StringMatcher.getInstance(); + private static final int ONE_LINE = 1; + private static final int TWO_LINE = 2; + private static final String TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT = "Battery Stats"; + private static final String TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT = + "Battery\nStats"; + private static final String TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT = + "flutterappflorafy"; + private static final String TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT = + "System UWB Field Test"; + private static final String TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT = + "System\nUWB Field Test"; + private static final String TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT = + "LEGO®Builder"; + private static final String TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT_RESULT = + "LEGO®\nBuilder"; + private static final String EMPTY_STRING = ""; + private static final int CHAR_CNT = 7; + + private BubbleTextView mBubbleTextView; + private ItemInfoWithIcon mItemInfoWithIcon; + private Context mContext; + private int mLimitedWidth; + + @Before + public void setUp() throws Exception { + Utilities.enableRunningInTestHarnessForTests(); + mContext = new ActivityContextWrapper(getApplicationContext()); + mBubbleTextView = new BubbleTextView(mContext); + mBubbleTextView.reset(); + mBubbleTextView.setDisplayAllApps(); + assertEquals(ONE_LINE, mBubbleTextView.getMaxLines()); + + BubbleTextView testView = new BubbleTextView(mContext); + testView.setTypeface(Typeface.MONOSPACE); + testView.setText("B"); + // calculate the maxWidth of the textView by calculating the width of one monospace + // character * CHAR_CNT + mLimitedWidth = + (int) (testView.getPaint().measureText(testView.getText().toString()) * CHAR_CNT); + // needed otherwise there is a NPE during setText() on checkForRelayout() + mBubbleTextView.setLayoutParams( + new ViewGroup.LayoutParams(mLimitedWidth, + BaseDragLayer.LayoutParams.WRAP_CONTENT)); + mItemInfoWithIcon = new ItemInfoWithIcon() { + @Override + public ItemInfoWithIcon clone() { + return null; + } + }; + } + + @Test + public void testEmptyString_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + mItemInfoWithIcon.title = EMPTY_STRING; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getMaxLines()); + } + + @Test + public void testEmptyString_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + mItemInfoWithIcon.title = EMPTY_STRING; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testStringWithSpaceLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "Battery Stats" + mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(TWO_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testStringWithSpaceLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "Battery Stats" + mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringNoSpaceLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "flutterappflorafy" + mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringNoSpaceLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "flutterappflorafy" + mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringWithSpaceLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "System UWB Field Test" + mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(TWO_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringWithSpaceLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "System UWB Field Test" + mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringSymbolLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "LEGO®Builder" + mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(TWO_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringSymbolLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "LEGO®Builder" + mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT() { + // test string: "Battery Stats" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT, newString); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT() { + // test string: "flutterappflorafy" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, newString); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT() { + // test string: "System UWB Field Test" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT, newString); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT() { + // test string: "LEGO®Builder" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT_RESULT, newString); + } +} From 5d8489e71e3f3ede301af695b9793078df1b91ae Mon Sep 17 00:00:00 2001 From: Charlie Anderson Date: Thu, 9 Mar 2023 18:25:33 -0500 Subject: [PATCH 11/11] flip app shortcut popup design on for U Bug: 247880037 Test: N/A Change-Id: I3c1410024bd4ffe13b436344de38b6018d4ce3f1 --- src/com/android/launcher3/config/FeatureFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index e8ef3427d9..e5cdd6e56c 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -302,7 +302,7 @@ public final class FeatureFlags { + " is enabled or in prefix state"); public static final BooleanFlag ENABLE_MATERIAL_U_POPUP = getDebugFlag(270395516, - "ENABLE_MATERIAL_U_POPUP", false, "Switch popup UX to use material U"); + "ENABLE_MATERIAL_U_POPUP", true, "Switch popup UX to use material U"); public static final BooleanFlag ENABLE_SEARCH_UNINSTALLED_APPS = getReleaseFlag(270395269, "ENABLE_SEARCH_UNINSTALLED_APPS", false, "Search uninstalled app results.");