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/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 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/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index f5d7f3b9bc..8c217784ac 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) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 11b07e886d..e66856a4d8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -94,6 +94,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private float mTransientTaskbarAllAppsButtonTranslationXOffset; + private final boolean mStartAlignTaskbar; + public TaskbarView(@NonNull Context context) { this(context, null); } @@ -121,6 +123,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; @@ -372,10 +376,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/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, + 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 c9585cbe46..00d6571f3c 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -83,6 +83,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; @@ -97,6 +98,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 */ @@ -117,6 +123,7 @@ public class SplitSelectStateController { mStateManager = stateManager; mDepthController = depthController; mRecentTasksModel = recentsModel; + mSplitAnimationController = new SplitAnimationController(this); } /** @@ -401,6 +408,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 */ @@ -508,6 +527,7 @@ public class SplitSelectStateController { mItemInfo = null; mSplitEvent = null; mAnimateCurrentTaskDismissal = false; + mDismissingFromSplitPair = false; } /** @@ -534,6 +554,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/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 0b83eafa23..0d61bc85f8 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -272,9 +272,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { */ public RectF getCurrentCropRect() { // Crop rect is the inverse of thumbnail matrix - RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets; - mTempRectF.set(-insets.left, -insets.top, - mTaskRect.width() + insets.right, mTaskRect.height() + insets.bottom); + mTempRectF.set(0, 0, mTaskRect.width(), mTaskRect.height()); mInversePositionMatrix.mapRect(mTempRectF); return mTempRectF; } @@ -351,14 +349,10 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { /* taskViewScale= */1f, mTaskRect.width(), mDp, mPositionHelper); // Apply thumbnail matrix - RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets; - float scale = mCurrentFullscreenParams.mScale; float taskWidth = mTaskRect.width(); float taskHeight = mTaskRect.height(); mMatrix.set(mPositionHelper.getMatrix()); - mMatrix.postTranslate(insets.left, insets.top); - mMatrix.postScale(scale, scale); // Apply TaskView matrix: taskRect, translate mMatrix.postTranslate(mTaskRect.left, mTaskRect.top); @@ -378,8 +372,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { applyWindowToHomeRotation(mMatrix); // Crop rect is the inverse of thumbnail matrix - mTempRectF.set(-insets.left, -insets.top, - taskWidth + insets.right, taskHeight + insets.bottom); + mTempRectF.set(0, 0, taskWidth, taskHeight); mInversePositionMatrix.mapRect(mTempRectF); mTempRectF.roundOut(mTmpCropRect); @@ -389,7 +382,6 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { return; } Log.d(TAG, "progress: " + fullScreenProgress - + " scale: " + scale + " recentsViewScale: " + recentsViewScale.value + " crop: " + mTmpCropRect + " radius: " + getCurrentCornerRadius() diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java index 5414068d84..ccc2df61c9 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java @@ -496,7 +496,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/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java index 96504afcd4..af80d5f51d 100644 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java @@ -97,11 +97,6 @@ public final class DigitalWellBeingToast { private View mBanner; private ViewOutlineProvider mOldBannerOutlineProvider; private float mBannerOffsetPercentage; - /** - * Clips rect provided by {@link #mOldBannerOutlineProvider} when in the model state to - * hide this banner as the taskView scales up and down - */ - private float mModalOffset = 0f; @Nullable private SplitBounds mSplitBounds; private int mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; @@ -336,17 +331,15 @@ public final class DigitalWellBeingToast { @Override public void getOutline(View view, Outline outline) { mOldBannerOutlineProvider.getOutline(view, outline); - float verticalTranslation = -view.getTranslationY() + mModalOffset - + mSplitOffsetTranslationY; + float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY; outline.offset(0, Math.round(verticalTranslation)); } }); mBanner.setClipToOutline(true); } - void updateBannerOffset(float offsetPercentage, float verticalOffset) { + void updateBannerOffset(float offsetPercentage) { if (mBanner != null && mBannerOffsetPercentage != offsetPercentage) { - mModalOffset = verticalOffset; mBannerOffsetPercentage = offsetPercentage; updateTranslationY(); mBanner.invalidateOutline(); @@ -359,10 +352,7 @@ public final class DigitalWellBeingToast { } mBanner.setTranslationY( - (mBannerOffsetPercentage * mBanner.getHeight()) + - mModalOffset + - mSplitOffsetTranslationY - ); + (mBannerOffsetPercentage * mBanner.getHeight()) + mSplitOffsetTranslationY); } private void updateTranslationX() { diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java index e9498fd0f9..5bfd0350b5 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(); } @@ -354,9 +391,7 @@ public class GroupedTaskView extends TaskView { // Value set by super call float scale = mIconView.getAlpha(); mIconView2.setAlpha(scale); - mDigitalWellBeingToast2.updateBannerOffset(1f - scale, - mCurrentFullscreenParams.mCurrentDrawnInsets.top - + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom); + mDigitalWellBeingToast2.updateBannerOffset(1f - scale); } @Override @@ -379,21 +414,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 6d3b35cc27..d02ae728dd 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -184,6 +184,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; @@ -192,6 +193,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; @@ -935,7 +937,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); @@ -4450,7 +4449,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 { @@ -4608,7 +4635,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); @@ -293,16 +338,9 @@ public class TaskThumbnailView extends View { @Override protected void onDraw(Canvas canvas) { - RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets; canvas.save(); - canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale); - canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top); // Draw the insets if we're being drawn fullscreen (we do this for quick switch). - drawOnCanvas(canvas, - -currentDrawnInsets.left, - -currentDrawnInsets.top, - getMeasuredWidth() + currentDrawnInsets.right, - getMeasuredHeight() + currentDrawnInsets.bottom, + drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mFullscreenParams.mCurrentDrawnCornerRadius); canvas.restore(); } @@ -342,10 +380,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 +398,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 +443,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 +471,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 +482,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; @@ -424,9 +499,7 @@ public class TaskThumbnailView extends View { return false; } - RectF insets = mPreviewPositionHelper.getClippedInsets(); - float thumbnailViewAspect = (getWidth() + insets.left + insets.right) - / (getHeight() + insets.top + insets.bottom); + float thumbnailViewAspect = getWidth() / (float) getHeight(); float thumbnailDataAspect = mThumbnailData.thumbnail.getWidth() / (float) mThumbnailData.thumbnail.getHeight(); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index ab72f2d10e..df90583033 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; @@ -82,7 +84,6 @@ import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; @@ -559,9 +560,7 @@ public class TaskView extends FrameLayout implements Reusable { } mModalness = modalness; mIconView.setAlpha(1 - modalness); - mDigitalWellBeingToast.updateBannerOffset(modalness, - mCurrentFullscreenParams.mCurrentDrawnInsets.top - + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom); + mDigitalWellBeingToast.updateBannerOffset(modalness); } public DigitalWellBeingToast getDigitalWellBeingToast() { @@ -706,9 +705,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 +720,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 +768,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 +1107,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); @@ -1120,9 +1145,7 @@ public class TaskView extends FrameLayout implements Reusable { float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp) .getInterpolation(progress); mIconView.setAlpha(scale); - mDigitalWellBeingToast.updateBannerOffset(1f - scale, - mCurrentFullscreenParams.mCurrentDrawnInsets.top - + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom); + mDigitalWellBeingToast.updateBannerOffset(1f - scale); } public void setIconScaleAnimStartProgress(float startProgress) { @@ -1179,6 +1202,7 @@ public class TaskView extends FrameLayout implements Reusable { setAlpha(mStableAlpha); setIconScaleAndDim(1); setColorTint(0, 0); + mSnapshotView.resetViewTransforms(); } public void setStableAlpha(float parentAlpha) { @@ -1720,10 +1744,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) { @@ -1740,17 +1766,11 @@ public class TaskView extends FrameLayout implements Reusable { private final float mCornerRadius; private final float mWindowCornerRadius; - public RectF mCurrentDrawnInsets = new RectF(); public float mCurrentDrawnCornerRadius; - /** The current scale we apply to the thumbnail to adjust for new left/right insets. */ - public float mScale = 1; - - private boolean mIsTaskbarTransient; public FullscreenDrawParams(Context context) { mCornerRadius = TaskCornerRadius.get(context); mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context); - mIsTaskbarTransient = DisplayController.isTransientTaskbar(context); mCurrentDrawnCornerRadius = mCornerRadius; } @@ -1760,36 +1780,9 @@ public class TaskView extends FrameLayout implements Reusable { */ public void setProgress(float fullscreenProgress, float parentScale, float taskViewScale, int previewWidth, DeviceProfile dp, PreviewPositionHelper pph) { - RectF insets = getInsetsToDrawInFullscreen(pph, dp, mIsTaskbarTransient); - - float currentInsetsLeft = insets.left * fullscreenProgress; - float currentInsetsTop = insets.top * fullscreenProgress; - float currentInsetsRight = insets.right * fullscreenProgress; - float currentInsetsBottom = insets.bottom * fullscreenProgress; - mCurrentDrawnInsets.set( - currentInsetsLeft, currentInsetsTop, currentInsetsRight, currentInsetsBottom); - mCurrentDrawnCornerRadius = Utilities.mapRange(fullscreenProgress, mCornerRadius, mWindowCornerRadius) / parentScale / taskViewScale; - - // We scaled the thumbnail to fit the content (excluding insets) within task view width. - // Now that we are drawing left/right insets again, we need to scale down to fit them. - if (previewWidth > 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/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/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/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/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 3eb03edf13..801b740ca1 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(); @@ -717,7 +832,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); } } } 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; diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 767c3d986e..e688709ef2 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; @@ -550,39 +554,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 +964,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/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/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 e8ef3427d9..5a1dd48f72 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"); @@ -302,7 +306,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."); diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index 5789a6c9c8..6450c8e52a 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -22,8 +22,10 @@ import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 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.content.Intent; @@ -56,7 +58,6 @@ import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; import com.android.launcher3.uioverrides.ApiWrapper; 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; @@ -368,12 +369,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/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/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java index 628aa9a28d..050e88f5bc 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; @@ -635,9 +634,11 @@ 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; + float scale = (float) totalThumbnailHeight / dp.availableHeightPx; float topTaskHeight = dp.availableHeightPx * taskPercent; float finalDividerHeight = dividerBar * scale; float scaledTopTaskHeight = topTaskHeight * scale; @@ -669,6 +670,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 +704,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()) { 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); } /** 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 */ 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/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); + } +} diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 4f1e9bfa26..228f57dc6b 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -231,6 +231,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 6b5305fe1b..b32fd9b764 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -1865,6 +1865,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);