Merge "Replace DragView bitmap with drawable" into sc-dev
diff --git a/quickstep/res/layout/fallback_recents_activity.xml b/quickstep/res/layout/fallback_recents_activity.xml
index cd64a94..55400a7 100644
--- a/quickstep/res/layout/fallback_recents_activity.xml
+++ b/quickstep/res/layout/fallback_recents_activity.xml
@@ -19,6 +19,14 @@
android:layout_height="match_parent"
android:fitsSystemWindows="true">
+ <com.android.quickstep.views.SplitPlaceholderView
+ android:id="@+id/split_placeholder"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/split_placeholder_size"
+ android:background="@android:color/white"
+ android:alpha=".8"
+ android:visibility="gone" />
+
<com.android.quickstep.fallback.RecentsDragLayer
android:id="@+id/drag_layer"
android:layout_width="match_parent"
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index fe57e9b..394e880 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -15,6 +15,14 @@
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <com.android.quickstep.views.SplitPlaceholderView
+ android:id="@+id/split_placeholder"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/split_placeholder_size"
+ android:background="@android:color/white"
+ android:alpha=".8"
+ android:visibility="gone" />
+
<com.android.quickstep.views.LauncherRecentsView
android:id="@+id/overview_panel"
android:layout_width="match_parent"
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index b124b33..84e2304 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -25,7 +25,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/taskbar_background"
- android:gravity="center"
- android:animateLayoutChanges="true"/>
+ android:gravity="center"/>
</com.android.launcher3.taskbar.TaskbarContainerView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 2a24624..755bce8 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -39,6 +39,7 @@
<dimen name="overview_grid_bottom_margin">90dp</dimen>
<dimen name="overview_grid_side_margin">54dp</dimen>
<dimen name="overview_grid_row_spacing">42dp</dimen>
+ <dimen name="split_placeholder_size">110dp</dimen>
<dimen name="recents_page_spacing">16dp</dimen>
<dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 9a4487c..0764bb3 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -22,8 +22,6 @@
<string name="derived_app_name" translatable="false">Quickstep</string>
<!-- Options for recent tasks -->
- <!-- Title for an option to enter split screen mode for a given app -->
- <string name="recent_task_option_split_screen">Split screen</string>
<!-- Title for an option to keep an app pinned to the screen until it is unpinned -->
<string name="recent_task_option_pin">Pin</string>
<!-- Title for an option to enter freeform mode for a given app -->
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 6eb1498..20a645e 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_SIZE;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
import android.animation.AnimatorSet;
@@ -57,8 +58,10 @@
import com.android.quickstep.TaskUtils;
import com.android.quickstep.util.RemoteAnimationProvider;
import com.android.quickstep.util.RemoteFadeOutAnimationListener;
+import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.SplitPlaceholderView;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -79,7 +82,7 @@
* Reusable command for applying the back button alpha on the background thread.
*/
public static final UiThreadHelper.AsyncCommand SET_BACK_BUTTON_ALPHA =
- (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setBackButtonAlpha(
+ (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setNavBarButtonAlpha(
Float.intBitsToFloat(arg1), arg2 != 0);
private OverviewActionsView mActionsView;
@@ -214,7 +217,12 @@
SysUINavigationMode.INSTANCE.get(this).updateMode();
mActionsView = findViewById(R.id.overview_actions_view);
- ((RecentsView) getOverviewPanel()).init(mActionsView);
+ SplitPlaceholderView splitPlaceholderView = findViewById(R.id.split_placeholder);
+ RecentsView overviewPanel = (RecentsView) getOverviewPanel();
+ splitPlaceholderView.init(
+ new SplitSelectStateController(SystemUiProxy.INSTANCE.get(this))
+ );
+ overviewPanel.init(mActionsView, splitPlaceholderView);
mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
addTaskbarIfNecessary();
@@ -328,6 +336,10 @@
@Override
public void onDragLayerHierarchyChanged() {
onLauncherStateOrFocusChanged();
+
+ if (mTaskbarController != null) {
+ mTaskbarController.onLauncherDragLayerHierarchyChanged();
+ }
}
@Override
@@ -369,8 +381,10 @@
*/
private void onLauncherStateOrFocusChanged() {
boolean shouldBackButtonBeHidden = shouldBackButtonBeHidden(getStateManager().getState());
- UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
- shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+ if (SysUINavigationMode.getMode(this) == TWO_BUTTONS) {
+ UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
+ shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+ }
if (getDragLayer() != null) {
getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
index 13501a4..ce94305 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.quickstep.AnimatedFloat.VALUE;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
@@ -30,7 +31,7 @@
import com.android.quickstep.SystemUiProxy;
/**
- * State handler for animating back button alpha
+ * State handler for animating back button alpha in two-button nav mode.
*/
public class BackButtonAlphaHandler implements StateHandler<LauncherState> {
@@ -51,14 +52,11 @@
return;
}
- if (!SysUINavigationMode.getMode(mLauncher).hasGestures) {
- // If the nav mode is not gestural, then force back button alpha to be 1
- UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
- BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, 1f, true /* animate */);
+ if (SysUINavigationMode.getMode(mLauncher) != TWO_BUTTONS) {
return;
}
- mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastBackButtonAlpha();
+ mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastNavButtonAlpha();
animation.setFloat(mBackAlpha, VALUE,
mLauncher.shouldBackButtonBeHidden(toState) ? 0 : 1, LINEAR);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index 52b3195..3ae8581 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -19,6 +19,8 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_TASKBAR;
+import static com.android.launcher3.AbstractFloatingView.TYPE_REPLACE_TASKBAR_WITH_HOTSEAT;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
@@ -40,6 +42,7 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.Hotseat;
import com.android.launcher3.LauncherState;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
@@ -144,22 +147,13 @@
ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
ActivityOptions.makeBasic());
} else if (tag instanceof FolderInfo) {
- FolderIcon folderIcon = (FolderIcon) view;
- Folder folder = folderIcon.getFolder();
-
- setTaskbarWindowFullscreen(true);
-
- mTaskbarContainerView.post(() -> {
- folder.animateOpen();
-
- folder.iterateOverItems((itemInfo, itemView) -> {
- itemView.setOnClickListener(getItemOnClickListener());
- itemView.setOnLongClickListener(getItemOnLongClickListener());
- // To play haptic when dragging, like other Taskbar items do.
- itemView.setHapticFeedbackEnabled(true);
- return false;
- });
- });
+ if (mLauncher.hasBeenResumed()) {
+ FolderInfo folderInfo = (FolderInfo) tag;
+ onClickedOnFolderFromHome(folderInfo);
+ } else {
+ FolderIcon folderIcon = (FolderIcon) view;
+ onClickedOnFolderInApp(folderIcon);
+ }
} else {
ItemClickHandler.INSTANCE.onClick(view);
}
@@ -169,6 +163,34 @@
};
}
+ // Open the real folder in Launcher.
+ private void onClickedOnFolderFromHome(FolderInfo folderInfo) {
+ alignRealHotseatWithTaskbar();
+
+ FolderIcon folderIcon = (FolderIcon) mLauncher.getHotseat()
+ .getFirstItemMatch((info, v) -> info == folderInfo);
+ folderIcon.post(folderIcon::performClick);
+ }
+
+ // Open the Taskbar folder, and handle clicks on folder items.
+ private void onClickedOnFolderInApp(FolderIcon folderIcon) {
+ Folder folder = folderIcon.getFolder();
+
+ setTaskbarWindowFullscreen(true);
+
+ mTaskbarContainerView.post(() -> {
+ folder.animateOpen();
+
+ folder.iterateOverItems((itemInfo, itemView) -> {
+ itemView.setOnClickListener(getItemOnClickListener());
+ itemView.setOnLongClickListener(getItemOnLongClickListener());
+ // To play haptic when dragging, like other Taskbar items do.
+ itemView.setHapticFeedbackEnabled(true);
+ return false;
+ });
+ });
+ }
+
@Override
public View.OnLongClickListener getItemOnLongClickListener() {
return view -> {
@@ -307,6 +329,7 @@
mAnimator = createAnimToLauncher(null, duration);
} else {
mAnimator = createAnimToApp(duration);
+ replaceTaskbarWithHotseatOrViceVersa();
}
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
@@ -356,6 +379,7 @@
@Override
public void onAnimationStart(Animator animation) {
mTaskbarView.updateHotseatItemsVisibility();
+ setReplaceTaskbarWithHotseat(false);
}
});
return anim.buildAnim();
@@ -452,6 +476,39 @@
mTaskbarView.getHeight() - hotseatBounds.bottom);
}
+ /**
+ * A view was added or removed from DragLayer, check if we need to hide our hotseat copy and
+ * show the real one instead.
+ */
+ public void onLauncherDragLayerHierarchyChanged() {
+ replaceTaskbarWithHotseatOrViceVersa();
+ }
+
+ private void replaceTaskbarWithHotseatOrViceVersa() {
+ boolean replaceTaskbarWithHotseat = AbstractFloatingView.getTopOpenViewWithType(mLauncher,
+ TYPE_REPLACE_TASKBAR_WITH_HOTSEAT) != null;
+ if (!mLauncher.hasBeenResumed()) {
+ replaceTaskbarWithHotseat = false;
+ }
+ setReplaceTaskbarWithHotseat(replaceTaskbarWithHotseat);
+
+ boolean hideTaskbar = AbstractFloatingView.getTopOpenViewWithType(mLauncher,
+ TYPE_HIDE_TASKBAR) != null;
+ mTaskbarVisibilityController.animateToVisibilityForFloatingView(hideTaskbar ? 0f : 1f);
+ }
+
+ private void setReplaceTaskbarWithHotseat(boolean replaceTaskbarWithHotseat) {
+ Hotseat hotseat = mLauncher.getHotseat();
+ if (replaceTaskbarWithHotseat) {
+ alignRealHotseatWithTaskbar();
+ hotseat.getReplaceTaskbarAlpha().setValue(1f);
+ mTaskbarView.setHotseatViewsHidden(true);
+ } else {
+ hotseat.getReplaceTaskbarAlpha().setValue(0f);
+ mTaskbarView.setHotseatViewsHidden(false);
+ }
+ }
+
private float getTaskbarScaleOnHome() {
return 1f / mTaskbarContainerView.getTaskbarActivityContext().getTaskbarIconScale();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index a729e77..1d762e9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar;
+import android.animation.LayoutTransition;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -63,6 +64,7 @@
private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
// Initialized in init().
+ private LayoutTransition mLayoutTransition;
private int mHotseatStartIndex;
private int mHotseatEndIndex;
private View mHotseatRecentsDivider;
@@ -76,6 +78,7 @@
private boolean mIsDraggingItem;
// Only non-null when the corresponding Folder is open.
private @Nullable FolderIcon mLeaveBehindFolderIcon;
+ private boolean mIsHotseatHidden;
public TaskbarView(@NonNull Context context) {
this(context, null);
@@ -107,6 +110,9 @@
}
protected void init(int numHotseatIcons, int numRecentIcons) {
+ mLayoutTransition = new LayoutTransition();
+ setLayoutTransitionsEnabled(true);
+
mHotseatStartIndex = 0;
mHotseatEndIndex = mHotseatStartIndex + numHotseatIcons - 1;
updateHotseatItems(new ItemInfo[numHotseatIcons]);
@@ -119,6 +125,10 @@
updateRecentTasks(new Task[numRecentIcons]);
}
+ private void setLayoutTransitionsEnabled(boolean enabled) {
+ setLayoutTransition(enabled ? mLayoutTransition : null);
+ }
+
protected void cleanup() {
removeAllViews();
}
@@ -206,9 +216,19 @@
}
}
+ /**
+ * Hides or shows the hotseat items immediately (without layout transitions).
+ */
+ protected void setHotseatViewsHidden(boolean hidden) {
+ mIsHotseatHidden = hidden;
+ setLayoutTransitionsEnabled(false);
+ updateHotseatItemsVisibility();
+ setLayoutTransitionsEnabled(true);
+ }
+
private void updateHotseatItemVisibility(View hotseatView) {
if (hotseatView.getTag() != null) {
- hotseatView.setVisibility(VISIBLE);
+ hotseatView.setVisibility(mIsHotseatHidden ? INVISIBLE : VISIBLE);
} else {
int oldVisibility = hotseatView.getVisibility();
int newVisibility = mControllerCallbacks.getEmptyHotseatViewVisibility();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarVisibilityController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarVisibilityController.java
index 6d20d97..2228eba 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarVisibilityController.java
@@ -31,6 +31,7 @@
public class TaskbarVisibilityController {
private static final long IME_VISIBILITY_ALPHA_DURATION = 120;
+ private static final long FLOATING_VIEW_VISIBILITY_ALPHA_DURATION = 120;
private final BaseQuickstepLauncher mLauncher;
private final TaskbarController.TaskbarVisibilityControllerCallbacks mTaskbarCallbacks;
@@ -44,6 +45,8 @@
this::updateVisibilityAlpha);
private AnimatedFloat mTaskbarVisibilityAlphaForIme = new AnimatedFloat(
this::updateVisibilityAlpha);
+ private AnimatedFloat mTaskbarVisibilityAlphaForFloatingView = new AnimatedFloat(
+ this::updateVisibilityAlpha);
public TaskbarVisibilityController(BaseQuickstepLauncher launcher,
TaskbarController.TaskbarVisibilityControllerCallbacks taskbarCallbacks) {
@@ -59,12 +62,14 @@
boolean isImeVisible = (SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags()
& QuickStepContract.SYSUI_STATE_IME_SHOWING) != 0;
mTaskbarVisibilityAlphaForIme.updateValue(isImeVisible ? 0f : 1f);
+ mTaskbarVisibilityAlphaForFloatingView.updateValue(1f);
onTaskbarBackgroundAlphaChanged();
updateVisibilityAlpha();
}
protected void cleanup() {
+ setNavBarButtonAlpha(1f);
}
protected AnimatedFloat getTaskbarVisibilityForLauncherState() {
@@ -81,6 +86,11 @@
.setDuration(IME_VISIBILITY_ALPHA_DURATION).start();
}
+ protected void animateToVisibilityForFloatingView(float toAlpha) {
+ mTaskbarVisibilityAlphaForIme.animateToValue(mTaskbarVisibilityAlphaForFloatingView.value,
+ toAlpha).setDuration(FLOATING_VIEW_VISIBILITY_ALPHA_DURATION).start();
+ }
+
private void onTaskbarBackgroundAlphaChanged() {
mTaskbarCallbacks.updateTaskbarBackgroundAlpha(mTaskbarBackgroundAlpha.value);
updateVisibilityAlpha();
@@ -92,7 +102,16 @@
// LauncherState if Launcher is paused.
float alphaDueToLauncher = Math.max(mTaskbarBackgroundAlpha.value,
mTaskbarVisibilityAlphaForLauncherState.value);
- float alphaDueToOther = mTaskbarVisibilityAlphaForIme.value;
- mTaskbarCallbacks.updateTaskbarVisibilityAlpha(alphaDueToLauncher * alphaDueToOther);
+ float alphaDueToOther = mTaskbarVisibilityAlphaForIme.value
+ * mTaskbarVisibilityAlphaForFloatingView.value;
+ float taskbarAlpha = alphaDueToLauncher * alphaDueToOther;
+ mTaskbarCallbacks.updateTaskbarVisibilityAlpha(taskbarAlpha);
+
+ // Make the nav bar invisible if taskbar is visible.
+ setNavBarButtonAlpha(1f - taskbarAlpha);
+ }
+
+ private void setNavBarButtonAlpha(float navBarAlpha) {
+ SystemUiProxy.INSTANCE.get(mLauncher).setNavBarButtonAlpha(navBarAlpha, false);
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 0f13ef9..bedaefa 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -32,6 +32,7 @@
import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_TRANSLATION;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import android.util.FloatProperty;
@@ -44,6 +45,7 @@
import com.android.launcher3.graphics.OverviewScrim;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.quickstep.views.RecentsView;
/**
@@ -105,7 +107,12 @@
config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
- setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
+ PagedOrientationHandler orientationHandler =
+ ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
+ FloatProperty taskViewsFloat = orientationHandler.getSplitSelectTaskOffset(
+ TASK_PRIMARY_TRANSLATION, TASK_SECONDARY_TRANSLATION, mLauncher.getDeviceProfile());
+ setter.setFloat(mRecentsView, taskViewsFloat,
+ toState.getOverviewSecondaryTranslation(mLauncher),
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index c9de662..750f673 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -17,11 +17,14 @@
import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
+import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
+import static com.android.launcher3.LauncherState.SPLIT_PLACHOLDER_VIEW;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
+import static com.android.quickstep.views.SplitPlaceholderView.ALPHA_FLOAT;
import android.annotation.TargetApi;
import android.os.Build;
@@ -77,11 +80,26 @@
AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
}
+ // Create or dismiss split screen select animations
+ LauncherState currentState = mLauncher.getStateManager().getState();
+ if (isSplitSelectionState(toState) && !isSplitSelectionState(currentState)) {
+ builder.add(mRecentsView.createSplitSelectInitAnimation().buildAnim());
+ } else if (!isSplitSelectionState(toState) && isSplitSelectionState(currentState)) {
+ builder.add(mRecentsView.cancelSplitSelect(true).buildAnim());
+ }
+
setAlphas(builder, config, toState);
builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
toState.getOverviewFullscreenProgress(), LINEAR);
}
+ /**
+ * @return true if {@param toState} is {@link LauncherState#OVERVIEW_SPLIT_SELECT}
+ */
+ private boolean isSplitSelectionState(@NonNull LauncherState toState) {
+ return toState == OVERVIEW_SPLIT_SELECT;
+ }
+
private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
LauncherState state) {
float clearAllButtonAlpha = (state.getVisibleElements(mLauncher) & CLEAR_ALL_BUTTON) != 0
@@ -93,6 +111,11 @@
propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
MultiValueAlpha.VALUE, overviewButtonAlpha, config.getInterpolator(
ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
+
+ float splitPlaceholderAlpha = state.areElementsVisible(mLauncher, SPLIT_PLACHOLDER_VIEW) ?
+ 1 : 0;
+ propertySetter.setFloat(mRecentsView.getSplitPlaceholder(), ALPHA_FLOAT,
+ splitPlaceholderAlpha, LINEAR);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 1f68a04..372784a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -175,4 +175,12 @@
public static OverviewState newModalTaskState(int id) {
return new OverviewModalTaskState(id);
}
+
+ /**
+ * New Overview substate representing state where 1 app for split screen has been selected and
+ * pinned and user is selecting the second one
+ */
+ public static OverviewState newSplitSelectState(int id) {
+ return new SplitScreenSelectState(id);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
new file mode 100644
index 0000000..722d74a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides.states;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * New Overview substate representing state where 1 app for split screen has been selected and
+ * pinned and user is selecting the second one
+ */
+public class SplitScreenSelectState extends OverviewState {
+ public SplitScreenSelectState(int id) {
+ super(id);
+ }
+
+ @Override
+ public void onBackPressed(Launcher launcher) {
+ launcher.getStateManager().goToState(OVERVIEW);
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return SPLIT_PLACHOLDER_VIEW;
+ }
+
+ @Override
+ public float getOverviewSecondaryTranslation(Launcher launcher) {
+ RecentsView recentsView = launcher.getOverviewPanel();
+ PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+ int splitPosition = recentsView.getSplitPlaceholder().getSplitController()
+ .getActiveSplitPositionOption().mStagePosition;
+ int direction = orientationHandler.getSplitTranslationDirectionFactor(splitPosition);
+ return launcher.getResources().getDimension(R.dimen.split_placeholder_size) * direction;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index cf345e6..7df86b9 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1469,7 +1469,9 @@
mSwipePipToHomeAnimator.getDestinationBounds());
mRecentsAnimationController.setFinishTaskBounds(
mSwipePipToHomeAnimator.getTaskId(),
- mSwipePipToHomeAnimator.getDestinationBounds());
+ mSwipePipToHomeAnimator.getDestinationBounds(),
+ mSwipePipToHomeAnimator.getFinishWindowCrop(),
+ mSwipePipToHomeAnimator.getFinishTransform());
mIsSwipingPipToHome = false;
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 3d68d64..d3ed791 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -63,7 +63,9 @@
import com.android.quickstep.fallback.RecentsDragLayer;
import com.android.quickstep.fallback.RecentsState;
import com.android.quickstep.util.RecentsAtomicAnimationFactory;
+import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.views.OverviewActionsView;
+import com.android.quickstep.views.SplitPlaceholderView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -105,8 +107,14 @@
mFallbackRecentsView = findViewById(R.id.overview_panel);
mActionsView = findViewById(R.id.overview_actions_view);
+ SplitPlaceholderView splitPlaceholderView = findViewById(R.id.split_placeholder);
+ splitPlaceholderView.init(
+ new SplitSelectStateController(
+ SystemUiProxy.INSTANCE.get(this))
+ );
+
mDragLayer.recreateControllers();
- mFallbackRecentsView.init(mActionsView);
+ mFallbackRecentsView.init(mActionsView, splitPlaceholderView);
}
@Override
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 646c5a0..ec585cc 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -149,10 +149,14 @@
* accordingly. This should be called before `finish`
* @param taskId for which the leash should be updated
* @param destinationBounds bounds of the final PiP window
+ * @param windowCrop bounds to crop as part of final transform.
+ * @param float9 An array of 9 floats to be used as matrix transform.
*/
- public void setFinishTaskBounds(int taskId, Rect destinationBounds) {
+ public void setFinishTaskBounds(int taskId, Rect destinationBounds, Rect windowCrop,
+ float[] float9) {
UI_HELPER_EXECUTOR.execute(
- () -> mController.setFinishTaskBounds(taskId, destinationBounds));
+ () -> mController.setFinishTaskBounds(taskId, destinationBounds, windowCrop,
+ float9));
}
/**
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 5668817..a70cc4c 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -45,7 +45,8 @@
/**
* Holds the reference to SystemUI.
*/
-public class SystemUiProxy implements ISystemUiProxy {
+public class SystemUiProxy implements ISystemUiProxy,
+ SysUINavigationMode.NavigationModeChangeListener {
private static final String TAG = SystemUiProxy.class.getSimpleName();
public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
@@ -59,14 +60,21 @@
// Used to dedupe calls to SystemUI
private int mLastShelfHeight;
private boolean mLastShelfVisible;
- private float mLastBackButtonAlpha;
- private boolean mLastBackButtonAnimate;
+ private float mLastNavButtonAlpha;
+ private boolean mLastNavButtonAnimate;
+ private boolean mHasNavButtonAlphaBeenSet = false;
// TODO(141886704): Find a way to remove this
private int mLastSystemUiStateFlags;
public SystemUiProxy(Context context) {
- // Do nothing
+ SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this);
+ }
+
+ @Override
+ public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+ // Whenever the nav mode changes, force reset the nav button alpha
+ setNavBarButtonAlpha(1f, false);
}
@Override
@@ -149,28 +157,19 @@
return null;
}
- @Override
- public void setBackButtonAlpha(float alpha, boolean animate) {
- boolean changed = Float.compare(alpha, mLastBackButtonAlpha) != 0
- || animate != mLastBackButtonAnimate;
- if (mSystemUiProxy != null && changed) {
- mLastBackButtonAlpha = alpha;
- mLastBackButtonAnimate = animate;
- try {
- mSystemUiProxy.setBackButtonAlpha(alpha, animate);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call setBackButtonAlpha", e);
- }
- }
- }
-
- public float getLastBackButtonAlpha() {
- return mLastBackButtonAlpha;
+ public float getLastNavButtonAlpha() {
+ return mLastNavButtonAlpha;
}
@Override
public void setNavBarButtonAlpha(float alpha, boolean animate) {
- if (mSystemUiProxy != null) {
+ boolean changed = Float.compare(alpha, mLastNavButtonAlpha) != 0
+ || animate != mLastNavButtonAnimate
+ || !mHasNavButtonAlphaBeenSet;
+ if (mSystemUiProxy != null && changed) {
+ mLastNavButtonAlpha = alpha;
+ mLastNavButtonAnimate = animate;
+ mHasNavButtonAlphaBeenSet = true;
try {
mSystemUiProxy.setNavBarButtonAlpha(alpha, animate);
} catch (RemoteException e) {
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 8636130..cd13200 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -36,11 +36,16 @@
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.quickstep.TaskShortcutFactory.SplitSelectSystemShortcut;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
@@ -57,11 +62,18 @@
*/
public class TaskOverlayFactory implements ResourceBasedOverride {
- public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView) {
+ public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView,
+ DeviceProfile deviceProfile) {
final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
SystemShortcut shortcut = menuOption.getShortcut(activity, taskView);
+ if (menuOption == TaskShortcutFactory.SPLIT_SCREEN &&
+ FeatureFlags.ENABLE_SPLIT_SELECT.get()) {
+ addSplitOptions(shortcuts, activity, taskView, deviceProfile);
+ continue;
+ }
+
if (shortcut != null) {
shortcuts.add(shortcut);
}
@@ -91,6 +103,18 @@
return shortcuts;
}
+
+ public static void addSplitOptions(List<SystemShortcut> outShortcuts,
+ BaseDraggingActivity activity, TaskView taskView, DeviceProfile deviceProfile) {
+ PagedOrientationHandler orientationHandler =
+ taskView.getRecentsView().getPagedOrientationHandler();
+ List<SplitPositionOption> positions =
+ orientationHandler.getSplitPositionOptions(deviceProfile);
+ for (SplitPositionOption option : positions) {
+ outShortcuts.add(new SplitSelectSystemShortcut(activity, taskView, option));
+ }
+ }
+
public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
return new TaskOverlay(thumbnailView);
}
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index d81f07f..c06e9a9 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -34,11 +34,13 @@
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
import com.android.launcher3.model.WellbeingModel;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.popup.SystemShortcut.AppInfo;
import com.android.launcher3.util.InstantAppResolver;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskThumbnailView;
import com.android.quickstep.views.TaskView;
@@ -58,7 +60,6 @@
* Represents a system shortcut that can be shown for a recent task.
*/
public interface TaskShortcutFactory {
-
SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view);
TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, view.getItemInfo());
@@ -93,6 +94,23 @@
}
}
+ class SplitSelectSystemShortcut extends SystemShortcut {
+ private final TaskView mTaskView;
+ private SplitPositionOption mSplitPositionOption;
+ public SplitSelectSystemShortcut(BaseDraggingActivity target, TaskView taskView,
+ SplitPositionOption option) {
+ super(option.mIconResId, option.mTextResId, target, taskView.getItemInfo());
+ mTaskView = taskView;
+ mSplitPositionOption = option;
+ setEnabled(taskView.getRecentsView().getTaskViewCount() > 1);
+ }
+
+ @Override
+ public void onClick(View view) {
+ mTaskView.initiateSplitSelect(mSplitPositionOption);
+ }
+ }
+
class MultiWindowSystemShortcut extends SystemShortcut {
private Handler mHandler;
@@ -211,6 +229,16 @@
}
@Override
+ public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
+ SystemShortcut shortcut = super.getShortcut(activity, taskView);
+ if (FeatureFlags.ENABLE_SPLIT_SELECT.get()) {
+ // Disable if there's only one recent app for split screen
+ shortcut.setEnabled(taskView.getRecentsView().getTaskViewCount() > 1);
+ }
+ return shortcut;
+ }
+
+ @Override
protected ActivityOptions makeLaunchOptions(Activity activity) {
final ActivityCompat act = new ActivityCompat(activity);
final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 2feeffa..7a428ce 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -23,6 +23,7 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.statehandlers.DepthController.DEPTH;
import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
@@ -37,6 +38,7 @@
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
+import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.view.View;
@@ -295,6 +297,93 @@
}
}
+ /**
+ * TODO: This doesn't animate at present. Feel free to blow out everyhing in this method
+ * if needed
+ *
+ * We could manually try to animate the just the bounds for the leashes we get back, but we try
+ * to do it through TaskViewSimulator(TVS) since that handles a lot of the recents UI stuff for
+ * us.
+ *
+ * First you have to call TVS#setPreview() to indicate which leash it will operate one
+ * Then operations happen in TVS#apply() on each frame callback.
+ *
+ * TVS uses DeviceProfile to try to figure out things like task height and such based on if the
+ * device is in multiWindowMode or not. It's unclear given the two calls to startTask() when the
+ * device is considered in multiWindowMode and things like insets and stuff change
+ * and calculations have to be adjusted in the animations for that
+ */
+ public static void composeRecentsSplitLaunchAnimator(@NonNull AnimatorSet anim,
+ @NonNull TaskView v, @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing,
+ @NonNull StateManager stateManager, @NonNull DepthController depthController,
+ int targetStage) {
+ PendingAnimation out = new PendingAnimation(RECENTS_LAUNCH_DURATION);
+ boolean isRunningTask = v.isRunningTask();
+ TransformParams params = null;
+ TaskViewSimulator tvs = null;
+ RecentsView recentsView = v.getRecentsView();
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask) {
+ params = recentsView.getLiveTileParams();
+ tvs = recentsView.getLiveTileTaskViewSimulator();
+ }
+
+ boolean inLiveTileMode =
+ ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1;
+ final RemoteAnimationTargets targets =
+ new RemoteAnimationTargets(appTargets, wallpaperTargets,
+ inLiveTileMode ? MODE_CLOSING : MODE_OPENING);
+
+ if (params == null) {
+ SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
+ targets.addReleaseCheck(applier);
+
+ params = new TransformParams()
+ .setSyncTransactionApplier(applier)
+ .setTargetSet(targets);
+ }
+
+ Rect crop = new Rect();
+ Context context = v.getContext();
+ DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+ if (tvs == null && targets.apps.length > 0) {
+ tvs = new TaskViewSimulator(recentsView.getContext(), recentsView.getSizeStrategy());
+ tvs.setDp(dp);
+
+ // RecentsView never updates the display rotation until swipe-up so the value may
+ // be stale. Use the display value instead.
+ int displayRotation = DisplayController.getDefaultDisplay(recentsView.getContext())
+ .getInfo().rotation;
+ tvs.getOrientationState().update(displayRotation, displayRotation);
+
+ tvs.setPreview(targets.apps[targets.apps.length - 1]);
+ tvs.fullScreenProgress.value = 0;
+ tvs.recentsViewScale.value = 1;
+// tvs.setScroll(startScroll);
+
+ // Fade in the task during the initial 20% of the animation
+ out.addFloat(params, TransformParams.TARGET_ALPHA, 0, 1,
+ clampToProgress(LINEAR, 0, 0.2f));
+ }
+
+ TaskViewSimulator topMostSimulator = null;
+
+ if (tvs != null) {
+ out.setFloat(tvs.fullScreenProgress,
+ AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
+ out.setFloat(tvs.recentsViewScale,
+ AnimatedFloat.VALUE, tvs.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
+ out.setInt(tvs, TaskViewSimulator.SCROLL, 0, TOUCH_RESPONSE_INTERPOLATOR);
+
+ TaskViewSimulator finalTsv = tvs;
+ TransformParams finalParams = params;
+ out.addOnFrameCallback(() -> finalTsv.apply(finalParams));
+ topMostSimulator = tvs;
+ }
+
+ anim.play(out.buildAnim());
+ }
+
public static void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
@NonNull RemoteAnimationTargetCompat[] appTargets,
@NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing,
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 13f6137..02fd5bb 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -34,6 +34,7 @@
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.SplitPlaceholderView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
@@ -56,8 +57,8 @@
}
@Override
- public void init(OverviewActionsView actionsView) {
- super.init(actionsView);
+ public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
+ super.init(actionsView, splitPlaceholderView);
setOverviewStateEnabled(true);
setOverlayEnabled(true);
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
new file mode 100644
index 0000000..d9154ed
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import android.animation.AnimatorSet;
+import android.app.ActivityOptions;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherAnimationRunner;
+import com.android.launcher3.WrappedAnimationRunnerImpl;
+import com.android.launcher3.WrappedLauncherAnimationRunner;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
+import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+/**
+ * Represent data needed for the transient state when user has selected one app for split screen
+ * and is in the process of either a) selecting a second app or b) exiting intention to invoke split
+ */
+public class SplitSelectStateController {
+
+ private final SystemUiProxy mSystemUiProxy;
+ private TaskView mInitialTaskView;
+ private SplitPositionOption mInitialPosition;
+
+ public SplitSelectStateController(SystemUiProxy systemUiProxy) {
+ mSystemUiProxy = systemUiProxy;
+ }
+
+ /**
+ * To be called after first task selected
+ */
+ public void setInitialTaskSelect(TaskView taskView, SplitPositionOption positionOption) {
+ mInitialTaskView = taskView;
+ mInitialPosition = positionOption;
+ }
+
+ /**
+ * To be called after second task selected
+ */
+ public void setSecondTaskId(TaskView taskView) {
+ // Assume initial mInitialTaskId is for top/left part of screen
+ WrappedAnimationRunnerImpl initialSplitRunnerWrapped = new SplitLaunchAnimationRunner(
+ mInitialTaskView, 0);
+ WrappedAnimationRunnerImpl secondarySplitRunnerWrapped = new SplitLaunchAnimationRunner(
+ taskView, 1);
+ RemoteAnimationRunnerCompat initialSplitRunner = new WrappedLauncherAnimationRunner(
+ new Handler(Looper.getMainLooper()), initialSplitRunnerWrapped,
+ true /* startAtFrontOfQueue */);
+ RemoteAnimationRunnerCompat secondarySplitRunner = new WrappedLauncherAnimationRunner(
+ new Handler(Looper.getMainLooper()), secondarySplitRunnerWrapped,
+ true /* startAtFrontOfQueue */);
+ ActivityOptions initialOptions = ActivityOptionsCompat.makeRemoteAnimation(
+ new RemoteAnimationAdapterCompat(initialSplitRunner, 300, 150));
+ ActivityOptions secondaryOptions = ActivityOptionsCompat.makeRemoteAnimation(
+ new RemoteAnimationAdapterCompat(secondarySplitRunner, 300, 150));
+ mSystemUiProxy.startTask(mInitialTaskView.getTask().key.id, mInitialPosition.mStageType,
+ mInitialPosition.mStagePosition,
+ /*null*/ initialOptions.toBundle());
+ Pair<Integer, Integer> compliment = getComplimentaryStageAndPosition(mInitialPosition);
+ mSystemUiProxy.startTask(taskView.getTask().key.id, compliment.first,
+ compliment.second,
+ /*null*/ secondaryOptions.toBundle());
+ // After successful launch, call resetState
+ resetState();
+ }
+
+ @Nullable
+ public SplitPositionOption getActiveSplitPositionOption() {
+ return mInitialPosition;
+ }
+
+ /**
+ * @return the opposite stage and position from the {@param position} provided as first and
+ * second object, respectively
+ * Ex. If position is has stage = Main and position = Top/Left, this will return
+ * Pair(stage=Side, position=Bottom/Left)
+ */
+ private Pair<Integer, Integer> getComplimentaryStageAndPosition(SplitPositionOption position) {
+ // Right now this is as simple as flipping between 0 and 1
+ int complimentStageType = position.mStageType ^ 1;
+ int complimentStagePosition = position.mStagePosition ^ 1;
+ return new Pair<>(complimentStageType, complimentStagePosition);
+ }
+
+ /**
+ * Remote animation runner for animation to launch an app.
+ */
+ private class SplitLaunchAnimationRunner implements WrappedAnimationRunnerImpl {
+
+ private final TaskView mV;
+ private final int mTargetState;
+
+ SplitLaunchAnimationRunner(TaskView v, int targetState) {
+ mV = v;
+ mTargetState = targetState;
+ }
+
+ @Override
+ public void onCreateAnimation(int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ LauncherAnimationRunner.AnimationResult result) {
+ AnimatorSet anim = new AnimatorSet();
+ BaseQuickstepLauncher activity = BaseActivity.fromContext(mV.getContext());
+ TaskViewUtils.composeRecentsSplitLaunchAnimator(anim, mV,
+ appTargets, wallpaperTargets, true, activity.getStateManager(),
+ activity.getDepthController(), mTargetState);
+ result.setAnimation(anim, activity);
+ }
+ }
+
+
+ /**
+ * To be called if split select was cancelled
+ */
+ public void resetState() {
+ mInitialTaskView = null;
+ mInitialPosition = null;
+ }
+
+ public boolean isSplitSelectActive() {
+ return mInitialTaskView != null;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 0ce5072..0a1a6e8 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -134,8 +134,9 @@
@Override
public void onAnimationEnd(Animator animation) {
- if (!mHasAnimationEnded) super.onAnimationEnd(animation);
- SwipePipToHomeAnimator.this.onAnimationEnd();
+ if (mHasAnimationEnded) return;
+ super.onAnimationEnd(animation);
+ mHasAnimationEnded = true;
}
});
addUpdateListener(this);
@@ -223,14 +224,34 @@
return mDestinationBounds;
}
- private void onAnimationEnd() {
- if (mHasAnimationEnded) return;
+ /**
+ * @return {@link Rect} of the final window crop in destination orientation.
+ */
+ public Rect getFinishWindowCrop() {
+ final Rect windowCrop = new Rect(mAppBounds);
+ if (mSourceHintRectInsets != null) {
+ windowCrop.inset(mSourceHintRectInsets);
+ }
+ return windowCrop;
+ }
- final SurfaceControl.Transaction tx =
- PipSurfaceTransactionHelper.newSurfaceControlTransaction();
- mSurfaceTransactionHelper.reset(tx, mLeash, mDestinationBoundsTransformed, mFromRotation);
- tx.apply();
- mHasAnimationEnded = true;
+ /**
+ * @return Array of 9 floats represents the final transform in destination orientation.
+ */
+ public float[] getFinishTransform() {
+ final Matrix transform = new Matrix();
+ final float[] float9 = new float[9];
+ if (mSourceHintRectInsets == null) {
+ transform.setRectToRect(new RectF(mAppBounds), new RectF(mDestinationBounds),
+ Matrix.ScaleToFit.FILL);
+ } else {
+ final float scale = mAppBounds.width() <= mAppBounds.height()
+ ? (float) mDestinationBounds.width() / mAppBounds.width()
+ : (float) mDestinationBounds.height() / mAppBounds.height();
+ transform.setScale(scale, scale);
+ }
+ transform.getValues(float9);
+ return float9;
}
private RotatedPosition getRotatedPosition(float fraction) {
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index df1229b..6e8a5f1 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -104,6 +104,7 @@
public final AnimatedFloat recentsViewScale = new AnimatedFloat();
public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
public final AnimatedFloat recentsViewSecondaryTranslation = new AnimatedFloat();
+ public final AnimatedFloat recentsViewPrimaryTranslation = new AnimatedFloat();
public final AnimatedFloat gridProgress = new AnimatedFloat();
private final ScrollState mScrollState = new ScrollState();
@@ -347,6 +348,8 @@
mMatrix.postScale(recentsViewScale.value, recentsViewScale.value, mPivot.x, mPivot.y);
mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
recentsViewSecondaryTranslation.value);
+ mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
+ recentsViewPrimaryTranslation.value);
applyWindowToHomeRotation(mMatrix);
// Crop rect is the inverse of thumbnail matrix
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index ceb343d..9d31190 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -86,8 +86,8 @@
}
@Override
- public void init(OverviewActionsView actionsView) {
- super.init(actionsView);
+ public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
+ super.init(actionsView, splitPlaceholderView);
setContentAlpha(0);
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9a903dc..f5b62d5 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -26,6 +26,7 @@
import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
+import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.Utilities.squaredHypot;
@@ -47,6 +48,7 @@
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
+import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.LayoutTransition;
import android.animation.LayoutTransition.TransitionListener;
@@ -113,6 +115,7 @@
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.OverScroller;
import com.android.launcher3.util.ResourceBasedOverride.Overrides;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.ViewPool;
import com.android.quickstep.AnimatedFloat;
@@ -210,11 +213,17 @@
}
};
+ /**
+ * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
+ * are currently both used to apply secondary translation. Should their use cases change to be
+ * more specific, we'd want to create a similar FloatProperty just for a TaskView's
+ * offsetX/Y property
+ */
public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION =
new FloatProperty<RecentsView>("taskSecondaryTranslation") {
@Override
public void setValue(RecentsView recentsView, float v) {
- recentsView.setTaskViewsSecondaryTranslation(v);
+ recentsView.setTaskViewsResistanceTranslation(v);
}
@Override
@@ -223,6 +232,25 @@
}
};
+ /**
+ * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
+ * are currently both used to apply secondary translation. Should their use cases change to be
+ * more specific, we'd want to create a similar FloatProperty just for a TaskView's
+ * offsetX/Y property
+ */
+ public static final FloatProperty<RecentsView> TASK_PRIMARY_TRANSLATION =
+ new FloatProperty<RecentsView>("taskPrimaryTranslation") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setTaskViewsPrimaryTranslation(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mTaskViewsPrimaryTranslation;
+ }
+ };
+
/** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
new FloatProperty<RecentsView>("recentsScale") {
@@ -233,7 +261,8 @@
view.mLastComputedTaskPushOutDistance = null;
view.mLiveTileTaskViewSimulator.recentsViewScale.value = scale;
view.updatePageOffsets();
- view.setTaskViewsSecondaryTranslation(view.mTaskViewsSecondaryTranslation);
+ view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation);
+ view.setTaskViewsPrimaryTranslation(view.mTaskViewsPrimaryTranslation);
}
@Override
@@ -305,6 +334,7 @@
private float mAdjacentPageOffset = 0;
private float mTaskViewsSecondaryTranslation = 0;
+ private float mTaskViewsPrimaryTranslation = 0;
// Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
private float mGridProgress = 0;
private boolean mShowAsGrid;
@@ -426,6 +456,28 @@
private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
private Layout mEmptyTextLayout;
+ /**
+ * Placeholder view indicating where the first split screen selected app will be placed
+ */
+ private SplitPlaceholderView mSplitPlaceholderView;
+ /**
+ * The first task that split screen selection was initiated with. When split select state is
+ * initialized, we create a
+ * {@link #createTaskDismissAnimation(TaskView, boolean, boolean, long)} for this TaskView but
+ * don't actually remove the task since the user might back out. As such, we also ensure this
+ * View doesn't go back into the {@link #mTaskViewPool}, see {@link #onViewRemoved(View)}
+ */
+ private TaskView mSplitHiddenTaskView;
+ /**
+ * Keeps track of the index of the TaskView that split screen was initialized with so we know
+ * where to insert it back into list of taskViews in case user backs out of entering split
+ * screen.
+ * NOTE: This index is the index while {@link #mSplitHiddenTaskView} was a child of recentsView,
+ * this doesn't get adjusted to reflect the new child count after the taskView is dismissed/
+ * removed from recentsView
+ */
+ private int mSplitHiddenTaskViewIndex;
+
// Keeps track of the index where the first TaskView should be
private int mTaskViewStartIndex = 0;
private OverviewActionsView mActionsView;
@@ -581,9 +633,19 @@
loadVisibleTaskData();
}
- public void init(OverviewActionsView actionsView) {
+ public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
mActionsView = actionsView;
mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+ mSplitPlaceholderView = splitPlaceholderView;
+
+ }
+
+ public SplitPlaceholderView getSplitPlaceholder() {
+ return mSplitPlaceholderView;
+ }
+
+ public boolean isSplitSelectionActive() {
+ return mSplitPlaceholderView.getSplitController().isSplitSelectActive();
}
@Override
@@ -627,8 +689,9 @@
public void onViewRemoved(View child) {
super.onViewRemoved(child);
- // Clear the task data for the removed child if it was visible
- if (child instanceof TaskView) {
+ // Clear the task data for the removed child if it was visible unless it's the initial
+ // taskview for entering split screen, we only pretend to dismiss the task
+ if (child instanceof TaskView && child != mSplitHiddenTaskView) {
TaskView taskView = (TaskView) child;
mHasVisibleTaskData.delete(taskView.getTask().key.id);
mTaskViewPool.recycle(taskView);
@@ -706,6 +769,9 @@
// Reset the running task when leaving overview since it can still have a reference to
// its thumbnail
mTmpRunningTask = null;
+ if (mSplitPlaceholderView.getSplitController().isSplitSelectActive()) {
+ cancelSplitSelect(false);
+ }
}
}
@@ -1624,12 +1690,12 @@
float clearAllShorterRowCompensation =
mIsRtl ? -shorterRowCompensation : shorterRowCompensation;
- // If the total width is shorter than one task's width, move ClearAllButton further away
+ // If the total width is shorter than one grid's width, move ClearAllButton further away
// accordingly.
float clearAllShortTotalCompensation = 0;
float longRowWidth = Math.max(topRowWidth, bottomRowWidth);
- if (longRowWidth < mTaskWidth) {
- float shortTotalCompensation = mTaskWidth - longRowWidth;
+ if (longRowWidth < mLastComputedGridSize.width()) {
+ float shortTotalCompensation = mLastComputedGridSize.width() - longRowWidth;
clearAllShortTotalCompensation =
mIsRtl ? -shortTotalCompensation : shortTotalCompensation;
}
@@ -1763,7 +1829,7 @@
// alpha is set to 0 so that it can be recycled in the view pool properly
anim.setFloat(taskView, VIEW_ALPHA, 0, ACCEL_2);
FloatProperty<TaskView> secondaryViewTranslate =
- taskView.getDismissTaskTranslationProperty();
+ taskView.getSecondaryDissmissTranslationProperty();
int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
@@ -1826,10 +1892,23 @@
offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
}
}
+
+ // Additional offset for fake landscape, if the pinning happens to the right or
+ // left, we need to scroll all the tasks away from the direction of the splaceholder
+ // view
+ if (isSplitSelectionActive()) {
+ int splitPosition = getSplitPlaceholder().getSplitController()
+ .getActiveSplitPositionOption().mStagePosition;
+ int direction = mOrientationHandler
+ .getSplitTranslationDirectionFactor(splitPosition);
+ int splitOffset = mOrientationHandler.getSplitAnimationTranslation(
+ mSplitPlaceholderView.getHeight(), mActivity.getDeviceProfile());
+ offset += direction * splitOffset;
+ }
int scrollDiff = newScroll[i] - oldScroll[i] + offset;
if (scrollDiff != 0) {
FloatProperty translationProperty = child instanceof TaskView
- ? ((TaskView) child).getFillDismissGapTranslationProperty()
+ ? ((TaskView) child).getPrimaryDismissTranslationProperty()
: mOrientationHandler.getPrimaryViewTranslate();
ResourceProvider rp = DynamicResource.provider(mActivity);
@@ -1907,6 +1986,12 @@
onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
}
resetTaskVisuals();
+ if (mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
+ // We want to keep the tasks translations in this temporary state
+ // after resetting the rest above
+ setTaskViewsResistanceTranslation(mTaskViewsSecondaryTranslation);
+ setTaskViewsPrimaryTranslation(mTaskViewsPrimaryTranslation);
+ }
mPendingAnimation = null;
}
});
@@ -2298,7 +2383,7 @@
return distanceToOffscreen * offsetProgress;
}
- private void setTaskViewsSecondaryTranslation(float translation) {
+ private void setTaskViewsResistanceTranslation(float translation) {
mTaskViewsSecondaryTranslation = translation;
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView task = getTaskViewAt(i);
@@ -2307,6 +2392,15 @@
mLiveTileTaskViewSimulator.recentsViewSecondaryTranslation.value = translation;
}
+ private void setTaskViewsPrimaryTranslation(float translation) {
+ mTaskViewsPrimaryTranslation = translation;
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView task = getTaskViewAt(i);
+ task.getPrimaryDismissTranslationProperty().set(task, translation / getScaleY());
+ }
+ mLiveTileTaskViewSimulator.recentsViewPrimaryTranslation.value = translation;
+ }
+
/**
* TODO: Do not assume motion across X axis for adjacent page
*/
@@ -2324,6 +2418,111 @@
}
}
+ public void initiateSplitSelect(TaskView taskView, SplitPositionOption splitPositionOption) {
+ mSplitHiddenTaskView = taskView;
+ mSplitPlaceholderView.getSplitController().setInitialTaskSelect(taskView,
+ splitPositionOption);
+ mSplitHiddenTaskViewIndex = indexOfChild(taskView);
+ mActivity.getStateManager().goToState(LauncherState.OVERVIEW_SPLIT_SELECT);
+ }
+
+ public PendingAnimation createSplitSelectInitAnimation() {
+ int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
+ return createTaskDismissAnimation(mSplitHiddenTaskView, true, false, duration);
+ }
+
+ public void confirmSplitSelect(TaskView taskView) {
+ mSplitPlaceholderView.getSplitController().setSecondTaskId(taskView);
+ resetTaskVisuals();
+ setTranslationY(0);
+ }
+
+ public PendingAnimation cancelSplitSelect(boolean animate) {
+ mSplitPlaceholderView.getSplitController().resetState();
+ int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
+ PendingAnimation pendingAnim = new PendingAnimation(duration);
+ if (!animate) {
+ resetFromSplitSelectionState();
+ return pendingAnim;
+ }
+
+ addViewInLayout(mSplitHiddenTaskView, mSplitHiddenTaskViewIndex,
+ mSplitHiddenTaskView.getLayoutParams());
+ mSplitHiddenTaskView.setAlpha(0);
+ int[] oldScroll = new int[getChildCount()];
+ getPageScrolls(oldScroll, false,
+ view -> view.getVisibility() != GONE && view != mSplitHiddenTaskView);
+
+ // x is correct, y is before tasks move up
+ int[] locationOnScreen = mSplitHiddenTaskView.getLocationOnScreen();
+ int[] newScroll = new int[getChildCount()];
+ getPageScrolls(newScroll, false, SIMPLE_SCROLL_LOGIC);
+
+ boolean needsCurveUpdates = false;
+ for (int i = mSplitHiddenTaskViewIndex; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (child == mSplitHiddenTaskView) {
+
+ int left = newScroll[i] + getPaddingStart();
+ int topMargin = mSplitHiddenTaskView.getThumbnailTopMargin();
+ int top = -mSplitHiddenTaskView.getHeight() - locationOnScreen[1];
+ mSplitHiddenTaskView.layout(left, top,
+ left + mSplitHiddenTaskView.getWidth(),
+ top + mSplitHiddenTaskView.getHeight());
+ pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, TRANSLATION_Y,
+ -top + mSplitPlaceholderView.getHeight() - topMargin));
+ pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, ALPHA, 1));
+ } else {
+ // If insertion is on last index (furthest from clear all), we directly add the view
+ // else we translate all views to the right of insertion index further right,
+ // ignore views to left
+ int scrollDiff = newScroll[i] - oldScroll[i];
+ if (scrollDiff != 0) {
+ FloatProperty translationProperty = child instanceof TaskView
+ ? ((TaskView) child).getPrimaryDismissTranslationProperty()
+ : mOrientationHandler.getPrimaryViewTranslate();
+
+ ResourceProvider rp = DynamicResource.provider(mActivity);
+ SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
+ .setDampingRatio(
+ rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
+ .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
+ pendingAnim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
+ .setDuration(duration), ACCEL, sp);
+ needsCurveUpdates = true;
+ }
+ }
+ }
+
+ if (needsCurveUpdates) {
+ pendingAnim.addOnFrameCallback(this::updateCurveProperties);
+ }
+
+ pendingAnim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ resetFromSplitSelectionState();
+ }
+ });
+
+ return pendingAnim;
+ }
+
+ private void resetFromSplitSelectionState() {
+ mSplitHiddenTaskView.setTranslationY(0);
+ int pageToSnapTo = mCurrentPage;
+ if (mSplitHiddenTaskViewIndex <= pageToSnapTo) {
+ pageToSnapTo += 1;
+ } else {
+ pageToSnapTo = mSplitHiddenTaskViewIndex;
+ }
+ snapToPageImmediately(pageToSnapTo);
+ onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
+ resetTaskVisuals();
+ mSplitHiddenTaskView = null;
+ mSplitHiddenTaskViewIndex = -1;
+ }
+
private void updateDeadZoneRects() {
// Get the deadzone rect surrounding the clear all button to not dismiss overview to home
mClearAllButtonDeadZoneRect.setEmpty();
diff --git a/quickstep/src/com/android/quickstep/views/SplitPlaceholderView.java b/quickstep/src/com/android/quickstep/views/SplitPlaceholderView.java
new file mode 100644
index 0000000..fb9be81
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/SplitPlaceholderView.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.quickstep.util.SplitSelectStateController;
+
+public class SplitPlaceholderView extends View {
+
+ public static final FloatProperty<SplitPlaceholderView> ALPHA_FLOAT =
+ new FloatProperty<SplitPlaceholderView>("SplitViewAlpha") {
+ @Override
+ public void setValue(SplitPlaceholderView splitPlaceholderView, float v) {
+ splitPlaceholderView.setVisibility(v != 0 ? VISIBLE : GONE);
+ splitPlaceholderView.setAlpha(v);
+ }
+
+ @Override
+ public Float get(SplitPlaceholderView splitPlaceholderView) {
+ return splitPlaceholderView.getAlpha();
+ }
+ };
+
+ private SplitSelectStateController mSplitController;
+
+ public SplitPlaceholderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void init(SplitSelectStateController controller) {
+ this.mSplitController = controller;
+ }
+
+ public SplitSelectStateController getSplitController() {
+ return mSplitController;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index fe7ece2..a5b7a5b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -186,7 +186,8 @@
mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
mTaskName.setOnClickListener(v -> close(true));
- TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
+ TaskOverlayFactory.getEnabledShortcuts(taskView, mActivity.getDeviceProfile())
+ .forEach(this::addMenuOption);
}
private void addMenuOption(SystemShortcut menuOption) {
@@ -196,6 +197,8 @@
menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
+ menuOptionView.setEnabled(menuOption.isEnabled());
+ menuOptionView.setAlpha(menuOption.isEnabled() ? 1 : 0.5f);
menuOptionView.setOnClickListener(view -> {
if (LIVE_TILE.get()) {
RecentsView recentsView = mTaskView.getRecentsView();
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index eace0f8..cd8ea76 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -28,6 +28,7 @@
import static android.widget.Toast.LENGTH_SHORT;
import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
import static com.android.launcher3.Utilities.comp;
import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
@@ -84,6 +85,7 @@
import com.android.launcher3.util.ActivityOptionsWrapper;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.TransformingTouchDelegate;
import com.android.launcher3.util.ViewPool.Reusable;
import com.android.quickstep.RecentsModel;
@@ -345,7 +347,12 @@
});
anim.start();
} else {
- launchTaskAnimated();
+ if (mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
+ // User tapped to select second split screen app
+ getRecentsView().confirmSplitSelect(this);
+ } else {
+ launchTaskAnimated();
+ }
}
mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
.log(LAUNCHER_TASK_LAUNCH_TAP);
@@ -591,6 +598,11 @@
}
private boolean showTaskMenu() {
+ if (getRecentsView().mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
+ // Don't show menu when selecting second split screen app
+ return true;
+ }
+
if (!getRecentsView().isClearAllHidden()) {
getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
} else {
@@ -619,6 +631,10 @@
}
}
+ public int getThumbnailTopMargin() {
+ return (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+ }
+
public void setOrientationState(RecentsOrientedState orientationState) {
PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
@@ -998,12 +1014,12 @@
return Utilities.mapRange(progress, 0, endTranslation);
}
- public FloatProperty<TaskView> getFillDismissGapTranslationProperty() {
+ public FloatProperty<TaskView> getPrimaryDismissTranslationProperty() {
return getPagedOrientationHandler().getPrimaryValue(
DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
}
- public FloatProperty<TaskView> getDismissTaskTranslationProperty() {
+ public FloatProperty<TaskView> getSecondaryDissmissTranslationProperty() {
return getPagedOrientationHandler().getSecondaryValue(
DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
}
@@ -1081,7 +1097,8 @@
getContext().getText(R.string.accessibility_close)));
final Context context = getContext();
- for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
+ for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+ mActivity.getDeviceProfile())) {
info.addAction(s.createAccessibilityAction(context));
}
@@ -1113,7 +1130,8 @@
return true;
}
- for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
+ for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+ mActivity.getDeviceProfile())) {
if (s.hasHandlerForAction(action)) {
s.onClick(this);
return true;
@@ -1274,6 +1292,12 @@
mSnapshotView.setOverlayEnabled(overlayEnabled);
}
+ public void initiateSplitSelect(SplitPositionOption splitPositionOption) {
+ RecentsView rv = getRecentsView();
+ getMenuView().close(false);
+ rv.initiateSplitSelect(this, splitPositionOption);
+ }
+
/**
* We update and subsequently draw these in {@link #setFullscreenProgress(float)}.
*/
diff --git a/res/drawable/ic_gm_close_24.xml b/res/drawable/ic_gm_close_24.xml
new file mode 100644
index 0000000..2c9c932
--- /dev/null
+++ b/res/drawable/ic_gm_close_24.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?android:attr/textColorTertiary"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z"/>
+</vector>
diff --git a/quickstep/res/drawable/ic_split_screen.xml b/res/drawable/ic_split_screen.xml
similarity index 100%
rename from quickstep/res/drawable/ic_split_screen.xml
rename to res/drawable/ic_split_screen.xml
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index 6c18d7a..226c4f7 100644
--- a/res/layout/widgets_full_sheet.xml
+++ b/res/layout/widgets_full_sheet.xml
@@ -51,5 +51,13 @@
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="@dimen/fastscroll_end_margin" />
+
+ <com.android.launcher3.widget.picker.WidgetsRecyclerView
+ android:id="@+id/search_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:clipToPadding="false" />
+
</com.android.launcher3.views.TopRoundedCornerView>
</com.android.launcher3.widget.picker.WidgetsFullSheet>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_search_and_recommendations.xml b/res/layout/widgets_full_sheet_search_and_recommendations.xml
index 9a6f922..6182255 100644
--- a/res/layout/widgets_full_sheet_search_and_recommendations.xml
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -34,16 +34,5 @@
android:textSize="24sp"
android:layout_marginTop="16dp"
android:text="@string/widget_button_text"/>
- <!-- Disable the search bar because it has not been implemented. -->
- <EditText
- android:id="@+id/widgets_search_bar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- android:layout_marginTop="16dp"
- android:background="@drawable/bg_widgets_searchbox"
- android:drawablePadding="8dp"
- android:drawableStart="@drawable/ic_allapps_search"
- android:hint="@string/widgets_full_sheet_search_bar_hint"
- android:padding="12dp" />
+ <include layout="@layout/widgets_search_bar"/>
</LinearLayout>
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
index 041e007..1590286 100644
--- a/res/layout/widgets_list_row_header.xml
+++ b/res/layout/widgets_list_row_header.xml
@@ -19,7 +19,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
- android:paddingVertical="20dp"
+ android:paddingVertical="@dimen/widget_list_header_view_vertical_padding"
android:orientation="horizontal">
<ImageView
@@ -52,6 +52,8 @@
android:id="@+id/app_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
tools:text="m widgets, n shortcuts" />
</LinearLayout>
diff --git a/res/layout/widgets_search_bar.xml b/res/layout/widgets_search_bar.xml
new file mode 100644
index 0000000..252637d
--- /dev/null
+++ b/res/layout/widgets_search_bar.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.launcher3.widget.picker.search.WidgetsSearchBar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widgets_search_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginTop="16dp"
+ android:background="@drawable/bg_widgets_searchbox"
+ android:padding="12dp"
+ android:visibility="gone">
+
+ <EditText
+ android:id="@+id/widgets_search_bar_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawablePadding="8dp"
+ android:drawableStart="@drawable/ic_allapps_search"
+ android:background="@null"
+ android:hint="@string/widgets_full_sheet_search_bar_hint"
+ android:maxLines="1"
+ android:layout_weight="1"
+ android:inputType="text"/>
+
+ <ImageButton
+ android:id="@+id/widgets_search_cancel_button"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:src="@drawable/ic_gm_close_24"
+ android:background="?android:selectableItemBackground"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+</com.android.launcher3.widget.picker.search.WidgetsSearchBar>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index da43758..d135b43 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -108,6 +108,8 @@
<dimen name="widget_cell_vertical_padding">8dp</dimen>
<dimen name="widget_cell_horizontal_padding">16dp</dimen>
+ <dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
+
<dimen name="widget_preview_shadow_blur">0.5dp</dimen>
<dimen name="widget_preview_key_shadow_distance">1dp</dimen>
<dimen name="widget_preview_corner_radius">2dp</dimen>
diff --git a/res/values/id.xml b/res/values/id.xml
new file mode 100644
index 0000000..39c49bd
--- /dev/null
+++ b/res/values/id.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <item type="id" name="view_type_widgets_list" />
+ <item type="id" name="view_type_widgets_header" />
+ <item type="id" name="view_type_widgets_search_header" />
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 351182d..0600cae 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -38,6 +38,13 @@
<!-- User visible name for the launcher/home screen. [CHAR_LIMIT=30] -->
<string name="home_screen">Home</string>
+ <!-- Options for recent tasks -->
+ <!-- Title for an option to enter split screen mode for a given app -->
+ <string name="recent_task_option_split_screen">Split screen</string>
+ <string translatable="false" name="split_screen_position_top">Pin to top</string>
+ <string translatable="false" name="split_screen_position_left">Pin to left</string>
+ <string translatable="false" name="split_screen_position_right">Pin to right</string>
+
<!-- Widgets -->
<!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->
<string name="long_press_widget_to_add">Touch & hold to move a widget.</string>
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
index b972c6f..cc36f63 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -221,6 +221,27 @@
assertThat(currentList).containsExactlyElementsIn(newList);
}
+ @Test
+ public void headersContentsMix_headerWidgetsModified_shouldInvokeCorrectCallbacks() {
+ // GIVEN the current list has app headers [A, B, E content].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mContentE));
+ // GIVEN the new list has one of the headers widgets list modified.
+ List<WidgetsListBaseEntry> newList = List.of(
+ new WidgetsListHeaderEntry(
+ mHeaderA.mPkgItem, mHeaderA.mTitleSectionName,
+ mHeaderA.mWidgets.subList(0, 1)),
+ mHeaderB, mContentE);
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN notify "A" has been changed.
+ verify(mAdapter).notifyItemChanged(/* position= */ 0);
+ // THEN the current list contains all elements from the new list.
+ assertThat(currentList).containsExactlyElementsIn(newList);
+ }
+
private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
int numOfWidgets) {
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
index a7c8d92..e1214ff 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -26,6 +26,8 @@
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
+import android.os.Process;
+import android.os.UserHandle;
import android.view.LayoutInflater;
import androidx.recyclerview.widget.RecyclerView;
@@ -37,6 +39,7 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -67,6 +70,7 @@
private WidgetsListAdapter mAdapter;
private InvariantDeviceProfile mTestProfile;
+ private UserHandle mUserHandle;
private Context mContext;
@Before
@@ -76,6 +80,7 @@
mTestProfile = new InvariantDeviceProfile();
mTestProfile.numRows = 5;
mTestProfile.numColumns = 5;
+ mUserHandle = Process.myUserHandle();
mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
mIconCache, null, null);
mAdapter.registerAdapterDataObserver(mListener);
@@ -126,7 +131,8 @@
mAdapter.setWidgets(generateSampleMap(3));
// WHEN com.google.test.1 header is expanded.
- mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1);
+ mAdapter.onHeaderClicked(/* showWidgets= */ true,
+ new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
// THEN the visible entries list becomes:
// [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
@@ -143,7 +149,8 @@
// GIVEN test com.google.test1 is expanded.
// Visible entries in the adapter are:
// [com.google.test0, com.google.test1, com.google.test1 content]
- mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1);
+ mAdapter.onHeaderClicked(/* showWidgets= */ true,
+ new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
Mockito.reset(mListener);
// WHEN the adapter is updated with the same list of apps but com.google.test1 has 2 widgets
@@ -200,6 +207,30 @@
verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1);
}
+ @Test
+ public void setWidgetsOnSearch_expandedApp_shouldResetExpandedApp() {
+ // GIVEN a list of widgets entries:
+ // [com.google.test0, com.google.test0 content,
+ // com.google.test1, com.google.test1 content,
+ // com.google.test2, com.google.test2 content]
+ // The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2].
+ ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(2);
+ mAdapter.setWidgetsOnSearch(allEntries);
+ // GIVEN com.google.test.1 header is expanded. The visible entries list becomes:
+ // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
+ mAdapter.onHeaderClicked(/* showWidgets= */ true,
+ new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
+ Mockito.reset(mListener);
+
+ // WHEN same widget entries are set again.
+ mAdapter.setWidgetsOnSearch(allEntries);
+
+ // THEN expanded app is reset and the visible entries list becomes:
+ // [com.google.test0, com.google.test1, com.google.test2]
+ verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull());
+ verify(mListener).onItemRangeRemoved(/* positionStart= */ 2, /* itemCount= */ 1);
+ }
+
/**
* Generates a list of sample widget entries.
*
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index 848630e..e8c11da 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -18,7 +18,9 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.appwidget.AppWidgetProviderInfo;
@@ -26,12 +28,9 @@
import android.content.Context;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
-import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
-import androidx.annotation.Nullable;
-
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
@@ -41,10 +40,9 @@
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
-import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
import org.junit.After;
import org.junit.Before;
@@ -74,12 +72,13 @@
// testing.
private ActivityController<TestActivity> mActivityController;
private TestActivity mTestActivity;
- private FakeOnHeaderClickListener mFakeOnHeaderClickListener = new FakeOnHeaderClickListener();
@Mock
private IconCache mIconCache;
@Mock
private DeviceProfile mDeviceProfile;
+ @Mock
+ private OnHeaderClickListener mOnHeaderClickListener;
@Before
public void setUp() {
@@ -99,8 +98,7 @@
}).when(mIconCache).getTitleNoCache(any());
mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
- LayoutInflater.from(mTestActivity),
- mFakeOnHeaderClickListener);
+ LayoutInflater.from(mTestActivity), mOnHeaderClickListener);
}
@After
@@ -125,6 +123,23 @@
assertThat(appSubtitle.getText()).isEqualTo("3 widgets");
}
+ @Test
+ public void bindViewHolder_shouldAttachOnHeaderClickListener() {
+ WidgetsListHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ WidgetsListHeaderEntry entry = generateSampleAppHeader(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+
+ mViewHolderBinder.bindViewHolder(viewHolder, entry);
+ widgetsListHeader.callOnClick();
+
+ verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
+ eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
+ }
+
private WidgetsListHeaderEntry generateSampleAppHeader(String appName, String packageName,
int numOfWidgets) {
PackageItemInfo appInfo = new PackageItemInfo(packageName);
@@ -152,22 +167,4 @@
}
return widgetItems;
}
-
- private void assertWidgetCellWithLabel(View view, String label) {
- assertThat(view).isInstanceOf(WidgetCell.class);
- TextView widgetLabel = (TextView) view.findViewById(R.id.widget_name);
- assertThat(widgetLabel.getText()).isEqualTo(label);
- }
-
- private final class FakeOnHeaderClickListener implements OnHeaderClickListener {
-
- boolean mShowWidgets = false;
- @Nullable String mHeaderClickedPackage = null;
-
- @Override
- public void onHeaderClicked(boolean showWidgets, String packageName) {
- mShowWidgets = showWidgets;
- mHeaderClickedPackage = packageName;
- }
- }
}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
new file mode 100644
index 0000000..07fbfd2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListSearchHeaderViewHolderBinderTest {
+ private static final String TEST_PACKAGE = "com.google.test";
+ private static final String APP_NAME = "Test app";
+
+ private Context mContext;
+ private WidgetsListSearchHeaderViewHolderBinder mViewHolderBinder;
+ private InvariantDeviceProfile mTestProfile;
+ // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+ // testing.
+ private ActivityController<TestActivity> mActivityController;
+ private TestActivity mTestActivity;
+
+ @Mock
+ private IconCache mIconCache;
+ @Mock
+ private DeviceProfile mDeviceProfile;
+ @Mock
+ private OnHeaderClickListener mOnHeaderClickListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+
+ mActivityController = Robolectric.buildActivity(TestActivity.class);
+ mTestActivity = mActivityController.setup().get();
+ mTestActivity.setDeviceProfile(mDeviceProfile);
+
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+ return componentWithLabel.getComponent().getShortClassName();
+ }).when(mIconCache).getTitleNoCache(any());
+
+ mViewHolderBinder = new WidgetsListSearchHeaderViewHolderBinder(
+ LayoutInflater.from(mTestActivity), mOnHeaderClickListener);
+ }
+
+ @After
+ public void tearDown() {
+ mActivityController.destroy();
+ }
+
+ @Test
+ public void bindViewHolder_appWith3Widgets_shouldShowTheCorrectAppNameAndSubtitle() {
+ WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+ mViewHolderBinder.bindViewHolder(viewHolder, entry);
+
+ TextView appTitle = widgetsListHeader.findViewById(R.id.app_title);
+ TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle);
+ assertThat(appTitle.getText()).isEqualTo(APP_NAME);
+ assertThat(appSubtitle.getText())
+ .isEqualTo(".SampleWidget0, .SampleWidget1, .SampleWidget2");
+ }
+
+ @Test
+ public void bindViewHolder_shouldAttachOnHeaderClickListener() {
+ WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+
+ mViewHolderBinder.bindViewHolder(viewHolder, entry);
+ widgetsListHeader.callOnClick();
+
+ verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
+ eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
+ }
+
+ private WidgetsListSearchHeaderEntry generateSampleSearchHeader(String appName,
+ String packageName, int numOfWidgets) {
+ PackageItemInfo appInfo = new PackageItemInfo(packageName);
+ appInfo.title = appName;
+ appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+ return new WidgetsListSearchHeaderEntry(appInfo,
+ /* titleSectionName= */ "",
+ generateWidgetItems(packageName, numOfWidgets));
+ }
+
+ private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ for (int i = 0; i < numOfWidgets; i++) {
+ ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(cn));
+
+ widgetItems.add(new WidgetItem(
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+ mTestProfile, mIconCache));
+ }
+ return widgetItems;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipelineTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipelineTest.java
index 8aebf12..17ededd 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipelineTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipelineTest.java
@@ -19,8 +19,6 @@
import static android.os.Looper.getMainLooper;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.robolectric.Shadows.shadowOf;
@@ -40,6 +38,7 @@
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
import org.junit.Before;
import org.junit.Test;
@@ -56,9 +55,6 @@
@RunWith(RobolectricTestRunner.class)
public class SimpleWidgetsSearchPipelineTest {
- private static final SimpleWidgetsSearchPipeline.StringMatcher MATCHER =
- SimpleWidgetsSearchPipeline.StringMatcher.getInstance();
-
@Mock private IconCache mIconCache;
private InvariantDeviceProfile mTestProfile;
@@ -73,9 +69,10 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
- .getComponent().getPackageName())
- .when(mIconCache).getTitleNoCache(any());
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+ return componentWithLabel.getComponent().getShortClassName();
+ }).when(mIconCache).getTitleNoCache(any());
mTestProfile = new InvariantDeviceProfile();
mTestProfile.numRows = 5;
mTestProfile.numColumns = 5;
@@ -85,54 +82,60 @@
createWidgetsHeaderEntry("com.example.android.Calendar", "Calendar", 2);
mCalendarContentEntry =
createWidgetsContentEntry("com.example.android.Calendar", "Calendar", 2);
- mCameraHeaderEntry = createWidgetsHeaderEntry("com.example.android.Camera", "Camera", 5);
- mCameraContentEntry = createWidgetsContentEntry("com.example.android.Camera", "Camera", 5);
+ mCameraHeaderEntry = createWidgetsHeaderEntry("com.example.android.Camera", "Camera", 11);
+ mCameraContentEntry = createWidgetsContentEntry("com.example.android.Camera", "Camera", 11);
mClockHeaderEntry = createWidgetsHeaderEntry("com.example.android.Clock", "Clock", 3);
mClockContentEntry = createWidgetsContentEntry("com.example.android.Clock", "Clock", 3);
}
@Test
- public void query_shouldInformCallbackWithResultsMatchedOnAppName() {
+ public void query_shouldMatchOnAppName() {
SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
mCameraContentEntry, mClockHeaderEntry, mClockContentEntry));
pipeline.query("Ca", results ->
- assertEquals(results, List.of(mCalendarHeaderEntry, mCalendarContentEntry,
- mCameraHeaderEntry, mCameraContentEntry)));
+ assertEquals(results,
+ List.of(
+ new WidgetsListSearchHeaderEntry(
+ mCalendarHeaderEntry.mPkgItem,
+ mCalendarHeaderEntry.mTitleSectionName,
+ mCalendarHeaderEntry.mWidgets),
+ mCalendarContentEntry,
+ new WidgetsListSearchHeaderEntry(
+ mCameraHeaderEntry.mPkgItem,
+ mCameraHeaderEntry.mTitleSectionName,
+ mCameraHeaderEntry.mWidgets),
+ mCameraContentEntry)));
shadowOf(getMainLooper()).idle();
}
@Test
- public void testMatches() {
- assertTrue(MATCHER.matches("q", "Q"));
- assertTrue(MATCHER.matches("q", " Q"));
- assertTrue(MATCHER.matches("e", "elephant"));
- assertTrue(MATCHER.matches("eL", "Elephant"));
- assertTrue(MATCHER.matches("elephant ", "elephant"));
- assertTrue(MATCHER.matches("whitec", "white cow"));
- assertTrue(MATCHER.matches("white c", "white cow"));
- assertTrue(MATCHER.matches("white ", "white cow"));
- assertTrue(MATCHER.matches("white c", "white cow"));
- assertTrue(MATCHER.matches("电", "电子邮件"));
- assertTrue(MATCHER.matches("电子", "电子邮件"));
- assertTrue(MATCHER.matches("다", "다운로드"));
- assertTrue(MATCHER.matches("드", "드라이브"));
- assertTrue(MATCHER.matches("åbç", "abc"));
- assertTrue(MATCHER.matches("ål", "Alpha"));
+ public void query_shouldMatchOnWidgetLabel() {
+ SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
+ List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
+ mCameraContentEntry));
- assertFalse(MATCHER.matches("phant", "elephant"));
- assertFalse(MATCHER.matches("elephants", "elephant"));
- assertFalse(MATCHER.matches("cow", "white cow"));
- assertFalse(MATCHER.matches("cow", "whiteCow"));
- assertFalse(MATCHER.matches("dog", "cats&Dogs"));
- assertFalse(MATCHER.matches("ba", "Bot"));
- assertFalse(MATCHER.matches("ba", "bot"));
- assertFalse(MATCHER.matches("子", "电子邮件"));
- assertFalse(MATCHER.matches("邮件", "电子邮件"));
- assertFalse(MATCHER.matches("ㄷ", "다운로드 드라이브"));
- assertFalse(MATCHER.matches("ㄷㄷ", "다운로드 드라이브"));
- assertFalse(MATCHER.matches("åç", "abc"));
+ pipeline.query("Widget1", results ->
+ assertEquals(results,
+ List.of(
+ new WidgetsListSearchHeaderEntry(
+ mCalendarHeaderEntry.mPkgItem,
+ mCalendarHeaderEntry.mTitleSectionName,
+ mCalendarHeaderEntry.mWidgets.subList(1, 2)),
+ new WidgetsListContentEntry(
+ mCalendarHeaderEntry.mPkgItem,
+ mCalendarHeaderEntry.mTitleSectionName,
+ mCalendarHeaderEntry.mWidgets.subList(1, 2)),
+ new WidgetsListSearchHeaderEntry(
+ mCameraHeaderEntry.mPkgItem,
+ mCameraHeaderEntry.mTitleSectionName,
+ mCameraHeaderEntry.mWidgets.subList(1, 3)),
+ new WidgetsListContentEntry(
+ mCameraHeaderEntry.mPkgItem,
+ mCameraHeaderEntry.mTitleSectionName,
+ mCameraHeaderEntry.mWidgets.subList(1, 3)))));
+ shadowOf(getMainLooper()).idle();
}
private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java
new file mode 100644
index 0000000..7fc9650
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+@RunWith(RobolectricTestRunner.class)
+public class WidgetsSearchBarControllerTest {
+
+ private WidgetsSearchBarController mController;
+ private Context mContext;
+ private EditText mEditText;
+ private ImageButton mCancelButton;
+ @Mock
+ private SearchModeListener mSearchModeListener;
+ @Mock
+ private SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mEditText = new EditText(mContext);
+ mCancelButton = new ImageButton(mContext);
+ mController = new WidgetsSearchBarController(
+ mSearchAlgorithm, mEditText, mCancelButton, mSearchModeListener);
+ }
+
+ @Test
+ public void onSearchResult_shouldInformSearchModeListener() {
+ ArrayList<WidgetsListBaseEntry> entries = new ArrayList<>();
+ mController.onSearchResult("abc", entries);
+
+ verify(mSearchModeListener).onSearchResults(entries);
+ }
+
+ @Test
+ public void afterTextChanged_shouldInformSearchModeListenerToEnterSearch() {
+ mEditText.setText("abc");
+
+ verify(mSearchModeListener).enterSearchMode();
+ verifyNoMoreInteractions(mSearchModeListener);
+ }
+
+ @Test
+ public void afterTextChanged_shouldDoSearch() {
+ mEditText.setText("abc");
+
+ verify(mSearchAlgorithm).doSearch(eq("abc"), any());
+ }
+
+ @Test
+ public void afterTextChanged_shouldShowCancelButton() {
+ mEditText.setText("abc");
+
+ assertEquals(mCancelButton.getVisibility(), View.VISIBLE);
+ }
+
+ @Test
+ public void afterTextChanged_empty_shouldInformSearchModeListenerToExitSearch() {
+ mEditText.setText("");
+
+ verify(mSearchModeListener).exitSearchMode();
+ verifyNoMoreInteractions(mSearchModeListener);
+ }
+
+ @Test
+ public void afterTextChanged_empty_shouldCancelSearch() {
+ mEditText.setText("");
+
+ verify(mSearchAlgorithm).cancel(true);
+ verifyNoMoreInteractions(mSearchAlgorithm);
+ }
+
+ @Test
+ public void afterTextChanged_empty_shouldHideCancelButton() {
+ mEditText.setText("");
+
+ assertEquals(mCancelButton.getVisibility(), View.GONE);
+ }
+
+ @Test
+ public void cancelSearch_shouldInformSearchModeListenerToExitSearch() {
+ mCancelButton.performClick();
+
+ verify(mSearchModeListener).exitSearchMode();
+ verifyNoMoreInteractions(mSearchModeListener);
+ }
+
+ @Test
+ public void cancelSearch_shouldCancelSearch() {
+ mCancelButton.performClick();
+
+ verify(mSearchAlgorithm).cancel(true);
+ verifyNoMoreInteractions(mSearchAlgorithm);
+ }
+
+ @Test
+ public void cancelSearch_shouldClearSearchBar() {
+ mCancelButton.performClick();
+
+ assertEquals(mEditText.getText().toString(), "");
+ }
+}
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 95cdbdd..d894bb4 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -98,6 +98,13 @@
public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
| TYPE_SNACKBAR | TYPE_WIDGET_RESIZE_FRAME | TYPE_LISTENER;
+ // When these types of floating views are open, hide the taskbar hotseat and show the real one.
+ public static final int TYPE_REPLACE_TASKBAR_WITH_HOTSEAT = TYPE_FOLDER | TYPE_ACTION_POPUP;
+
+ // Hide the taskbar when these types of floating views are open.
+ public static final int TYPE_HIDE_TASKBAR = TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGETS_FULL_SHEET
+ | TYPE_ON_BOARD_POPUP;
+
public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER
& ~TYPE_ALL_APPS_EDU;
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 2440854..90cc384 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -167,6 +167,8 @@
// Taskbar
public boolean isTaskbarPresent;
public int taskbarSize;
+ // How much of the bottom inset is due to Taskbar rather than other system elements.
+ public int nonOverlappingTaskbarInset;
DeviceProfile(Context context, InvariantDeviceProfile inv, Info info,
Point minSize, Point maxSize, int width, int height, boolean isLandscape,
@@ -221,7 +223,7 @@
WindowInsets windowInsets = DisplayController.INSTANCE.get(context).getHolder(mInfo.id)
.getDisplayContext().getSystemService(WindowManager.class)
.getCurrentWindowMetrics().getWindowInsets();
- int nonOverlappingTaskbarInset =
+ nonOverlappingTaskbarInset =
taskbarSize - windowInsets.getSystemWindowInsetBottom();
if (nonOverlappingTaskbarInset > 0) {
nonFinalAvailableHeightPx -= nonOverlappingTaskbarInset;
@@ -708,10 +710,11 @@
mInsets.top + availableHeightPx);
} else {
// Folders should only appear below the drop target bar and above the hotseat
+ int hotseatTop = isTaskbarPresent ? taskbarSize : hotseatBarSizePx;
return new Rect(mInsets.left + edgeMarginPx,
mInsets.top + dropTargetBarSizePx + edgeMarginPx,
mInsets.left + availableWidthPx - edgeMarginPx,
- mInsets.top + availableHeightPx - hotseatBarSizePx
+ mInsets.top + availableHeightPx - hotseatTop
- workspacePageIndicatorHeight - edgeMarginPx);
}
}
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 02c6162..c79dabe 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -131,10 +131,9 @@
public void reset() {
if (!TextUtils.isEmpty(getText())) {
setText("");
- } else {
- if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
- return;
- }
+ }
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ return;
}
if (isFocused()) {
View nextFocus = focusSearch(View.FOCUS_DOWN);
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index b2112ad..e5b75c1 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -29,6 +29,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.MultiValueAlpha;
import java.util.function.Consumer;
@@ -37,6 +38,10 @@
*/
public class Hotseat extends CellLayout implements Insettable {
+ private static final int ALPHA_INDEX_STATE = 0;
+ private static final int ALPHA_INDEX_REPLACE_TASKBAR = 1;
+ private static final int NUM_ALPHA_CHANNELS = 2;
+
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mHasVerticalHotseat;
private Workspace mWorkspace;
@@ -44,6 +49,8 @@
@Nullable
private Consumer<Boolean> mOnVisibilityAggregatedCallback;
+ private final MultiValueAlpha mMultiValueAlpha;
+
public Hotseat(Context context) {
this(context, null);
}
@@ -54,6 +61,8 @@
public Hotseat(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS, MultiValueAlpha.Mode.MAX);
+ mMultiValueAlpha.setUpdateVisibility(true);
}
/**
@@ -174,4 +183,12 @@
public View getFirstItemMatch(Workspace.ItemOperator itemOperator) {
return mWorkspace.getFirstMatch(new CellLayout[] { this }, itemOperator);
}
+
+ public MultiValueAlpha.AlphaProperty getStateAlpha() {
+ return mMultiValueAlpha.getProperty(ALPHA_INDEX_STATE);
+ }
+
+ public MultiValueAlpha.AlphaProperty getReplaceTaskbarAlpha() {
+ return mMultiValueAlpha.getProperty(ALPHA_INDEX_REPLACE_TASKBAR);
+ }
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 1546ee3..c57f621 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -113,7 +113,6 @@
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.allapps.search.LiveSearchManager;
import com.android.launcher3.anim.PropertyListBuilder;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
@@ -277,8 +276,6 @@
private Configuration mOldConfig;
- private LiveSearchManager mLiveSearchManager;
-
@Thunk
Workspace mWorkspace;
@Thunk
@@ -401,8 +398,6 @@
mAllAppsController = new AllAppsTransitionController(this);
mStateManager = new StateManager<>(this, NORMAL);
- mLiveSearchManager = new LiveSearchManager(this);
-
mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs);
mAppWidgetManager = new WidgetManagerHelper(this);
@@ -490,10 +485,6 @@
}
}
- public LiveSearchManager getLiveSearchManager() {
- return mLiveSearchManager;
- }
-
protected LauncherOverlayManager getDefaultOverlay() {
return new LauncherOverlayManager() { };
}
@@ -1594,7 +1585,6 @@
mOverlayManager.onActivityDestroyed(this);
mUserChangedCallbackCloseable.close();
- mLiveSearchManager.stop();
}
public LauncherAccessibilityDelegate getAccessibilityDelegate() {
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 76c4518..83ddf64 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -41,8 +41,15 @@
}
private void handleSystemWindowInsets(Rect insets) {
- // Update device profile before notifying th children.
- mActivity.getDeviceProfile().updateInsets(insets);
+ DeviceProfile dp = mActivity.getDeviceProfile();
+
+ // Taskbar provides insets, but we don't want that for most Launcher elements so remove it.
+ mTempRect.set(insets);
+ insets = mTempRect;
+ insets.bottom = Math.max(0, insets.bottom - dp.nonOverlappingTaskbarInset);
+
+ // Update device profile before notifying the children.
+ dp.updateInsets(insets);
boolean resetState = !insets.equals(mInsets);
setInsets(insets);
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 21c40ef..aa97450 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -22,6 +22,7 @@
import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.OVERVIEW_MODAL_TASK_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
@@ -60,6 +61,7 @@
public static final int TASKBAR = 1 << 7;
public static final int CLEAR_ALL_BUTTON = 1 << 8;
public static final int WORKSPACE_PAGE_INDICATOR = 1 << 9;
+ public static final int SPLIT_PLACHOLDER_VIEW = 1 << 10;
/** Mask of all the items that are contained in the apps view. */
public static final int APPS_VIEW_ITEM_MASK =
@@ -126,6 +128,8 @@
OverviewState.newSwitchState(QUICK_SWITCH_STATE_ORDINAL);
public static final LauncherState BACKGROUND_APP =
OverviewState.newBackgroundState(BACKGROUND_APP_STATE_ORDINAL);
+ public static final LauncherState OVERVIEW_SPLIT_SELECT =
+ OverviewState.newSplitSelectState(OVERVIEW_SPLIT_SELECT_ORDINAL);
public final int ordinal;
@@ -241,6 +245,14 @@
}
/**
+ * For this state, how much additional vertical translation there should be for each of the
+ * child TaskViews.
+ */
+ public float getOverviewSecondaryTranslation(Launcher launcher) {
+ return 0;
+ }
+
+ /**
* The amount of blur and wallpaper zoom to apply to the background of either the app
* or Launcher surface in this state. Should be a number between 0 and 1, inclusive.
*
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 6da090c..d1daac8 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -310,6 +310,8 @@
Rect padding = grid.workspacePadding;
setPadding(padding.left, padding.top, padding.right, padding.bottom);
mInsets.set(insets);
+ // Increase our bottom insets so we don't overlap with the taskbar.
+ mInsets.bottom += grid.nonOverlappingTaskbarInset;
if (mWorkspaceFadeInAdjacentScreens) {
// In landscape mode the page spacing is set to the default.
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 660eeab..d6d2f73 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -55,6 +55,7 @@
import com.android.launcher3.graphics.WorkspaceDragScrim;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.MultiValueAlpha;
import com.android.systemui.plugins.ResourceProvider;
/**
@@ -143,8 +144,8 @@
}
float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
- propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha,
- config.getInterpolator(ANIM_HOTSEAT_FADE, fadeInterpolator));
+ propertySetter.setFloat(hotseat.getStateAlpha(), MultiValueAlpha.VALUE,
+ hotseatIconsAlpha, config.getInterpolator(ANIM_HOTSEAT_FADE, fadeInterpolator));
float workspacePageIndicatorAlpha = (elements & WORKSPACE_PAGE_INDICATOR) != 0 ? 1 : 0;
propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
workspacePageIndicatorAlpha, fadeInterpolator);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index edd9a9f..fdc69ec 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -201,9 +201,9 @@
}
if (!mAH[AdapterHolder.MAIN].appsList.hasFilter()) {
rebindAdapters(hasWorkApps);
- }
- if (hasWorkApps) {
- resetWorkProfile();
+ if (hasWorkApps) {
+ resetWorkProfile();
+ }
}
}
@@ -246,11 +246,7 @@
hideInput();
return false;
}
- boolean shouldScroll = rv.shouldContainerScroll(ev, mLauncher.getDragLayer());
- if (shouldScroll) {
- hideInput();
- }
- return shouldScroll;
+ return rv.shouldContainerScroll(ev, mLauncher.getDragLayer());
}
@Override
@@ -395,7 +391,8 @@
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
if (Utilities.ATLEAST_Q) {
- mNavBarScrimHeight = insets.getTappableElementInsets().bottom;
+ mNavBarScrimHeight = insets.getTappableElementInsets().bottom
+ - mLauncher.getDeviceProfile().nonOverlappingTaskbarInset;
} else {
mNavBarScrimHeight = insets.getStableInsetBottom();
}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 179cb77..f307a53 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -31,6 +31,7 @@
import android.util.SparseIntArray;
import android.view.MotionEvent;
import android.view.View;
+import android.view.WindowInsets;
import androidx.recyclerview.widget.RecyclerView;
@@ -188,6 +189,7 @@
case SCROLL_STATE_DRAGGING:
mgr.logger().sendToInteractionJankMonitor(
LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this);
+ getWindowInsetsController().hide(WindowInsets.Type.ime());
break;
case SCROLL_STATE_IDLE:
mgr.logger().sendToInteractionJankMonitor(
diff --git a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
index f4d735e..269e390 100644
--- a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
+++ b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
@@ -56,11 +56,10 @@
SectionDecorationInfo sectionInfo = adapterItem.sectionDecorationInfo;
SectionDecorationHandler decorationHandler = sectionInfo.getDecorationHandler();
if (decorationHandler != null) {
- decorationHandler.extendBounds(view);
if (sectionInfo.isFocusedView()) {
decorationHandler.onFocusDraw(c, view);
} else {
- decorationHandler.onGroupDraw(c);
+ decorationHandler.onGroupDraw(c, view);
}
}
}
@@ -131,26 +130,13 @@
}
/**
- * Extends current bounds to include the view.
- */
- public void extendBounds(View view) {
- if (mBounds.isEmpty()) {
- mBounds.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
- } else {
- mBounds.set(
- Math.min(mBounds.left, view.getLeft()),
- Math.min(mBounds.top, view.getTop()),
- Math.max(mBounds.right, view.getRight()),
- Math.max(mBounds.bottom, view.getBottom())
- );
- }
- }
-
- /**
* Draw bounds onto canvas.
*/
- public void onGroupDraw(Canvas canvas) {
+ public void onGroupDraw(Canvas canvas, View view) {
+ if (view == null) return;
+
mPaint.setColor(mFillcolor);
+ mBounds.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
onDraw(canvas);
}
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index abf63dc..1e6f829 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -246,6 +246,7 @@
* TODO: This logic should go in {@link LauncherState}
*/
private void onProgressAnimationEnd() {
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) return;
if (Float.compare(mProgress, 1f) == 0) {
mAppsView.reset(false /* animate */);
}
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 3319018..d3c9993 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -144,7 +144,7 @@
@Override
public void onFocusChange(View view, boolean hasFocus) {
- if (!hasFocus) {
+ if (!hasFocus && !FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
mInput.hideKeyboard();
}
}
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
index f9fb22e..34895ed 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
@@ -25,6 +25,7 @@
import com.android.launcher3.model.BaseModelUpdateTask;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.search.StringMatcherUtility;
import java.util.ArrayList;
import java.util.List;
@@ -67,10 +68,10 @@
// apps that don't match all of the words in the query.
final String queryTextLower = query.toLowerCase();
final ArrayList<AppInfo> result = new ArrayList<>();
- DefaultAppSearchAlgorithm.StringMatcher matcher =
- DefaultAppSearchAlgorithm.StringMatcher.getInstance();
+ StringMatcherUtility.StringMatcher matcher =
+ StringMatcherUtility.StringMatcher.getInstance();
for (AppInfo info : apps) {
- if (DefaultAppSearchAlgorithm.matches(info, queryTextLower, matcher)) {
+ if (StringMatcherUtility.matches(queryTextLower, info.title.toString(), matcher)) {
result.add(info);
}
}
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index 4e213b0..a386ef8 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -20,12 +20,9 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
-import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.search.SearchAlgorithm;
import com.android.launcher3.search.SearchCallback;
-import java.text.Collator;
-
/**
* The default search implementation.
*/
@@ -54,132 +51,4 @@
() -> callback.onSearchResult(query, results)),
null);
}
-
- public static boolean matches(AppInfo info, String query, StringMatcher matcher) {
- int queryLength = query.length();
-
- String title = info.title.toString();
- int titleLength = title.length();
-
- if (titleLength < queryLength || queryLength <= 0) {
- return false;
- }
-
- if (requestSimpleFuzzySearch(query)) {
- return title.toLowerCase().contains(query);
- }
-
- int lastType;
- int thisType = Character.UNASSIGNED;
- int nextType = Character.getType(title.codePointAt(0));
-
- int end = titleLength - queryLength;
- for (int i = 0; i <= end; i++) {
- lastType = thisType;
- thisType = nextType;
- nextType = i < (titleLength - 1) ?
- Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED;
- if (isBreak(thisType, lastType, nextType) &&
- matcher.matches(query, title.substring(i, i + queryLength))) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Returns true if the current point should be a break point. Following cases
- * are considered as break points:
- * 1) Any non space character after a space character
- * 2) Any digit after a non-digit character
- * 3) Any capital character after a digit or small character
- * 4) Any capital character before a small character
- */
- private static boolean isBreak(int thisType, int prevType, int nextType) {
- switch (prevType) {
- case Character.UNASSIGNED:
- case Character.SPACE_SEPARATOR:
- case Character.LINE_SEPARATOR:
- case Character.PARAGRAPH_SEPARATOR:
- return true;
- }
- switch (thisType) {
- case Character.UPPERCASE_LETTER:
- if (nextType == Character.UPPERCASE_LETTER) {
- return true;
- }
- // Follow through
- case Character.TITLECASE_LETTER:
- // Break point if previous was not a upper case
- return prevType != Character.UPPERCASE_LETTER;
- case Character.LOWERCASE_LETTER:
- // Break point if previous was not a letter.
- return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
- case Character.DECIMAL_DIGIT_NUMBER:
- case Character.LETTER_NUMBER:
- case Character.OTHER_NUMBER:
- // Break point if previous was not a number
- return !(prevType == Character.DECIMAL_DIGIT_NUMBER
- || prevType == Character.LETTER_NUMBER
- || prevType == Character.OTHER_NUMBER);
- case Character.MATH_SYMBOL:
- case Character.CURRENCY_SYMBOL:
- case Character.OTHER_PUNCTUATION:
- case Character.DASH_PUNCTUATION:
- // Always a break point for a symbol
- return true;
- default:
- return false;
- }
- }
-
- public static class StringMatcher {
-
- private static final char MAX_UNICODE = '\uFFFF';
-
- private final Collator mCollator;
-
- StringMatcher() {
- // On android N and above, Collator uses ICU implementation which has a much better
- // support for non-latin locales.
- mCollator = Collator.getInstance();
- mCollator.setStrength(Collator.PRIMARY);
- mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
- }
-
- /**
- * Returns true if {@param query} is a prefix of {@param target}
- */
- public boolean matches(String query, String target) {
- switch (mCollator.compare(query, target)) {
- case 0:
- return true;
- case -1:
- // The target string can contain a modifier which would make it larger than
- // the query string (even though the length is same). If the query becomes
- // larger after appending a unicode character, it was originally a prefix of
- // the target string and hence should match.
- return mCollator.compare(query + MAX_UNICODE, target) > -1;
- default:
- return false;
- }
- }
-
- public static StringMatcher getInstance() {
- return new StringMatcher();
- }
- }
-
- private static boolean requestSimpleFuzzySearch(String s) {
- for (int i = 0; i < s.length(); ) {
- int codepoint = s.codePointAt(i);
- i += Character.charCount(codepoint);
- switch (Character.UnicodeScript.of(codepoint)) {
- case HAN:
- //Character.UnicodeScript.HAN: use String.contains to match
- return true;
- }
- }
- return false;
- }
}
diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
deleted file mode 100644
index adb882a..0000000
--- a/src/com/android/launcher3/allapps/search/LiveSearchManager.java
+++ /dev/null
@@ -1,325 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.allapps.search;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
-
-import android.app.Activity;
-import android.app.Application.ActivityLifecycleCallbacks;
-import android.appwidget.AppWidgetHost;
-import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-import androidx.lifecycle.Observer;
-import androidx.slice.Slice;
-import androidx.slice.SliceViewManager;
-import androidx.slice.SliceViewManager.SliceCallback;
-
-import com.android.launcher3.Alarm;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.function.Consumer;
-
-/**
- * Manages Lifecycle for Live search results
- */
-public class LiveSearchManager implements StateListener<LauncherState> {
-
- private static final String TAG = "LiveSearchManager";
-
- private static final long SLICE_TIMEOUT_MS = 50;
- public static final int SEARCH_APPWIDGET_HOST_ID = 2048;
-
- private final Launcher mLauncher;
- private final HashMap<Uri, SliceLifeCycle> mUriSliceMap = new HashMap<>();
-
- private final HashMap<ComponentKey, SearchWidgetInfoContainer> mWidgetPlaceholders =
- new HashMap<>();
- private SearchWidgetHost mSearchWidgetHost;
-
- public LiveSearchManager(Launcher launcher) {
- mLauncher = launcher;
- mLauncher.getStateManager().addStateListener(this);
- }
-
- /**
- * Creates new {@link AppWidgetHostView} from {@link AppWidgetProviderInfo}. Caches views for
- * quicker result within the same search session
- */
- public SearchWidgetInfoContainer getPlaceHolderWidget(AppWidgetProviderInfo providerInfo) {
- if (mSearchWidgetHost == null) {
- mSearchWidgetHost = new SearchWidgetHost(mLauncher);
- mSearchWidgetHost.startListening();
- }
-
- ComponentName provider = providerInfo.provider;
- UserHandle userHandle = providerInfo.getProfile();
-
- ComponentKey key = new ComponentKey(provider, userHandle);
- if (mWidgetPlaceholders.containsKey(key)) {
- return mWidgetPlaceholders.get(key);
- }
-
- LauncherAppWidgetProviderInfo pinfo = LauncherAppWidgetProviderInfo.fromProviderInfo(
- mLauncher, providerInfo);
- PendingAddWidgetInfo pendingAddWidgetInfo = new PendingAddWidgetInfo(pinfo);
-
- Bundle options = getDefaultOptionsForWidget(mLauncher, pendingAddWidgetInfo);
- int appWidgetId = mSearchWidgetHost.allocateAppWidgetId();
- boolean success = AppWidgetManager.getInstance(mLauncher)
- .bindAppWidgetIdIfAllowed(appWidgetId, userHandle, provider, options);
- if (!success) {
- mSearchWidgetHost.deleteAppWidgetId(appWidgetId);
- mWidgetPlaceholders.put(key, null);
- return null;
- }
-
- SearchWidgetInfoContainer view = (SearchWidgetInfoContainer) mSearchWidgetHost.createView(
- mLauncher, appWidgetId, providerInfo);
- view.setTag(pendingAddWidgetInfo);
- mWidgetPlaceholders.put(key, view);
- return view;
- }
-
- /**
- * Stop search session
- */
- public void stop() {
- clearWidgetHost();
- }
-
- private void clearWidgetHost() {
- if (mSearchWidgetHost != null) {
- mSearchWidgetHost.stopListening();
- mSearchWidgetHost.clearViews();
- mSearchWidgetHost.deleteHost();
- mWidgetPlaceholders.clear();
- mSearchWidgetHost = null;
- }
- }
-
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- if (finalState != ALL_APPS) {
- // Clear all search session related objects
- mUriSliceMap.values().forEach(SliceLifeCycle::destroy);
- mUriSliceMap.clear();
-
- clearWidgetHost();
- }
- }
-
- /**
- * Adds a new observer for the provided uri and returns a callback to cancel this observer
- */
- public SafeCloseable addObserver(Uri uri, Observer<Slice> listener,
- Consumer<Uri> timeoutConsumer) {
- SliceLifeCycle slc = mUriSliceMap.get(uri);
- if (slc == null) {
- slc = new SliceLifeCycle(uri, mLauncher);
- mUriSliceMap.put(uri, slc);
- }
- if (slc.mLastValue != null) {
- listener.onChanged(slc.mLastValue);
- }
-
- // Use a listener wrapper to handle error timeout.
- Observer<Slice> listenerWrapper = new Observer<Slice>() {
- final Alarm mErrorTimeout = new Alarm();
- {
- mErrorTimeout.setOnAlarmListener(alarm -> {
- alarm.cancelAlarm();
- timeoutConsumer.accept(uri);
- });
- mErrorTimeout.setAlarm(SLICE_TIMEOUT_MS);
- }
-
- @Override
- public void onChanged(Slice slice) {
- if (slice == null) {
- return;
- }
-
- if (mErrorTimeout.alarmPending()) {
- mErrorTimeout.cancelAlarm();
- }
-
- if (mUriSliceMap.get(uri) != null) {
- mUriSliceMap.get(uri).mLastValue = slice;
- }
-
- listener.onChanged(slice);
- }
- };
-
- slc.addListener(listenerWrapper);
-
- final SliceLifeCycle sliceLifeCycle = slc;
- return () -> sliceLifeCycle.removeListener(listenerWrapper);
- }
-
- static class SearchWidgetHost extends AppWidgetHost {
- SearchWidgetHost(Context context) {
- super(context, SEARCH_APPWIDGET_HOST_ID);
- }
-
- @Override
- protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
- AppWidgetProviderInfo appWidget) {
- return new SearchWidgetInfoContainer(context);
- }
-
- @Override
- public void clearViews() {
- super.clearViews();
- }
- }
-
- private static class SliceLifeCycle
- implements ActivityLifecycleCallbacks, SliceCallback {
-
- private final Uri mUri;
- private final Launcher mLauncher;
- private final SliceViewManager mSliceViewManager;
- private final ArrayList<Observer<Slice>> mListeners = new ArrayList<>();
-
- private boolean mDestroyed = false;
- private boolean mWasListening = false;
-
- Slice mLastValue;
-
- SliceLifeCycle(Uri uri, Launcher launcher) {
- mUri = uri;
- mLauncher = launcher;
- mSliceViewManager = SliceViewManager.getInstance(launcher);
- launcher.registerActivityLifecycleCallbacks(this);
-
- if (launcher.isDestroyed()) {
- onActivityDestroyed(launcher);
- } else if (launcher.isStarted()) {
- onActivityStarted(launcher);
- }
- }
-
- @Override
- public void onActivityDestroyed(Activity activity) {
- destroy();
- }
-
- @Override
- public void onActivityStarted(Activity activity) {
- updateListening();
- }
-
- @Override
- public void onActivityStopped(Activity activity) {
- updateListening();
- }
-
- private void updateListening() {
- boolean isListening = mDestroyed
- ? false
- : (mLauncher.isStarted() && !mListeners.isEmpty());
- UI_HELPER_EXECUTOR.execute(() -> uploadListeningBg(isListening));
- }
-
- @WorkerThread
- private void uploadListeningBg(boolean isListening) {
- if (mWasListening != isListening) {
- mWasListening = isListening;
- if (isListening) {
- mSliceViewManager.registerSliceCallback(mUri, MAIN_EXECUTOR, this);
- // Update slice one-time on the different thread so that we can display
- // multiple slices in parallel
- THREAD_POOL_EXECUTOR.execute(this::updateSlice);
- } else {
- mSliceViewManager.unregisterSliceCallback(mUri, this);
- }
- }
- }
-
- @UiThread
- private void addListener(Observer<Slice> listener) {
- mListeners.add(listener);
- updateListening();
- }
-
- @UiThread
- private void removeListener(Observer<Slice> listener) {
- mListeners.remove(listener);
- updateListening();
- }
-
- @WorkerThread
- private void updateSlice() {
- try {
- Slice s = mSliceViewManager.bindSlice(mUri);
- MAIN_EXECUTOR.execute(() -> onSliceUpdated(s));
- } catch (Exception e) {
- Log.d(TAG, "Error fetching slice", e);
- }
- }
-
- @UiThread
- @Override
- public void onSliceUpdated(@Nullable Slice s) {
- mListeners.forEach(l -> l.onChanged(s));
- }
-
- private void destroy() {
- if (mDestroyed) {
- return;
- }
- mDestroyed = true;
- mLauncher.unregisterActivityLifecycleCallbacks(this);
- mListeners.clear();
- }
-
- @Override
- public void onActivityCreated(Activity activity, Bundle bundle) { }
-
- @Override
- public void onActivityPaused(Activity activity) { }
-
- @Override
- public void onActivityResumed(Activity activity) { }
-
- @Override
- public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
- }
-}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index e406e9b..48e41d5 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -198,13 +198,16 @@
"ENABLE_APP_PREDICTIONS_WHILE_VISIBLE", true, "Allows app "
+ "predictions to be updated while they are visible to the user.");
- public static final BooleanFlag ENABLE_TASKBAR = new DeviceFlag(
+ public static final BooleanFlag ENABLE_TASKBAR = getDebugFlag(
"ENABLE_TASKBAR", false, "Allows a system Taskbar to be shown on larger devices.");
- public static final BooleanFlag ENABLE_OVERVIEW_GRID = new DeviceFlag(
+ public static final BooleanFlag ENABLE_OVERVIEW_GRID = getDebugFlag(
"ENABLE_OVERVIEW_GRID", false, "Uses grid overview layout. "
+ "Only applicable on large screen devices.");
+ public static final BooleanFlag ENABLE_SPLIT_SELECT = getDebugFlag(
+ "ENABLE_SPLIT_SELECT", false, "Uses new split screen selection overview UI");
+
public static void initialize(Context context) {
synchronized (sDebugFlags) {
for (DebugFlag flag : sDebugFlags) {
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 61f2c2a..988794c 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -131,6 +131,7 @@
/**
* Fetches high-res icon for the provided ItemInfo and updates the caller when done.
+ *
* @return a request ID that can be used to cancel the request.
*/
public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller,
@@ -139,7 +140,7 @@
if (mPendingIconRequestCount <= 0) {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
}
- mPendingIconRequestCount ++;
+ mPendingIconRequestCount++;
HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler,
() -> {
@@ -158,7 +159,7 @@
}
private void onIconRequestEnd() {
- mPendingIconRequestCount --;
+ mPendingIconRequestCount--;
if (mPendingIconRequestCount <= 0) {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
@@ -289,7 +290,8 @@
@NonNull Supplier<LauncherActivityInfo> activityInfoProvider,
boolean usePkgIcon, boolean useLowResIcon) {
CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user,
- activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon, useLowResIcon);
+ activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon,
+ useLowResIcon);
applyCacheEntry(entry, infoInOut);
}
@@ -315,7 +317,8 @@
}
public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) {
- cachePackageInstallInfo(key.mPackageName, key.mUser, info.getAppIcon(), info.getAppLabel());
+ cachePackageInstallInfo(key.mPackageName, key.mUser, info.getAppIcon(),
+ info.getAppLabel());
}
@Override
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
index 6189dc9..a7cd10d 100644
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
@@ -269,7 +269,7 @@
lp.leftMargin = lp.rightMargin = 0;
lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
lp.bottomMargin = grid.isTaskbarPresent
- ? grid.workspacePadding.bottom + insets.bottom
+ ? grid.workspacePadding.bottom + grid.taskbarSize
: grid.hotseatBarSizePx + insets.bottom;
}
setLayoutParams(lp);
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 5a34d2a..15915e5 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -164,7 +164,9 @@
reverseOrder(viewsToFlip);
}
onInflationComplete(reverseOrder);
- addArrow();
+ if (shouldAddArrow()) {
+ addArrow();
+ }
animateOpen();
}
@@ -174,7 +176,9 @@
protected void show() {
setupForDisplay();
onInflationComplete(false);
- addArrow();
+ if (shouldAddArrow()) {
+ addArrow();
+ }
animateOpen();
}
@@ -233,6 +237,13 @@
}
/**
+ * Returns whether or not we should add the arrow.
+ */
+ protected boolean shouldAddArrow() {
+ return true;
+ }
+
+ /**
* Provide the location of the target object relative to the dragLayer.
*/
protected abstract void getTargetObjectLocation(Rect outPos);
@@ -392,13 +403,18 @@
return getChildCount() > 0 ? getChildAt(0) : this;
}
+ private int getArrowDuration() {
+ return shouldAddArrow()
+ ? getResources().getInteger(R.integer.config_popupArrowOpenCloseDuration)
+ : 0;
+ }
private void animateOpen() {
setVisibility(View.VISIBLE);
final AnimatorSet openAnim = new AnimatorSet();
final Resources res = getResources();
final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
- final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration);
+ final long arrowDuration = getArrowDuration();
final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
// Rectangular reveal.
@@ -460,7 +476,7 @@
final Resources res = getResources();
final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
final long revealDuration = res.getInteger(R.integer.config_popupOpenCloseDuration);
- final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration);
+ final long arrowDuration = getArrowDuration();
// Hide the arrow
Animator scaleArrow = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0)
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 577fe4a..e5424cf 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -45,6 +45,11 @@
protected final T mTarget;
protected final ItemInfo mItemInfo;
+ /**
+ * Indicates if it's invokable or not through some disabled UI
+ */
+ private boolean isEnabled = true;
+
public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) {
mIconResId = iconResId;
mLabelResId = labelResId;
@@ -83,6 +88,14 @@
mAccessibilityActionId, context.getText(mLabelResId));
}
+ public void setEnabled(boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
public boolean hasHandlerForAction(int action) {
return mAccessibilityActionId == action;
}
diff --git a/src/com/android/launcher3/search/SearchAlgorithm.java b/src/com/android/launcher3/search/SearchAlgorithm.java
index 1665354..a1720c7 100644
--- a/src/com/android/launcher3/search/SearchAlgorithm.java
+++ b/src/com/android/launcher3/search/SearchAlgorithm.java
@@ -31,4 +31,9 @@
* Cancels any active request.
*/
void cancel(boolean interruptActiveRequests);
+
+ /**
+ * Cleans up after search is no longer needed.
+ */
+ default void destroy() {};
}
diff --git a/src/com/android/launcher3/search/StringMatcherUtility.java b/src/com/android/launcher3/search/StringMatcherUtility.java
new file mode 100644
index 0000000..acab52b
--- /dev/null
+++ b/src/com/android/launcher3/search/StringMatcherUtility.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.search;
+
+import java.text.Collator;
+
+/**
+ * Utilities for matching query string to target string.
+ */
+public class StringMatcherUtility {
+
+ /**
+ * Returns {@code true} is {@code query} is a prefix substring of a complete word/phrase in
+ * {@code target}.
+ */
+ public static boolean matches(String query, String target, StringMatcher matcher) {
+ int queryLength = query.length();
+
+ int targetLength = target.length();
+
+ if (targetLength < queryLength || queryLength <= 0) {
+ return false;
+ }
+
+ if (requestSimpleFuzzySearch(query)) {
+ return target.toLowerCase().contains(query);
+ }
+
+ int lastType;
+ int thisType = Character.UNASSIGNED;
+ int nextType = Character.getType(target.codePointAt(0));
+
+ int end = targetLength - queryLength;
+ for (int i = 0; i <= end; i++) {
+ lastType = thisType;
+ thisType = nextType;
+ nextType = i < (targetLength - 1)
+ ? Character.getType(target.codePointAt(i + 1)) : Character.UNASSIGNED;
+ if (isBreak(thisType, lastType, nextType)
+ && matcher.matches(query, target.substring(i, i + queryLength))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the current point should be a break point. Following cases
+ * are considered as break points:
+ * 1) Any non space character after a space character
+ * 2) Any digit after a non-digit character
+ * 3) Any capital character after a digit or small character
+ * 4) Any capital character before a small character
+ */
+ private static boolean isBreak(int thisType, int prevType, int nextType) {
+ switch (prevType) {
+ case Character.UNASSIGNED:
+ case Character.SPACE_SEPARATOR:
+ case Character.LINE_SEPARATOR:
+ case Character.PARAGRAPH_SEPARATOR:
+ return true;
+ }
+ switch (thisType) {
+ case Character.UPPERCASE_LETTER:
+ if (nextType == Character.UPPERCASE_LETTER) {
+ return true;
+ }
+ // Follow through
+ case Character.TITLECASE_LETTER:
+ // Break point if previous was not a upper case
+ return prevType != Character.UPPERCASE_LETTER;
+ case Character.LOWERCASE_LETTER:
+ // Break point if previous was not a letter.
+ return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
+ case Character.DECIMAL_DIGIT_NUMBER:
+ case Character.LETTER_NUMBER:
+ case Character.OTHER_NUMBER:
+ // Break point if previous was not a number
+ return !(prevType == Character.DECIMAL_DIGIT_NUMBER
+ || prevType == Character.LETTER_NUMBER
+ || prevType == Character.OTHER_NUMBER);
+ case Character.MATH_SYMBOL:
+ case Character.CURRENCY_SYMBOL:
+ case Character.OTHER_PUNCTUATION:
+ case Character.DASH_PUNCTUATION:
+ // Always a break point for a symbol
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Performs locale sensitive string comparison using {@link Collator}.
+ */
+ public static class StringMatcher {
+
+ private static final char MAX_UNICODE = '\uFFFF';
+
+ private final Collator mCollator;
+
+ StringMatcher() {
+ // On android N and above, Collator uses ICU implementation which has a much better
+ // support for non-latin locales.
+ mCollator = Collator.getInstance();
+ mCollator.setStrength(Collator.PRIMARY);
+ mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
+ }
+
+ /**
+ * Returns true if {@param query} is a prefix of {@param target}
+ */
+ public boolean matches(String query, String target) {
+ switch (mCollator.compare(query, target)) {
+ case 0:
+ return true;
+ case -1:
+ // The target string can contain a modifier which would make it larger than
+ // the query string (even though the length is same). If the query becomes
+ // larger after appending a unicode character, it was originally a prefix of
+ // the target string and hence should match.
+ return mCollator.compare(query + MAX_UNICODE, target) > -1;
+ default:
+ return false;
+ }
+ }
+
+ public static StringMatcher getInstance() {
+ return new StringMatcher();
+ }
+ }
+
+ /**
+ * Matching optimization to search in Chinese.
+ */
+ private static boolean requestSimpleFuzzySearch(String s) {
+ for (int i = 0; i < s.length(); ) {
+ int codepoint = s.codePointAt(i);
+ i += Character.charCount(codepoint);
+ switch (Character.UnicodeScript.of(codepoint)) {
+ case HAN:
+ //Character.UnicodeScript.HAN: use String.contains to match
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 2b51e97..51767e7 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -77,6 +77,15 @@
return mCurrentStableState;
}
+ @Override
+ public String toString() {
+ return " StateManager(mLastStableState:" + mLastStableState
+ + ", mCurrentStableState:" + mCurrentStableState
+ + ", mState:" + mState
+ + ", mRestState:" + mRestState
+ + ", isInTransition:" + (mConfig.currentAnimation != null) + ")";
+ }
+
public void dump(String prefix, PrintWriter writer) {
writer.println(prefix + "StateManager:");
writer.println(prefix + "\tmLastStableState:" + mLastStableState);
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 7cb6e34..f34bff6 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -32,6 +32,7 @@
public static final int ALL_APPS_STATE_ORDINAL = 5;
public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
public static final int HINT_STATE_ORDINAL = 7;
+ public static final int OVERVIEW_SPLIT_SELECT_ORDINAL = 8;
public static final String TAPL_EVENTS_TAG = "TaplEvents";
public static final String SEQUENCE_MAIN = "Main";
public static final String SEQUENCE_TIS = "TIS";
@@ -55,6 +56,8 @@
return "Background";
case HINT_STATE_ORDINAL:
return "Hint";
+ case OVERVIEW_SPLIT_SELECT_ORDINAL:
+ return "OverviewSplitSelect";
default:
return "Unknown";
}
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 8a64f3d..c1cf0c8 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -21,6 +21,10 @@
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_SIDE;
import android.content.res.Resources;
import android.graphics.PointF;
@@ -36,8 +40,13 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+
+import java.util.ArrayList;
+import java.util.List;
public class LandscapePagedViewHandler implements PagedOrientationHandler {
@@ -212,6 +221,20 @@
}
@Override
+ public int getSplitTranslationDirectionFactor(int stagePosition) {
+ if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ return translationOffset;
+ }
+
+ @Override
public float getTaskMenuX(float x, View thumbnailView) {
return thumbnailView.getMeasuredWidth() + x;
}
@@ -282,4 +305,23 @@
public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
return rect.left;
}
+
+ @Override
+ public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
+ List<SplitPositionOption> options = new ArrayList<>(2);
+ // Add left/right options where left => position top, right => position bottom
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_left,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_right,
+ STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_SIDE));
+ return options;
+ }
+
+ @Override
+ public FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
+ DeviceProfile deviceProfile) {
+ return primary;
+ }
}
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index e1cec87..fcfa205 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -32,6 +32,10 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.PagedView;
import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+
+import java.util.List;
/**
* Abstraction layer to separate horizontal and vertical specific implementations
@@ -75,6 +79,8 @@
int getScrollOffsetEnd(View view, Rect insets);
int getPrimaryTranslationDirectionFactor();
int getSecondaryTranslationDirectionFactor();
+ int getSplitTranslationDirectionFactor(@StagePosition int stagePosition);
+ int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp);
ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
void setMaxScroll(AccessibilityEvent event, int maxScroll);
boolean getRecentsRtlSetting(Resources resources);
@@ -95,6 +101,9 @@
int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate, LinearLayout taskMenuLayout);
void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
+ List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
+ FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
+ DeviceProfile deviceProfile);
// The following are only used by TaskViewTouchHandler.
/** @return Either VERTICAL or HORIZONTAL. */
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index bcaf5f4..2bc2dc7 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -19,6 +19,10 @@
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_SIDE;
import android.content.res.Resources;
import android.graphics.PointF;
@@ -34,8 +38,13 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+
+import java.util.ArrayList;
+import java.util.List;
public class PortraitPagedViewHandler implements PagedOrientationHandler {
@@ -208,6 +217,23 @@
}
@Override
+ public int getSplitTranslationDirectionFactor(int stagePosition) {
+ if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ if (dp.isLandscape) {
+ return translationOffset;
+ }
+ return 0;
+ }
+
+ @Override
public float getTaskMenuX(float x, View thumbnailView) {
return x;
}
@@ -277,4 +303,35 @@
public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
return dp.heightPx - rect.bottom;
}
+
+ @Override
+ public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
+ List<SplitPositionOption> options = new ArrayList<>(2);
+ // TODO: Add in correct icons
+ if (dp.isLandscape) { // or seascape
+ // Add left/right options
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_left,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_right,
+ STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_SIDE));
+ } else {
+ // Only add top option
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_top,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ }
+ return options;
+ }
+
+ @Override
+ public FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
+ DeviceProfile dp) {
+ if (dp.isLandscape) { // or seascape
+ return primary;
+ } else {
+ return secondary;
+ }
+ }
}
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index 54af029..b5252f7 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -17,6 +17,10 @@
package com.android.launcher3.touch;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_SIDE;
import android.content.res.Resources;
import android.graphics.PointF;
@@ -25,7 +29,12 @@
import android.view.View;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+
+import java.util.ArrayList;
+import java.util.List;
public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
@@ -35,6 +44,20 @@
}
@Override
+ public int getSplitTranslationDirectionFactor(int stagePosition) {
+ if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ return translationOffset;
+ }
+
+ @Override
public boolean getRecentsRtlSetting(Resources resources) {
return Utilities.isRtl(resources);
}
@@ -71,6 +94,19 @@
return dp.widthPx - rect.right;
}
+ @Override
+ public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
+ List<SplitPositionOption> options = new ArrayList<>(2);
+ // Add left/right options where left => position bottom, right => position top
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_left,
+ STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_SIDE));
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_right,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ return options;
+ }
+
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
@Override
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index 5be9529..c79b1f6 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -42,16 +42,49 @@
}
};
+ /**
+ * Determines how each alpha should factor into the final alpha.
+ */
+ public enum Mode {
+ BLEND(1f) {
+ @Override
+ public float calculateNewAlpha(float currentAlpha, float otherAlpha) {
+ return currentAlpha * otherAlpha;
+ }
+ },
+
+ MAX(0f) {
+ @Override
+ public float calculateNewAlpha(float currentAlpha, float otherAlpha) {
+ return Math.max(currentAlpha, otherAlpha);
+ }
+ };
+
+ Mode(float startAlpha) {
+ mStartAlpha = startAlpha;
+ }
+
+ protected final float mStartAlpha;
+ protected abstract float calculateNewAlpha(float currentAlpha, float otherAlpha);
+ }
+
private final View mView;
private final AlphaProperty[] mMyProperties;
+ private final Mode mMode;
private int mValidMask;
// Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
private boolean mUpdateVisibility;
public MultiValueAlpha(View view, int size) {
+ this(view, size, Mode.BLEND);
+ }
+
+ public MultiValueAlpha(View view, int size, Mode mode) {
mView = view;
mMyProperties = new AlphaProperty[size];
+ mMode = mode;
+ mView.setAlpha(mMode.mStartAlpha);
mValidMask = 0;
for (int i = 0; i < size; i++) {
@@ -79,9 +112,9 @@
private final int mMyMask;
- private float mValue = 1;
+ private float mValue = mMode.mStartAlpha;
// Factor of all other alpha channels, only valid if mMyMask is present in mValidMask.
- private float mOthers = 1;
+ private float mOthers = mMode.mStartAlpha;
AlphaProperty(int myMask) {
mMyMask = myMask;
@@ -94,10 +127,10 @@
if ((mValidMask & mMyMask) == 0) {
// Our cache value is not correct, recompute it.
- mOthers = 1;
+ mOthers = mMode.mStartAlpha;
for (AlphaProperty prop : mMyProperties) {
if (prop != this) {
- mOthers *= prop.mValue;
+ mOthers = mMode.calculateNewAlpha(mOthers, prop.mValue);
}
}
}
@@ -107,7 +140,7 @@
mValidMask = mMyMask;
mValue = value;
- mView.setAlpha(mOthers * mValue);
+ mView.setAlpha(mMode.calculateNewAlpha(mOthers, mValue));
if (mUpdateVisibility) {
AlphaUpdateListener.updateVisibility(mView);
}
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
new file mode 100644
index 0000000..573c8bd
--- /dev/null
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+
+public final class SplitConfigurationOptions {
+
+ ///////////////////////////////////
+ // Taken from
+ // frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+ /**
+ * Stage position isn't specified normally meaning to use what ever it is currently set to.
+ */
+ public static final int STAGE_POSITION_UNDEFINED = -1;
+ /**
+ * Specifies that a stage is positioned at the top half of the screen if
+ * in portrait mode or at the left half of the screen if in landscape mode.
+ */
+ public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
+
+ /**
+ * Specifies that a stage is positioned at the bottom half of the screen if
+ * in portrait mode or at the right half of the screen if in landscape mode.
+ */
+ public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
+
+ @Retention(SOURCE)
+ @IntDef({STAGE_POSITION_UNDEFINED, STAGE_POSITION_TOP_OR_LEFT, STAGE_POSITION_BOTTOM_OR_RIGHT})
+ public @interface StagePosition {}
+
+ /**
+ * Stage type isn't specified normally meaning to use what ever the default is.
+ * E.g. exit split-screen and launch the app in fullscreen.
+ */
+ public static final int STAGE_TYPE_UNDEFINED = -1;
+ /**
+ * The main stage type.
+ */
+ public static final int STAGE_TYPE_MAIN = 0;
+
+ /**
+ * The side stage type.
+ */
+ public static final int STAGE_TYPE_SIDE = 1;
+
+ @IntDef({STAGE_TYPE_UNDEFINED, STAGE_TYPE_MAIN, STAGE_TYPE_SIDE})
+ public @interface StageType {}
+ ///////////////////////////////////
+
+ public static class SplitPositionOption {
+ public final int mIconResId;
+ public final int mTextResId;
+ @StagePosition
+ public final int mStagePosition;
+
+ @StageType
+ public final int mStageType;
+
+ public SplitPositionOption(int iconResId, int textResId, int stagePosition, int stageType) {
+ mIconResId = iconResId;
+ mTextResId = textResId;
+ mStagePosition = stagePosition;
+ mStageType = stageType;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index 5b33f18..e413d7f 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -58,7 +58,7 @@
Preconditions.assertUIThread();
Handler handler = new Handler();
- // LayoutInflater is not thread save as it maintains a global variable 'mConstructorArgs'.
+ // LayoutInflater is not thread safe as it maintains a global variable 'mConstructorArgs'.
// Create a different copy to use on the background thread.
LayoutInflater inflater = mInflater.cloneInContext(mInflater.getContext());
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 899dcf7..1a114f3 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -114,6 +114,11 @@
}
@Override
+ protected boolean shouldAddArrow() {
+ return false;
+ }
+
+ @Override
protected void getTargetObjectLocation(Rect outPos) {
mTargetRect.roundOut(outPos);
}
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index c9bd284..7f0765b 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -32,7 +32,6 @@
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.uioverrides.WallpaperColorInfo;
import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
import com.android.launcher3.util.Themes;
@@ -42,7 +41,6 @@
*/
public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener {
- private static final float SCRIM_ALPHA = .95f;
protected final T mLauncher;
private final WallpaperColorInfo mWallpaperColorInfo;
protected final int mEndScrim;
@@ -61,12 +59,7 @@
super(context, attrs);
mLauncher = Launcher.cast(Launcher.getLauncher(context));
mWallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context);
- int endScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
- if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
- endScrim = Themes.getColorBackgroundFloating(context);
- endScrim = ColorUtils.setAlphaComponent(endScrim, (int) (255 * SCRIM_ALPHA));
- }
- mEndScrim = endScrim;
+ mEndScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
mIsScrimDark = ColorUtils.calculateLuminance(mEndScrim) < 0.5f;
mMaxScrimAlpha = 0.7f;
diff --git a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
index 1ac5a33..9313266 100644
--- a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
+++ b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
@@ -15,6 +15,9 @@
*/
package com.android.launcher3.widget;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
+
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.os.Parcel;
@@ -78,8 +81,22 @@
return true;
}
+ /**
+ * Checks whether the widget needs configuration.
+ *
+ * A widget needs configuration if (1) it has a configuration activity and (2)
+ * it's configuration is not optional.
+ *
+ * @return true if the widget needs configuration, false otherwise.
+ */
public boolean needsConfigure() {
- return mProviderInfo.configure != null;
+ int featureFlags = mProviderInfo.widgetFeatures;
+ // A widget's configuration is optional only if it's configuration is marked as optional AND
+ // it can be reconfigured later.
+ boolean configurationOptional = (featureFlags & WIDGET_FEATURE_CONFIGURATION_OPTIONAL) != 0
+ && (featureFlags & WIDGET_FEATURE_RECONFIGURABLE) != 0;
+
+ return mProviderInfo.configure != null && !configurationOptional;
}
public LauncherAppWidgetProviderInfo getProviderInfo(Context context) {
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
index 09517e1..73bae6f 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -20,10 +20,14 @@
import androidx.annotation.IntDef;
+import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.WidgetItemComparator;
import java.lang.annotation.Retention;
+import java.util.List;
+import java.util.stream.Collectors;
/** Holder class to store the package information of an entry shown in the widgets list. */
public abstract class WidgetsListBaseEntry {
@@ -35,9 +39,14 @@
*/
public final String mTitleSectionName;
- public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName) {
+ public final List<WidgetItem> mWidgets;
+
+ public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
mPkgItem = pkgItem;
mTitleSectionName = titleSectionName;
+ this.mWidgets =
+ items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
}
/**
@@ -51,10 +60,11 @@
public abstract int getRank();
@Retention(SOURCE)
- @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_CONTENT})
+ @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_SEARCH_HEADER, RANK_WIDGETS_LIST_CONTENT})
public @interface Rank {
}
public static final int RANK_WIDGETS_LIST_HEADER = 1;
- public static final int RANK_WIDGETS_LIST_CONTENT = 2;
+ public static final int RANK_WIDGETS_LIST_SEARCH_HEADER = 2;
+ public static final int RANK_WIDGETS_LIST_CONTENT = 3;
}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
index b0cb8c7..0328cf6 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
@@ -17,10 +17,8 @@
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.widget.WidgetItemComparator;
import java.util.List;
-import java.util.stream.Collectors;
/**
* Holder class to store all the information related to a list of widgets from the same app which is
@@ -28,18 +26,14 @@
*/
public final class WidgetsListContentEntry extends WidgetsListBaseEntry {
- public final List<WidgetItem> mWidgets;
-
public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items) {
- super(pkgItem, titleSectionName);
- this.mWidgets =
- items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
+ super(pkgItem, titleSectionName, items);
}
@Override
public String toString() {
- return mPkgItem.packageName + ":" + mWidgets.size();
+ return "Content:" + mPkgItem.packageName + ":" + mWidgets.size();
}
@Override
@@ -47,4 +41,12 @@
public int getRank() {
return RANK_WIDGETS_LIST_CONTENT;
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof WidgetsListContentEntry)) return false;
+ WidgetsListContentEntry otherEntry = (WidgetsListContentEntry) obj;
+ return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+ }
}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
index 6899647..1fdc399 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -18,7 +18,7 @@
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
-import java.util.Collection;
+import java.util.List;
/** An information holder for an app which has widgets or/and shortcuts. */
public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
@@ -30,8 +30,8 @@
private boolean mHasEntryUpdated = false;
public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
- Collection<WidgetItem> items) {
- super(pkgItem, titleSectionName);
+ List<WidgetItem> items) {
+ super(pkgItem, titleSectionName, items);
widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
shortcutsCount = Math.max(0, items.size() - widgetsCount);
}
@@ -57,8 +57,21 @@
}
@Override
+ public String toString() {
+ return "Header:" + mPkgItem.packageName + ":" + mWidgets.size();
+ }
+
+ @Override
@Rank
public int getRank() {
return RANK_WIDGETS_LIST_HEADER;
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof WidgetsListHeaderEntry)) return false;
+ WidgetsListHeaderEntry otherEntry = (WidgetsListHeaderEntry) obj;
+ return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+ }
}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
new file mode 100644
index 0000000..2aec3f8
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.List;
+
+/** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */
+public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry {
+
+ private boolean mIsWidgetListShown = false;
+ private boolean mHasEntryUpdated = false;
+
+ public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ super(pkgItem, titleSectionName, items);
+ }
+
+ /** Sets if the widgets list associated with this header is shown. */
+ public void setIsWidgetListShown(boolean isWidgetListShown) {
+ if (mIsWidgetListShown != isWidgetListShown) {
+ this.mIsWidgetListShown = isWidgetListShown;
+ mHasEntryUpdated = true;
+ } else {
+ mHasEntryUpdated = false;
+ }
+ }
+
+ /** Returns {@code true} if the widgets list associated with this header is shown. */
+ public boolean isWidgetListShown() {
+ return mIsWidgetListShown;
+ }
+
+ /** Returns {@code true} if this entry has been updated due to user interactions. */
+ public boolean hasEntryUpdated() {
+ return mHasEntryUpdated;
+ }
+
+ @Override
+ public String toString() {
+ return "SearchHeader:" + mPkgItem.packageName + ":" + mWidgets.size();
+ }
+
+ @Override
+ @Rank
+ public int getRank() {
+ return RANK_WIDGETS_LIST_SEARCH_HEADER;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof WidgetsListSearchHeaderEntry)) return false;
+ WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj;
+ return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java
new file mode 100644
index 0000000..7372751
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import com.android.launcher3.util.PackageUserKey;
+
+/**
+ * A listener to be invoked when a header is clicked.
+ */
+public interface OnHeaderClickListener {
+ /**
+ * Calls when a header is clicked to show / hide widgets for a package.
+ */
+ void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey);
+}
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
index 95fa05f..7eb5b83 100644
--- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -34,6 +34,7 @@
private final boolean mHasWorkProfile;
private final SearchAndRecommendationViewHolder mViewHolder;
private final WidgetsRecyclerView mPrimaryRecyclerView;
+ private final WidgetsRecyclerView mSearchRecyclerView;
// The following are only non null if mHasWorkProfile is true.
@Nullable private final WidgetsRecyclerView mWorkRecyclerView;
@@ -48,12 +49,14 @@
SearchAndRecommendationViewHolder viewHolder,
WidgetsRecyclerView primaryRecyclerView,
@Nullable WidgetsRecyclerView workRecyclerView,
+ WidgetsRecyclerView searchRecyclerView,
@Nullable View personalWorkTabsView,
@Nullable PersonalWorkPagedView primaryWorkViewPager) {
mHasWorkProfile = hasWorkProfile;
mViewHolder = viewHolder;
mPrimaryRecyclerView = primaryRecyclerView;
mWorkRecyclerView = workRecyclerView;
+ mSearchRecyclerView = searchRecyclerView;
mPrimaryWorkTabsView = personalWorkTabsView;
mPrimaryWorkViewPager = primaryWorkViewPager;
mCurrentRecyclerView = mPrimaryRecyclerView;
@@ -149,6 +152,11 @@
mPrimaryRecyclerView.getPaddingRight(),
mPrimaryRecyclerView.getPaddingBottom());
}
+ mSearchRecyclerView.setPadding(
+ mSearchRecyclerView.getPaddingLeft(),
+ topContainerHeight,
+ mSearchRecyclerView.getPaddingRight(),
+ mSearchRecyclerView.getPaddingBottom());
}
/**
diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
index dbd1bdf..2366609 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -25,6 +25,7 @@
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
import java.util.ArrayList;
@@ -113,7 +114,7 @@
// or did the header view changed due to user interactions?
// or did the widget size and desc, span, etc change?
if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem)
- || hasHeaderUpdated(newRowEntry)
+ || hasHeaderUpdated(orgRowEntry, newRowEntry)
|| hasWidgetsListChanged(orgRowEntry, newRowEntry)) {
index = currentEntries.indexOf(orgRowEntry);
currentEntries.set(index, newRowEntry);
@@ -174,12 +175,16 @@
* Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has
* been changed due to user interactions.
*/
- private boolean hasHeaderUpdated(WidgetsListBaseEntry newRow) {
- if (!(newRow instanceof WidgetsListHeaderEntry)) {
- return false;
+ private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) {
+ if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) {
+ return ((WidgetsListHeaderEntry) newRow).hasEntryUpdated() || !curRow.equals(newRow);
}
- WidgetsListHeaderEntry newRowEntry = (WidgetsListHeaderEntry) newRow;
- return newRowEntry.hasEntryUpdated();
+ if (newRow instanceof WidgetsListSearchHeaderEntry
+ && curRow instanceof WidgetsListSearchHeaderEntry) {
+ return ((WidgetsListSearchHeaderEntry) newRow).hasEntryUpdated()
+ || !curRow.equals(newRow);
+ }
+ return false;
}
private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 330175f..6b3c71a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -34,7 +34,6 @@
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
-import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -53,6 +52,8 @@
import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.search.SearchModeListener;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
import com.android.launcher3.workprofile.PersonalWorkPagedView;
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
@@ -64,7 +65,7 @@
*/
public class WidgetsFullSheet extends BaseWidgetSheet
implements Insettable, ProviderChangedListener, OnActivePageChangedListener,
- WidgetsRecyclerView.HeaderViewDimensionsProvider {
+ WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
private static final long DEFAULT_OPEN_DURATION = 267;
private static final long FADE_IN_DURATION = 150;
@@ -81,6 +82,7 @@
@Nullable private PersonalWorkPagedView mViewPager;
private int mInitialTabsHeight = 0;
+ private boolean mIsInSearchMode;
private View mTabsView;
private TextView mNoWidgetsView;
private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
@@ -91,6 +93,7 @@
mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
+ mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
}
public WidgetsFullSheet(Context context, AttributeSet attrs) {
@@ -138,6 +141,7 @@
mSearchAndRecommendationViewHolder,
findViewById(R.id.primary_widgets_list_view),
mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null,
+ findViewById(R.id.search_widgets_list_view),
mTabsView,
mViewPager);
fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController);
@@ -145,17 +149,25 @@
mNoWidgetsView = findViewById(R.id.no_widgets_text);
onWidgetsBound();
+
+ mSearchAndRecommendationViewHolder.mSearchBar.initialize(
+ mLauncher.getPopupDataProvider().getAllWidgets(), /* searchModeListener= */ this);
}
@Override
public void onActivePageChanged(int currentActivePage) {
AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
- WidgetsRecyclerView currentRecyclerView = currentAdapterHolder.mWidgetsRecyclerView;
- currentRecyclerView.bindFastScrollbar();
- mSearchAndRecommendationsScrollController.setCurrentRecyclerView(currentRecyclerView);
+ WidgetsRecyclerView currentRecyclerView =
+ mAdapters.get(currentActivePage).mWidgetsRecyclerView;
updateNoWidgetsView(currentAdapterHolder);
+ attachScrollbarToRecyclerView(currentRecyclerView);
+ }
+
+ private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
+ recyclerView.bindFastScrollbar();
+ mSearchAndRecommendationsScrollController.setCurrentRecyclerView(recyclerView);
reset();
}
@@ -173,11 +185,15 @@
if (mHasWorkProfile) {
mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
}
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
mSearchAndRecommendationsScrollController.reset();
}
@VisibleForTesting
public WidgetsRecyclerView getRecyclerView() {
+ if (mIsInSearchMode) {
+ return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
+ }
if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
}
@@ -289,6 +305,8 @@
AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
primaryUserAdapterHolder.setup(findViewById(R.id.primary_widgets_list_view));
+ AdapterHolder searchAdapterHolder = mAdapters.get(AdapterHolder.SEARCH);
+ searchAdapterHolder.setup(findViewById(R.id.search_widgets_list_view));
primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
updateNoWidgetsView(primaryUserAdapterHolder);
@@ -300,6 +318,40 @@
}
}
+ @Override
+ public void enterSearchMode() {
+ if (mIsInSearchMode) return;
+ setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
+ attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
+ }
+
+ @Override
+ public void exitSearchMode() {
+ setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false);
+ if (mHasWorkProfile) {
+ mViewPager.snapToPage(AdapterHolder.PRIMARY);
+ }
+ attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView);
+ }
+
+ @Override
+ public void onSearchResults(List<WidgetsListBaseEntry> entries) {
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries);
+ }
+
+ private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
+ mIsInSearchMode = isInSearchMode;
+ if (mHasWorkProfile) {
+ mViewPager.setVisibility(isInSearchMode ? GONE : VISIBLE);
+ mTabsView.setVisibility(isInSearchMode ? GONE : VISIBLE);
+ } else {
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView
+ .setVisibility(isInSearchMode ? GONE : VISIBLE);
+ }
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView
+ .setVisibility(mIsInSearchMode ? VISIBLE : GONE);
+ }
+
private void open(boolean animate) {
if (animate) {
if (getPopupContainer().getInsets().bottom > 0) {
@@ -385,14 +437,16 @@
@Override
public int getHeaderViewHeight() {
- // No need to check work profile here because mInitialTabHeight is always 0 if there is no
- // work profile.
- return mInitialTabsHeight
- + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mContainer);
+ return measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mCollapseHandle)
+ + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mHeaderTitle)
+ + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mSearchBar);
}
/** private the height, in pixel, + the vertical margins of a given view. */
private static int measureHeightWithVerticalMargins(View view) {
+ if (view.getVisibility() != VISIBLE) {
+ return 0;
+ }
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+ marginLayoutParams.topMargin;
@@ -402,6 +456,7 @@
private final class AdapterHolder {
static final int PRIMARY = 0;
static final int WORK = 1;
+ static final int SEARCH = 2;
private final int mAdapterType;
private final WidgetsListAdapter mWidgetsListAdapter;
@@ -420,8 +475,16 @@
apps.getIconCache(),
/* iconClickListener= */ WidgetsFullSheet.this,
/* iconLongClickListener= */ WidgetsFullSheet.this);
- mWidgetsListAdapter.setFilter(
- mAdapterType == PRIMARY ? mPrimaryWidgetsFilter : mWorkWidgetsFilter);
+ switch (mAdapterType) {
+ case PRIMARY:
+ mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
+ break;
+ case WORK:
+ mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
+ break;
+ default:
+ break;
+ }
}
void setup(WidgetsRecyclerView recyclerView) {
@@ -437,7 +500,7 @@
final class SearchAndRecommendationViewHolder {
final View mContainer;
final View mCollapseHandle;
- final EditText mSearchBar;
+ final WidgetsSearchBar mSearchBar;
final TextView mHeaderTitle;
SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 8b49d1e..9009eb1 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -34,11 +34,12 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.recyclerview.ViewHolderBinder;
import com.android.launcher3.util.LabelComparator;
+import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
-import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
import java.util.ArrayList;
import java.util.Comparator;
@@ -62,8 +63,9 @@
private static final boolean DEBUG = false;
/** Uniquely identifies widgets list view type within the app. */
- private static final int VIEW_TYPE_WIDGETS_LIST = R.layout.widgets_list_row_view;
- private static final int VIEW_TYPE_WIDGETS_HEADER = R.layout.widgets_list_row_header;
+ private static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
+ private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
+ private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;
private final WidgetsDiffReporter mDiffReporter;
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
@@ -73,11 +75,13 @@
private List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
- @Nullable private String mWidgetsContentVisiblePackage = null;
+ @Nullable private PackageUserKey mWidgetsContentVisiblePackageUserKey = null;
private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
entry instanceof WidgetsListHeaderEntry
- || entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage);
+ || entry instanceof WidgetsListSearchHeaderEntry
+ || new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+ .equals(mWidgetsContentVisiblePackageUserKey);
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
@@ -87,8 +91,14 @@
mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader);
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
- mViewHolderBinders.put(VIEW_TYPE_WIDGETS_HEADER,
- new WidgetsListHeaderViewHolderBinder(layoutInflater, this::onHeaderClicked));
+ mViewHolderBinders.put(
+ VIEW_TYPE_WIDGETS_HEADER,
+ new WidgetsListHeaderViewHolderBinder(
+ layoutInflater, /*onHeaderClickListener=*/this));
+ mViewHolderBinders.put(
+ VIEW_TYPE_WIDGETS_SEARCH_HEADER,
+ new WidgetsListSearchHeaderViewHolderBinder(
+ layoutInflater, /*onHeaderClickListener=*/ this));
}
public void setFilter(Predicate<WidgetsListBaseEntry> filter) {
@@ -122,23 +132,40 @@
return mVisibleEntries.size();
}
+ /** Returns all items that will be drawn in a recycler view. */
+ public List<WidgetsListBaseEntry> getItems() {
+ return mVisibleEntries;
+ }
+
/** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
public String getSectionName(int pos) {
return mVisibleEntries.get(pos).mTitleSectionName;
}
- /** Updates the widget list. */
+ /** Updates the widget list based on {@code tempEntries}. */
public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
mAllEntries = tempEntries.stream().sorted(mRowComparator)
.collect(Collectors.toList());
updateVisibleEntries();
}
+ /** Updates the widget list based on {@code searchResults}. */
+ public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
+ // Forget the expanded package every time widget list is refreshed in search mode.
+ mWidgetsContentVisiblePackageUserKey = null;
+ setWidgets(searchResults);
+ }
+
private void updateVisibleEntries() {
mAllEntries.forEach(entry -> {
if (entry instanceof WidgetsListHeaderEntry) {
((WidgetsListHeaderEntry) entry).setIsWidgetListShown(
- entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage));
+ new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+ .equals(mWidgetsContentVisiblePackageUserKey));
+ } else if (entry instanceof WidgetsListSearchHeaderEntry) {
+ ((WidgetsListSearchHeaderEntry) entry).setIsWidgetListShown(
+ new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+ .equals(mWidgetsContentVisiblePackageUserKey));
}
});
List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
@@ -189,17 +216,19 @@
return VIEW_TYPE_WIDGETS_LIST;
} else if (entry instanceof WidgetsListHeaderEntry) {
return VIEW_TYPE_WIDGETS_HEADER;
+ } else if (entry instanceof WidgetsListSearchHeaderEntry) {
+ return VIEW_TYPE_WIDGETS_SEARCH_HEADER;
}
throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
}
@Override
- public void onHeaderClicked(boolean showWidgets, String expandedPackage) {
+ public void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey) {
if (showWidgets) {
- mWidgetsContentVisiblePackage = expandedPackage;
+ mWidgetsContentVisiblePackageUserKey = packageUserKey;
updateVisibleEntries();
- } else if (expandedPackage.equals(mWidgetsContentVisiblePackage)) {
- mWidgetsContentVisiblePackage = null;
+ } else if (packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) {
+ mWidgetsContentVisiblePackageUserKey = null;
updateVisibleEntries();
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index 070a9aa..119d094 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -41,6 +41,9 @@
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import java.util.stream.Collectors;
/**
* A UI represents a header of an app shown in the full widgets tray.
@@ -173,7 +176,7 @@
shortcutsCount);
} else if (entry.widgetsCount > 0) {
subtitle = resources.getQuantityString(R.plurals.widgets_count,
- entry.widgetsCount, entry.widgetsCount);
+ entry.widgetsCount, entry.widgetsCount);
} else {
subtitle = resources.getQuantityString(R.plurals.shortcuts_count,
entry.shortcutsCount, entry.shortcutsCount);
@@ -182,6 +185,32 @@
mSubtitle.setVisibility(VISIBLE);
}
+ /** Apply app icon, labels and tag using a generic {@link WidgetsListSearchHeaderEntry}. */
+ @UiThread
+ public void applyFromItemInfoWithIcon(WidgetsListSearchHeaderEntry entry) {
+ applyIconAndLabel(entry);
+ }
+
+ @UiThread
+ private void applyIconAndLabel(WidgetsListSearchHeaderEntry entry) {
+ PackageItemInfo info = entry.mPkgItem;
+ setIcon(info);
+ setTitles(entry);
+ setExpanded(entry.isWidgetListShown());
+
+ super.setTag(info);
+
+ verifyHighRes();
+ }
+
+ private void setTitles(WidgetsListSearchHeaderEntry entry) {
+ mTitle.setText(entry.mPkgItem.title);
+
+ mSubtitle.setText(entry.mWidgets.stream()
+ .map(item -> item.label).sorted().collect(Collectors.joining(", ")));
+ mSubtitle.setVisibility(VISIBLE);
+ }
+
@Override
public void reapplyItemInfo(ItemInfoWithIcon info) {
if (getTag() == info) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
index ed53e6f..fcefe3a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
@@ -20,6 +20,7 @@
import com.android.launcher3.R;
import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
/**
@@ -50,12 +51,9 @@
widgetsListHeader.applyFromItemInfoWithIcon(data);
widgetsListHeader.setExpanded(data.isWidgetListShown());
widgetsListHeader.setOnExpandChangeListener(isExpanded ->
- mOnHeaderClickListener.onHeaderClicked(isExpanded, data.mPkgItem.packageName));
- }
-
- /** A listener to be invoked when {@link WidgetsListHeader} is clicked. */
- public interface OnHeaderClickListener {
- /** Calls when {@link WidgetsListHeader} is clicked to show / hide widgets for a package. */
- void onHeaderClicked(boolean showWidgets, String packageName);
+ mOnHeaderClickListener.onHeaderClicked(
+ isExpanded,
+ new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)
+ ));
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java
new file mode 100644
index 0000000..9562af3
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * A {@link ViewHolder} for {@link WidgetsListHeader} of an app, which renders the app icon, the app
+ * name, label and a button for showing / hiding widgets.
+ */
+public final class WidgetsListSearchHeaderHolder extends ViewHolder {
+ final WidgetsListHeader mWidgetsListHeader;
+
+ public WidgetsListSearchHeaderHolder(WidgetsListHeader view) {
+ super(view);
+
+ mWidgetsListHeader = view;
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java
new file mode 100644
index 0000000..83c7948
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+/**
+ * Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
+ */
+public final class WidgetsListSearchHeaderViewHolderBinder implements
+ ViewHolderBinder<WidgetsListSearchHeaderEntry, WidgetsListSearchHeaderHolder> {
+ private final LayoutInflater mLayoutInflater;
+ private final OnHeaderClickListener mOnHeaderClickListener;
+
+ public WidgetsListSearchHeaderViewHolderBinder(LayoutInflater layoutInflater,
+ OnHeaderClickListener onHeaderClickListener) {
+ mLayoutInflater = layoutInflater;
+ mOnHeaderClickListener = onHeaderClickListener;
+ }
+
+ @Override
+ public WidgetsListSearchHeaderHolder newViewHolder(ViewGroup parent) {
+ WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+ R.layout.widgets_list_row_header, parent, false);
+
+ return new WidgetsListSearchHeaderHolder(header);
+ }
+
+ @Override
+ public void bindViewHolder(WidgetsListSearchHeaderHolder viewHolder,
+ WidgetsListSearchHeaderEntry data) {
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ widgetsListHeader.applyFromItemInfoWithIcon(data);
+ widgetsListHeader.setExpanded(data.isWidgetListShown());
+ widgetsListHeader.setOnExpandChangeListener(isExpanded ->
+ mOnHeaderClickListener.onHeaderClicked(isExpanded,
+ new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)));
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index d65a809..9ab6424 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -21,13 +21,19 @@
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
+import android.widget.TableLayout;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
/**
* The widgets recycler view.
@@ -39,8 +45,10 @@
private final int mScrollbarTop;
private final Point mFastScrollerOffset = new Point();
+ private final int mEstimatedWidgetListHeaderHeight;
private boolean mTouchDownOnScroller;
private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
+ private int mLastVisibleWidgetContentTableHeight = 0;
public WidgetsRecyclerView(Context context) {
this(context, null);
@@ -55,6 +63,12 @@
super(context, attrs, defStyleAttr);
mScrollbarTop = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
addOnItemTouchListener(this);
+
+ ActivityContext activity = ActivityContext.lookupContext(getContext());
+ DeviceProfile grid = activity.getDeviceProfile();
+ mEstimatedWidgetListHeaderHeight = grid.iconSizePx
+ + 2 * context.getResources().getDimensionPixelSize(
+ R.dimen.widget_list_header_view_vertical_padding);
}
@Override
@@ -123,21 +137,32 @@
View child = getChildAt(0);
int rowIndex = getChildPosition(child);
- int y = (child.getMeasuredHeight() * rowIndex);
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ if (view instanceof TableLayout) {
+ // This assumes there is ever only one content shown in this recycler view.
+ mLastVisibleWidgetContentTableHeight = view.getMeasuredHeight();
+ }
+ }
+
+ int scrollPosition = getItemsHeight(rowIndex);
int offset = getLayoutManager().getDecoratedTop(child);
- return getPaddingTop() + y - offset;
+ return getPaddingTop() + scrollPosition - offset;
}
/**
- * Returns the available scroll height:
- * AvailableScrollHeight = Total height of the all items - last page height
+ * Returns the available scroll height, in pixel.
+ *
+ * <p>If the recycler view can't be scrolled, returns 0.
*/
@Override
protected int getAvailableScrollHeight() {
- View child = getChildAt(0);
- return child.getMeasuredHeight() * mAdapter.getItemCount() + getScrollBarTop()
- + getPaddingBottom() - mScrollbar.getHeight();
+ // AvailableScrollHeight = Total height of the all items - first page height
+ int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+ int totalHeightOfAllItems = getItemsHeight(/* untilIndex= */ mAdapter.getItemCount());
+ int availableScrollHeight = totalHeightOfAllItems - firstPageHeight;
+ return Math.max(0, availableScrollHeight);
}
private boolean isModelNotReady() {
@@ -181,6 +206,31 @@
}
/**
+ * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until
+ * {@code untilIndex}.
+ *
+ * <p>If the untilIndex is larger than the total number of items in this adapter, returns the
+ * sum of all items' height.
+ */
+ private int getItemsHeight(int untilIndex) {
+ if (untilIndex > mAdapter.getItems().size()) {
+ untilIndex = mAdapter.getItems().size();
+ }
+ int totalItemsHeight = 0;
+ for (int i = 0; i < untilIndex; i++) {
+ WidgetsListBaseEntry entry = mAdapter.getItems().get(i);
+ if (entry instanceof WidgetsListHeaderEntry) {
+ totalItemsHeight += mEstimatedWidgetListHeaderHeight;
+ } else if (entry instanceof WidgetsListContentEntry) {
+ totalItemsHeight += mLastVisibleWidgetContentTableHeight;
+ } else {
+ throw new UnsupportedOperationException("Can't estimate height for " + entry);
+ }
+ }
+ return totalItemsHeight;
+ }
+
+ /**
* Provides dimensions of the header view that is shown at the top of a
* {@link WidgetsRecyclerView}.
*/
diff --git a/src/com/android/launcher3/widget/picker/search/SearchModeListener.java b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java
new file mode 100644
index 0000000..cee7d67
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker.search;
+
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
+
+/**
+ * A listener to help with widgets picker search.
+ */
+public interface SearchModeListener {
+ /**
+ * Notifies the subscriber when user enters widget picker search mode.
+ */
+ void enterSearchMode();
+
+ /**
+ * Notifies the subscriber when user exits widget picker search mode.
+ */
+ void exitSearchMode();
+
+ /**
+ * Notifies the subscriber with search results.
+ */
+ void onSearchResults(List<WidgetsListBaseEntry> entries);
+}
diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipeline.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipeline.java
index 9911495..5222e8e 100644
--- a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipeline.java
+++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipeline.java
@@ -16,12 +16,19 @@
package com.android.launcher3.widget.picker.search;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import static com.android.launcher3.search.StringMatcherUtility.matches;
-import java.text.Collator;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* Implementation of {@link WidgetsPickerSearchPipeline} that performs search by prefix matching on
@@ -37,52 +44,29 @@
@Override
public void query(String input, Consumer<List<WidgetsListBaseEntry>> callback) {
- StringMatcher matcher = StringMatcher.getInstance();
ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
- // TODO(b/157286785): Filter entries based on query prefix matching on widget labels also.
- for (WidgetsListBaseEntry e : mAllEntries) {
- if (matcher.matches(input, e.mPkgItem.title.toString())) {
- results.add(e);
- }
- }
+ mAllEntries.stream().filter(entry -> entry instanceof WidgetsListHeaderEntry)
+ .forEach(headerEntry -> {
+ List<WidgetItem> matchedWidgetItems = filterWidgetItems(
+ input, headerEntry.mPkgItem.title.toString(), headerEntry.mWidgets);
+ if (matchedWidgetItems.size() > 0) {
+ results.add(new WidgetsListSearchHeaderEntry(headerEntry.mPkgItem,
+ headerEntry.mTitleSectionName, matchedWidgetItems));
+ results.add(new WidgetsListContentEntry(headerEntry.mPkgItem,
+ headerEntry.mTitleSectionName, matchedWidgetItems));
+ }
+ });
callback.accept(results);
}
- /**
- * Performs locale sensitive string comparison using {@link Collator}.
- */
- public static class StringMatcher {
-
- private static final char MAX_UNICODE = '\uFFFF';
-
- private final Collator mCollator;
-
- StringMatcher() {
- mCollator = Collator.getInstance();
- mCollator.setStrength(Collator.PRIMARY);
- mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
+ private List<WidgetItem> filterWidgetItems(String query, String packageTitle,
+ List<WidgetItem> items) {
+ StringMatcher matcher = StringMatcher.getInstance();
+ if (matches(query, packageTitle, matcher)) {
+ return items;
}
-
- /**
- * Returns true if {@param query} is a prefix of {@param target}.
- */
- public boolean matches(String query, String target) {
- switch (mCollator.compare(query, target)) {
- case 0:
- return true;
- case -1:
- // The target string can contain a modifier which would make it larger than
- // the query string (even though the length is same). If the query becomes
- // larger after appending a unicode character, it was originally a prefix of
- // the target string and hence should match.
- return mCollator.compare(query + MAX_UNICODE, target) > -1;
- default:
- return false;
- }
- }
-
- public static StringMatcher getInstance() {
- return new StringMatcher();
- }
+ return items.stream()
+ .filter(item -> matches(query, item.label, matcher))
+ .collect(Collectors.toList());
}
}
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
new file mode 100644
index 0000000..d8e9733
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
+
+/**
+ * View for a search bar with an edit text with a cancel button.
+ */
+public class WidgetsSearchBar extends LinearLayout {
+ private WidgetsSearchBarController mController;
+ private EditText mEditText;
+ private ImageButton mCancelButton;
+
+ public WidgetsSearchBar(Context context) {
+ this(context, null, 0);
+ }
+
+ public WidgetsSearchBar(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WidgetsSearchBar(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Attaches a controller to the search bar which interacts with {@code searchModeListener}.
+ */
+ public void initialize(List<WidgetsListBaseEntry> allWidgets,
+ SearchModeListener searchModeListener) {
+ SearchAlgorithm<WidgetsListBaseEntry> algo =
+ new SimpleWidgetsSearchAlgorithm(new SimpleWidgetsSearchPipeline(allWidgets));
+ mController = new WidgetsSearchBarController(
+ algo, mEditText, mCancelButton, searchModeListener);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mEditText = findViewById(R.id.widgets_search_bar_edit_text);
+ mCancelButton = findViewById(R.id.widgets_search_cancel_button);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mController.onDestroy();
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
new file mode 100644
index 0000000..6c37484
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.ArrayList;
+
+/**
+ * Controller for a search bar with an edit text and a cancel button.
+ */
+public class WidgetsSearchBarController implements TextWatcher,
+ SearchCallback<WidgetsListBaseEntry> {
+ private static final String TAG = "WidgetsSearchBarController";
+ private static final boolean DEBUG = false;
+
+ protected SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
+ protected EditText mInput;
+ protected ImageButton mCancelButton;
+ protected SearchModeListener mSearchModeListener;
+ protected String mQuery;
+
+ public WidgetsSearchBarController(
+ SearchAlgorithm<WidgetsListBaseEntry> algo, EditText editText, ImageButton cancelButton,
+ SearchModeListener searchModeListener) {
+ mSearchAlgorithm = algo;
+ mInput = editText;
+ mInput.addTextChangedListener(this);
+ mCancelButton = cancelButton;
+ mCancelButton.setOnClickListener(v -> clearSearchResult());
+ mSearchModeListener = searchModeListener;
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ mQuery = s.toString();
+ if (mQuery.isEmpty()) {
+ mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
+ mSearchModeListener.exitSearchMode();
+ mCancelButton.setVisibility(GONE);
+ } else {
+ mSearchAlgorithm.cancel(/* interruptActiveRequests= */ false);
+ mSearchModeListener.enterSearchMode();
+ mSearchAlgorithm.doSearch(mQuery, this);
+ mCancelButton.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
+ if (DEBUG) {
+ Log.d(TAG, "onSearchResult query: " + query + " items: " + items);
+ }
+ mSearchModeListener.onSearchResults(items);
+ }
+
+ @Override
+ public void onAppendSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
+ // Not needed.
+ }
+
+ @Override
+ public void clearSearchResult() {
+ mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
+ mInput.getText().clear();
+ mInput.clearFocus();
+ mSearchModeListener.exitSearchMode();
+ }
+
+ /**
+ * Cleans up after search is no longer needed.
+ */
+ public void onDestroy() {
+ mSearchAlgorithm.destroy();
+ }
+}
diff --git a/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
deleted file mode 100644
index f8a9a04..0000000
--- a/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import android.os.Parcelable;
-
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-
-import java.util.List;
-
-/**
- * Interface to provide SmartspaceTargets to BcSmartspace.
- */
-@ProvidesInterface(action = BcSmartspaceDataPlugin.ACTION, version = BcSmartspaceDataPlugin.VERSION)
-public interface BcSmartspaceDataPlugin extends Plugin {
- String ACTION = "com.android.systemui.action.PLUGIN_BC_SMARTSPACE_DATA";
- int VERSION = 1;
-
- /** Register a listener to get Smartspace data. */
- void registerListener(SmartspaceTargetListener listener);
-
- /** Unregister a listener. */
- void unregisterListener(SmartspaceTargetListener listener);
-
- /** Provides Smartspace data to registered listeners. */
- interface SmartspaceTargetListener {
- /** Each Parcelable is a SmartspaceTarget that represents a card. */
- void onSmartspaceTargetsUpdated(List<? extends Parcelable> targets);
- }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
index da5a94f..e85e505 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -49,4 +49,11 @@
public static OverviewState newModalTaskState(int id) {
return new OverviewState(id);
}
+
+ /**
+ * New Overview substate that represents the overview in modal mode (one task shown on its own)
+ */
+ public static OverviewState newSplitSelectState(int id) {
+ return new OverviewState(id);
+ }
}
diff --git a/tests/Launcher3Tests.xml b/tests/Launcher3Tests.xml
new file mode 100644
index 0000000..3fff622
--- /dev/null
+++ b/tests/Launcher3Tests.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- This test config file is auto-generated. -->
+<configuration description="Runs Launcher3 tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="set-test-harness" value="true" />
+ <option name="run-command" value="am force-stop com.android.launcher3" />
+ <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher" />
+ <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher.out_of_proc_tests" />
+ <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher.tests" />
+ <option name="run-command" value="pm disable com.google.android.googlequicksearchbox" />
+
+ <option name="run-command" value="input keyevent 82" />
+ <option name="run-command" value="settings delete secure assistant" />
+ <option name="run-command" value="settings put global airplane_mode_on 1" />
+ <option name="run-command" value="am broadcast -a android.intent.action.AIRPLANE_MODE" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="Launcher3Tests.apk" />
+ <option name="test-file-name" value="Launcher3.apk" />
+ </target_preparer>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/data/user/0/com.android.launcher3/files" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.launcher3.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
deleted file mode 100644
index 39709a9..0000000
--- a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2016 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.allapps.search;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ComponentName;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.model.data.AppInfo;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link DefaultAppSearchAlgorithm}
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class DefaultAppSearchAlgorithmTest {
- private static final DefaultAppSearchAlgorithm.StringMatcher MATCHER =
- DefaultAppSearchAlgorithm.StringMatcher.getInstance();
-
- @Test
- public void testMatches() {
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white cow"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCow"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCOW"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCOW"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white2cow"), "cow", MATCHER));
-
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecow"), "cow", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitEcow"), "cow", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCow"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecow cow"), "cow", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecowcow"), "cow", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whit ecowcow"), "cow", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&dogs"), "dog", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "dog", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "&", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "43", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "3", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Q"), "q", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo(" Q"), "q", MATCHER));
-
- // match lower case words
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("elephant"), "e", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电子", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "子", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "邮件", MATCHER));
-
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("Bot"), "ba", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("bot"), "ba", MATCHER));
- }
-
- @Test
- public void testMatchesVN() {
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드"), "다", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("드라이브"), "드", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷ", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("운로 드라이브"), "ㄷ", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åbç", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Alpha"), "ål", MATCHER));
-
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷㄷ", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("로드라이브"), "ㄷ", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åç", MATCHER));
- }
-
- private AppInfo getInfo(String title) {
- AppInfo info = new AppInfo();
- info.title = title;
- info.componentName = new ComponentName("Test", title);
- return info;
- }
-}
diff --git a/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java
new file mode 100644
index 0000000..413f404
--- /dev/null
+++ b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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.search;
+
+import static com.android.launcher3.search.StringMatcherUtility.matches;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link StringMatcherUtility}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StringMatcherUtilityTest {
+ private static final StringMatcher MATCHER =
+ StringMatcher.getInstance();
+
+ @Test
+ public void testMatches() {
+ assertTrue(matches("white ", "white cow", MATCHER));
+ assertTrue(matches("white c", "white cow", MATCHER));
+ assertTrue(matches("cow", "white cow", MATCHER));
+ assertTrue(matches("cow", "whiteCow", MATCHER));
+ assertTrue(matches("cow", "whiteCOW", MATCHER));
+ assertTrue(matches("cow", "whitecowCOW", MATCHER));
+ assertTrue(matches("cow", "white2cow", MATCHER));
+
+ assertFalse(matches("cow", "whitecow", MATCHER));
+ assertFalse(matches("cow", "whitEcow", MATCHER));
+
+ assertTrue(matches("cow", "whitecowCow", MATCHER));
+ assertTrue(matches("cow", "whitecow cow", MATCHER));
+ assertFalse(matches("cow", "whitecowcow", MATCHER));
+ assertFalse(matches("cow", "whit ecowcow", MATCHER));
+
+ assertTrue(matches("dog", "cats&dogs", MATCHER));
+ assertTrue(matches("dog", "cats&Dogs", MATCHER));
+ assertTrue(matches("&", "cats&Dogs", MATCHER));
+
+ assertTrue(matches("43", "2+43", MATCHER));
+ assertFalse(matches("3", "2+43", MATCHER));
+
+ assertTrue(matches("q", "Q", MATCHER));
+ assertTrue(matches("q", " Q", MATCHER));
+
+ // match lower case words
+ assertTrue(matches("e", "elephant", MATCHER));
+ assertTrue(matches("eL", "Elephant", MATCHER));
+
+ assertTrue(matches("电", "电子邮件", MATCHER));
+ assertTrue(matches("电子", "电子邮件", MATCHER));
+ assertTrue(matches("子", "电子邮件", MATCHER));
+ assertTrue(matches("邮件", "电子邮件", MATCHER));
+
+ assertFalse(matches("ba", "Bot", MATCHER));
+ assertFalse(matches("ba", "bot", MATCHER));
+ assertFalse(matches("phant", "elephant", MATCHER));
+ assertFalse(matches("elephants", "elephant", MATCHER));
+ }
+
+ @Test
+ public void testMatchesVN() {
+ assertTrue(matches("다", "다운로드", MATCHER));
+ assertTrue(matches("드", "드라이브", MATCHER));
+ assertTrue(matches("ㄷ", "다운로드 드라이브", MATCHER));
+ assertTrue(matches("ㄷ", "운로 드라이브", MATCHER));
+ assertTrue(matches("åbç", "abc", MATCHER));
+ assertTrue(matches("ål", "Alpha", MATCHER));
+
+ assertFalse(matches("ㄷㄷ", "다운로드 드라이브", MATCHER));
+ assertFalse(matches("ㄷ", "로드라이브", MATCHER));
+ assertFalse(matches("åç", "abc", MATCHER));
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index b6c17df..e32250e 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -27,7 +27,6 @@
import androidx.test.uiautomator.Direction;
import androidx.test.uiautomator.UiObject2;
-import com.android.launcher3.ResourceUtils;
import com.android.launcher3.testing.TestProtocol;
import java.util.stream.Collectors;
@@ -108,26 +107,24 @@
"apps_list_view");
final UiObject2 searchBox = getSearchBox(allAppsContainer);
- int bottomGestureMargin = ResourceUtils.getNavbarSize(
- ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1;
- int deviceHeight = mLauncher.getDevice().getDisplayHeight();
- int displayBottom = deviceHeight - bottomGestureMargin;
+ int deviceHeight = mLauncher.getRealDisplaySize().y;
+ int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen();
final BySelector appIconSelector = AppIcon.getAppIconSelector(appName, mLauncher);
if (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
- displayBottom)) {
+ bottomGestureStartOnScreen)) {
scrollBackToBeginning();
int attempts = 0;
int scroll = getAllAppsScroll();
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled")) {
while (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
- displayBottom)) {
+ bottomGestureStartOnScreen)) {
mLauncher.scrollToLastVisibleRow(
allAppsContainer,
mLauncher.getObjectsInContainer(allAppsContainer, "icon")
.stream()
.filter(icon ->
- mLauncher.getVisibleBounds(icon).bottom
- <= displayBottom)
+ mLauncher.getVisibleBounds(icon).top
+ < bottomGestureStartOnScreen)
.collect(Collectors.toList()),
mLauncher.getVisibleBounds(searchBox).bottom
- mLauncher.getVisibleBounds(allAppsContainer).top);
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index a3f37d2..5138f02 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1027,16 +1027,20 @@
expectedState);
}
- int getBottomGestureSize() {
+ private int getBottomGestureSize() {
return ResourceUtils.getNavbarSize(
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()) + 1;
}
int getBottomGestureMarginInContainer(UiObject2 container) {
- final int bottomGestureStartOnScreen = getRealDisplaySize().y - getBottomGestureSize();
+ final int bottomGestureStartOnScreen = getBottomGestureStartOnScreen();
return getVisibleBounds(container).bottom - bottomGestureStartOnScreen;
}
+ int getBottomGestureStartOnScreen() {
+ return getRealDisplaySize().y - getBottomGestureSize();
+ }
+
void clickLauncherObject(UiObject2 object) {
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);