Merge changes from topic "SEARCH_INTEGRATE" into sc-dev
* changes: Add a WidgetsSearchBar(Launcher3) and a WidgetsSearchController. Have a recycler view to show search results.
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+21
@@ -221,6 +221,27 @@ public final class WidgetsDiffReporterTest {
|
||||
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) {
|
||||
|
||||
+33
-2
@@ -26,6 +26,8 @@ import android.appwidget.AppWidgetProviderInfo;
|
||||
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.ComponentWithLabel;
|
||||
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 @@ public final class WidgetsListAdapterTest {
|
||||
|
||||
private WidgetsListAdapter mAdapter;
|
||||
private InvariantDeviceProfile mTestProfile;
|
||||
private UserHandle mUserHandle;
|
||||
private Context mContext;
|
||||
|
||||
@Before
|
||||
@@ -76,6 +80,7 @@ public final class WidgetsListAdapterTest {
|
||||
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 @@ public final class WidgetsListAdapterTest {
|
||||
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 @@ public final class WidgetsListAdapterTest {
|
||||
// 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 @@ public final class WidgetsListAdapterTest {
|
||||
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.
|
||||
*
|
||||
|
||||
+23
-26
@@ -18,7 +18,9 @@ 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;
|
||||
@@ -26,12 +28,9 @@ import android.content.ComponentName;
|
||||
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.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.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 @@ public final class WidgetsListHeaderViewHolderBinderTest {
|
||||
// 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 @@ public final class WidgetsListHeaderViewHolderBinderTest {
|
||||
}).when(mIconCache).getTitleNoCache(any());
|
||||
|
||||
mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
|
||||
LayoutInflater.from(mTestActivity),
|
||||
mFakeOnHeaderClickListener);
|
||||
LayoutInflater.from(mTestActivity), mOnHeaderClickListener);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -125,6 +123,23 @@ public final class WidgetsListHeaderViewHolderBinderTest {
|
||||
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 @@ public final class WidgetsListHeaderViewHolderBinderTest {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+171
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
-41
@@ -19,8 +19,6 @@ package com.android.launcher3.widget.picker.search;
|
||||
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.model.data.PackageItemInfo;
|
||||
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 @@ import java.util.List;
|
||||
|
||||
@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 @@ public class SimpleWidgetsSearchPipelineTest {
|
||||
@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 @@ public class SimpleWidgetsSearchPipelineTest {
|
||||
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,
|
||||
|
||||
+140
@@ -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(), "");
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import com.android.launcher3.model.AllAppsList;
|
||||
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 @@ public class AppsSearchPipeline implements SearchPipeline {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,9 @@ import android.os.Handler;
|
||||
|
||||
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 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm<AdapterItem> {
|
||||
() -> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,9 @@ public interface SearchAlgorithm<T> {
|
||||
* Cancels any active request.
|
||||
*/
|
||||
void cancel(boolean interruptActiveRequests);
|
||||
|
||||
/**
|
||||
* Cleans up after search is no longer needed.
|
||||
*/
|
||||
default void destroy() {};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,14 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
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 abstract class WidgetsListBaseEntry {
|
||||
*/
|
||||
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 class WidgetsListBaseEntry {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,8 @@ package com.android.launcher3.widget.model;
|
||||
|
||||
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 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
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 final class WidgetsListContentEntry extends WidgetsListBaseEntry {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ package com.android.launcher3.widget.model;
|
||||
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 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
|
||||
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);
|
||||
}
|
||||
@@ -56,9 +56,22 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
|
||||
return mHasEntryUpdated;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ final class SearchAndRecommendationsScrollController implements
|
||||
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 @@ final class SearchAndRecommendationsScrollController implements
|
||||
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 @@ final class SearchAndRecommendationsScrollController implements
|
||||
mPrimaryRecyclerView.getPaddingRight(),
|
||||
mPrimaryRecyclerView.getPaddingBottom());
|
||||
}
|
||||
mSearchRecyclerView.setPadding(
|
||||
mSearchRecyclerView.getPaddingLeft(),
|
||||
topContainerHeight,
|
||||
mSearchRecyclerView.getPaddingRight(),
|
||||
mSearchRecyclerView.getPaddingBottom());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.android.launcher3.model.data.PackageItemInfo;
|
||||
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 @@ public class WidgetsDiffReporter {
|
||||
// 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 @@ public class WidgetsDiffReporter {
|
||||
* 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) {
|
||||
|
||||
@@ -34,7 +34,6 @@ import android.view.MotionEvent;
|
||||
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.views.TopRoundedCornerView;
|
||||
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 @@ import java.util.function.Predicate;
|
||||
*/
|
||||
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 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
|
||||
@Nullable private PersonalWorkPagedView mViewPager;
|
||||
private int mInitialTabsHeight = 0;
|
||||
private boolean mIsInSearchMode;
|
||||
private View mTabsView;
|
||||
private TextView mNoWidgetsView;
|
||||
private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
|
||||
@@ -91,6 +93,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
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 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
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 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
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 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
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 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
|
||||
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 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
@@ -404,6 +456,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
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;
|
||||
@@ -422,8 +475,16 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
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) {
|
||||
@@ -439,7 +500,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
final class SearchAndRecommendationViewHolder {
|
||||
final View mContainer;
|
||||
final View mCollapseHandle;
|
||||
final EditText mSearchBar;
|
||||
final WidgetsSearchBar mSearchBar;
|
||||
final TextView mHeaderTitle;
|
||||
|
||||
SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) {
|
||||
|
||||
@@ -34,11 +34,12 @@ import com.android.launcher3.WidgetPreviewLoader;
|
||||
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 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
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 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
|
||||
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 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
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) {
|
||||
@@ -132,18 +142,30 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
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()
|
||||
@@ -194,17 +216,19 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ import com.android.launcher3.model.data.ItemInfoWithIcon;
|
||||
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 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
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 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
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) {
|
||||
|
||||
@@ -20,6 +20,7 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -50,12 +51,9 @@ public final class WidgetsListHeaderViewHolderBinder implements
|
||||
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)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 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.text.Collator;
|
||||
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 @@ public final class SimpleWidgetsSearchPipeline implements WidgetsPickerSearchPip
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 List<WidgetItem> filterWidgetItems(String query, String packageTitle,
|
||||
List<WidgetItem> items) {
|
||||
StringMatcher matcher = StringMatcher.getInstance();
|
||||
if (matches(query, packageTitle, matcher)) {
|
||||
return items;
|
||||
}
|
||||
return items.stream()
|
||||
.filter(item -> matches(query, item.label, matcher))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user