Snap for 9728260 from 9abfa85900 to udc-release

Change-Id: I5acb4b7d22c0ab8c2fce06d8cfeecf51420eb084
This commit is contained in:
Android Build Coastguard Worker
2023-03-11 04:29:12 +00:00
39 changed files with 1383 additions and 325 deletions
@@ -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;
+148
View File
@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
<issue
id="NewApi"
message="Call requires API level 28 (current min is 26): `android.app.Person#getKey`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
line="72"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/util/DynamicResource.java"
line="73"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 30 (current min is 26): `android.graphics.Outline#setPath`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/popup/RoundedArrowDrawable.java"
line="114"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 30 (current min is 26): `android.view.View#getWindowInsetsController`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
line="902"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 30 (current min is 26): `android.view.WindowInsets.Type#ime`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
line="902"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 30 (current min is 26): `android.view.WindowInsetsController#hide`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
line="902"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#resetColorResources`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
line="114"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 31 (current min is 30): `android.appwidget.AppWidgetHostView#setColorResources`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
line="415"/>
</issue>
<issue
id="NewApi"
message="Field requires API level 28 (current min is 26): `android.appwidget.AppWidgetProviderInfo#widgetFeatures`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetAddFlowHandler.java"
line="93"/>
</issue>
<issue
id="NewApi"
message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
line="1814"/>
</issue>
<issue
id="NewApi"
message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
line="1824"/>
</issue>
<issue
id="NewApi"
message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
line="1815"/>
</issue>
<issue
id="NewApi"
message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
line="1823"/>
</issue>
<issue
id="NewApi"
message="Method reference requires API level 28 (current min is 26): `Person::getKey`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/model/data/WorkspaceItemInfo.java"
line="195"/>
</issue>
<issue
id="NewApi"
message="Method reference requires API level 28 (current min is 26): `Person::getKey`">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
line="73"/>
</issue>
<issue
id="NewApi"
message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)">
<location
file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
line="6"/>
</issue>
<issue
id="NewApi"
message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)">
<location
file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
line="7"/>
</issue>
<issue
id="NewApi"
message="`@android:dimen/system_app_widget_background_radius` requires API level 31 (current min is 26)">
<location
file="packages/apps/Launcher3/res/drawable/widget_resize_frame.xml"
line="20"/>
</issue>
</issues>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Applies to large tablet screens portrait -->
<resources>
<!-- Taskbar -->
<!-- Align the Taskbar to the start (Left/Right) of the device when 3 button nav is enabled. -->
<bool name="start_align_taskbar">true</bool>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Applies to large tablet screens landscape -->
<resources>
<!-- Taskbar -->
<!-- Align the Taskbar to the start (Left/Right) of the device when 3 button nav is enabled. -->
<bool name="start_align_taskbar">false</bool>
</resources>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Applies to large tablet screens portrait -->
<resources>
<!-- Taskbar -->
<!-- Align the Taskbar to the start (Left/Right) of the device when 3 button nav is enabled. -->
<bool name="start_align_taskbar">true</bool>
</resources>
+4
View File
@@ -51,4 +51,8 @@
</item>
<string name="setup_wizard_pkg" translatable="false" />
<!-- Taskbar -->
<!-- Align the Taskbar to the start (Left/Right) of the device when 3 button nav is enabled. -->
<bool name="start_align_taskbar">false</bool>
</resources>
@@ -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) {
@@ -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;
}
@@ -244,6 +244,7 @@ public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_T
float maxScale = res.getFloat(R.dimen.overview_max_scale);
int taskMargin = dp.overviewTaskMarginPx;
calculateTaskSizeInternal(
context,
dp,
dp.overviewTaskThumbnailTopMarginPx,
dp.getOverviewActionsClaimedSpace(),
@@ -259,10 +260,10 @@ public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_T
float maxScale = res.getFloat(R.dimen.overview_max_scale);
Rect gridRect = new Rect();
calculateGridSize(dp, gridRect);
calculateTaskSizeInternal(dp, gridRect, maxScale, Gravity.CENTER, outRect);
calculateTaskSizeInternal(context, dp, gridRect, maxScale, Gravity.CENTER, outRect);
}
private void calculateTaskSizeInternal(DeviceProfile dp, int claimedSpaceAbove,
private void calculateTaskSizeInternal(Context context, DeviceProfile dp, int claimedSpaceAbove,
int claimedSpaceBelow, int minimumHorizontalPadding, float maxScale, int gravity,
Rect outRect) {
Rect insets = dp.getInsets();
@@ -275,12 +276,12 @@ public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_T
minimumHorizontalPadding,
claimedSpaceBelow);
calculateTaskSizeInternal(dp, potentialTaskRect, maxScale, gravity, outRect);
calculateTaskSizeInternal(context, dp, potentialTaskRect, maxScale, gravity, outRect);
}
private void calculateTaskSizeInternal(DeviceProfile dp,
private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
Rect potentialTaskRect, float maxScale, int gravity, Rect outRect) {
PointF taskDimension = getTaskDimension(dp);
PointF taskDimension = getTaskDimension(context, dp);
float scale = Math.min(
potentialTaskRect.width() / taskDimension.x,
@@ -292,19 +293,19 @@ public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_T
Gravity.apply(gravity, outWidth, outHeight, potentialTaskRect, outRect);
}
private static PointF getTaskDimension(DeviceProfile dp) {
private static PointF getTaskDimension(Context context, DeviceProfile dp) {
PointF dimension = new PointF();
getTaskDimension(dp, dimension);
getTaskDimension(context, dp, dimension);
return dimension;
}
/**
* Gets the dimension of the task in the current system state.
*/
public static void getTaskDimension(DeviceProfile dp, PointF out) {
public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) {
out.x = dp.widthPx;
out.y = dp.heightPx;
if (dp.isTablet) {
if (dp.isTablet && !DisplayController.isTransientTaskbar(context)) {
out.y -= dp.taskbarSize;
}
}
@@ -339,7 +340,7 @@ public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_T
float rowHeight = (potentialTaskRect.height() + dp.overviewTaskThumbnailTopMarginPx
- dp.overviewRowSpacing) / 2f;
PointF taskDimension = getTaskDimension(dp);
PointF taskDimension = getTaskDimension(context, dp);
float scale = (rowHeight - dp.overviewTaskThumbnailTopMarginPx) / taskDimension.y;
int outWidth = Math.round(scale * taskDimension.x);
int outHeight = Math.round(scale * taskDimension.y);
@@ -373,6 +374,7 @@ public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_T
Math.round((dp.availableWidthPx - outRect.width() * maxScale) / 2);
}
calculateTaskSizeInternal(
context,
dp,
dp.overviewTaskMarginPx,
claimedSpaceBelow,
@@ -396,7 +396,7 @@ public class RecentsOrientedState implements
* Returns the scale and pivot so that the provided taskRect can fit the provided full size
*/
public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) {
getTaskDimension(dp, outPivot);
getTaskDimension(mContext, dp, outPivot);
float scale = Math.min(outPivot.x / taskView.width(), outPivot.y / taskView.height());
if (scale == 1) {
outPivot.set(taskView.centerX(), taskView.centerY());
@@ -0,0 +1,163 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.quickstep.util
import android.animation.ObjectAnimator
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.View
import com.android.launcher3.DeviceProfile
import com.android.launcher3.anim.PendingAnimation
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
import com.android.quickstep.views.TaskThumbnailView
import com.android.quickstep.views.TaskView
import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
import java.util.function.Supplier
/**
* Utils class to help run animations for initiating split screen from launcher.
* Will be expanded with future refactors. Works in conjunction with the state stored in
* [SplitSelectStateController]
*/
class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) {
companion object {
// Break this out into maybe enums? Abstractions into its own classes? Tbd.
data class SplitAnimInitProps(
val originalView: View,
val originalBitmap: Bitmap?,
val iconDrawable: Drawable,
val fadeWithThumbnail: Boolean,
val isStagedTask: Boolean,
val iconView: View?
)
}
/**
* Returns different elements to animate for the initial split selection animation
* depending on the state of the surface from which the split was initiated
*/
fun getFirstAnimInitViews(taskViewSupplier: Supplier<TaskView>,
splitSelectSourceSupplier: Supplier<SplitSelectSource>)
: 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))
}
}
}
@@ -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);
}
@@ -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()
@@ -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);
}
@@ -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() {
@@ -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 {
@@ -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<ACTIVITY_TYPE extends StatefulActivity<STATE_T
if (mHandleTaskStackChanges) {
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView != null) {
for (TaskView.TaskIdAttributeContainer container :
for (TaskIdAttributeContainer container :
taskView.getTaskIdAttributeContainers()) {
if (container == null || taskId != container.getTask().key.id) {
continue;
@@ -3099,29 +3101,26 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
RectF startingTaskRect = new RectF();
safeRemoveDragLayerView(mFirstFloatingTaskView);
SplitAnimInitProps splitAnimInitProps =
mSplitSelectStateController.getSplitAnimationController().getFirstAnimInitViews(
() -> 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<ACTIVITY_TYPE extends StatefulActivity<STATE_T
mTaskViewsSecondarySplitTranslation = translation;
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView taskView = requireTaskViewAt(i);
if (taskView == mSplitHiddenTaskView) {
if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) {
continue;
}
taskView.getSecondarySplitTranslationProperty().set(taskView, translation);
@@ -4501,6 +4500,10 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
mSplitSelectStateController
.setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal);
// Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
&& mSplitHiddenTaskView.containsMultipleTasks());
mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
@@ -4519,8 +4522,32 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
* Modifies a PendingAnimation with the animations for entering split staging
*/
public void createSplitSelectInitAnimation(PendingAnimation builder, int duration) {
if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) {
// Splitting from Overview
boolean isInitiatingSplitFromTaskView =
mSplitSelectStateController.isAnimateCurrentTaskDismissal();
boolean isInitiatingTaskViewSplitPair =
mSplitSelectStateController.isDismissingFromSplitPair();
if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair) {
// Splitting from Overview for split pair task
createInitialSplitSelectAnimation(builder);
// Animate pair thumbnail into full thumbnail
boolean primaryTaskSelected =
mSplitHiddenTaskView.getTaskIdAttributeContainers()[0].getTask().key.id ==
mSplitSelectStateController.getInitialTaskId();
TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView
.getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0];
TaskThumbnailView thumbnail = taskIdAttributeContainer.getThumbnailView();
mSplitSelectStateController.getSplitAnimationController()
.addInitialSplitFromPair(taskIdAttributeContainer, builder,
mActivity.getDeviceProfile(),
mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
primaryTaskSelected);
builder.addOnFrameCallback(() ->{
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<ACTIVITY_TYPE extends StatefulActivity<STATE_T
mSecondSplitHiddenView = containerTaskView;
if (mSecondSplitHiddenView != null) {
mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE);
mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE,
mSplitSelectStateController.getSecondTaskId());
}
InteractionJankMonitorWrapper.begin(this,
@@ -4634,7 +4662,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
}
if (mSecondSplitHiddenView != null) {
mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE);
mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
mSecondSplitHiddenView = null;
}
@@ -4660,7 +4688,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
resetTaskVisuals();
mSplitHiddenTaskViewIndex = -1;
if (mSplitHiddenTaskView != null) {
mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE);
mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
mSplitHiddenTaskView = null;
}
if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) {
@@ -5534,7 +5562,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
}
taskView.setShowScreenshot(true);
for (TaskView.TaskIdAttributeContainer container :
for (TaskIdAttributeContainer container :
taskView.getTaskIdAttributeContainers()) {
if (container == null) {
continue;
@@ -81,6 +81,47 @@ public class TaskThumbnailView extends View {
}
};
public static final Property<TaskThumbnailView, Float> SPLASH_ALPHA =
new FloatProperty<TaskThumbnailView>("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<TaskThumbnailView, Float> SPLIT_SELECT_TRANSLATE_X =
new FloatProperty<TaskThumbnailView>("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<TaskThumbnailView, Float> SPLIT_SELECT_TRANSLATE_Y =
new FloatProperty<TaskThumbnailView>("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();
@@ -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;
}
}
@@ -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)
}
}
-1
View File
@@ -19,7 +19,6 @@
android:id="@+id/icon"
android:singleLine="false"
android:lines="2"
android:inputType="textMultiLine"
launcher:iconDisplay="all_apps"
launcher:centerVertically="true" />
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Split display specific version of Launcher3/res/xml/default_workspace_4x4.xml -->
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
<!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
<favorite
launcher:container="-101"
launcher:screen="0"
launcher:x="0"
launcher:y="0"
launcher:className="com.google.android.apps.chrome.Main"
launcher:packageName="com.android.chrome" />
</favorites>
@@ -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();
+119 -4
View File
@@ -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<BubbleTextView, Float> DOT_SCALE_PROPERTY
= new Property<BubbleTextView, Float>(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);
}
}
}
+6 -6
View File
@@ -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;
+34 -29
View File
@@ -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,
@@ -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";
@@ -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<T extends Context & ActivityContext> 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<T> apps, SearchAdapterProvider<?> adapterProvider) {
@@ -152,7 +153,8 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> 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<T extends Context & ActivityContext> 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:
@@ -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.");
@@ -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.");
}
@@ -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
@@ -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()) {
@@ -236,8 +236,8 @@ public abstract class AbstractSlideInView<T extends Context & ActivityContext>
/** 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);
}
/**
@@ -321,13 +321,6 @@ public class LauncherWidgetHolder {
mFlags &= ~FLAG_LISTENING;
}
/**
* Delete the host
*/
public void deleteHost() {
mWidgetHost.deleteHost();
}
/**
* @return The app widget ids
*/
@@ -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";
@@ -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 "" that's why there are no break points
assertEquals(IntArray.wrap(), getListOfBreakpoints("", 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));
}
}
@@ -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);
}
}
@@ -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.
@@ -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);