Support exclusion widget category filter in the widget picker
The newly added NOT_KEYGUARD category enables hosts that shows all
widgets to let widgets opt out from being displayed in keyguard like
surfaces.
Bug: 394047125
Test: Unit tests and support app
Flag: EXEMPT bugfix
Change-Id: Ibaab0c8a052700b77289cd571bca33e3d96fa09f
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index dc0f899..1cf7dda 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -23,6 +23,8 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
import static java.util.Collections.emptyList;
import android.appwidget.AppWidgetManager;
@@ -53,6 +55,7 @@
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.WidgetCategoryFilter;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
@@ -81,6 +84,10 @@
// the intent, then widgets will not be filtered for size.
private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
+ // Unlike the AppWidgetManager.EXTRA_CATEGORY_FILTER, this filter removes certain categories.
+ // Filter is ignore if it is not a negative value.
+ // Example usage: WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
+ private static final String EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter";
/**
* Widgets currently added by the user in the UI surface.
* <p>This allows widget picker to exclude existing widgets from suggestions.</p>
@@ -120,7 +127,8 @@
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
- private int mWidgetCategoryFilter;
+ private WidgetCategoryFilter mWidgetCategoryInclusionFilter;
+ private WidgetCategoryFilter mWidgetCategoryExclusionFilter;
@Nullable
private String mUiSurface;
// Widgets existing on the host surface.
@@ -194,8 +202,19 @@
getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
// Defaults to '0' to indicate that there isn't a category filter.
- mWidgetCategoryFilter =
- getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+ // Negative value indicates it's an exclusion filter (e.g. NOT_KEYGUARD_CATEGORY.inv())
+ // Positive value indicates it's inclusion filter (e.g. HOME_SCREEN or KEYGUARD)
+ // Note: A filter can either be inclusion or exclusion filter; not both.
+ int inclusionFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+ if (inclusionFilter < 0) {
+ Log.w(TAG, "Invalid EXTRA_CATEGORY_FILTER: " + inclusionFilter);
+ }
+ mWidgetCategoryInclusionFilter = new WidgetCategoryFilter(max(0, inclusionFilter));
+ int exclusionFilter = getIntent().getIntExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, 0);
+ if (exclusionFilter > 0) {
+ Log.w(TAG, "Invalid EXTRA_CATEGORY_EXCLUSION_FILTER: " + exclusionFilter);
+ }
+ mWidgetCategoryExclusionFilter = new WidgetCategoryFilter(min(0 , exclusionFilter));
String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE);
if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) {
@@ -436,11 +455,13 @@
widget.user.getIdentifier());
}
- if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
+ if (!mWidgetCategoryInclusionFilter.matches(info.widgetCategory)
+ || !mWidgetCategoryExclusionFilter.matches(info.widgetCategory)) {
return rejectWidget(
widget,
- "doesn't match category filter [filter=%d, widget=%d]",
- mWidgetCategoryFilter,
+ "doesn't match category filter [inclusion=%d, exclusion=%d, widget=%d]",
+ mWidgetCategoryInclusionFilter.getCategoryMask(),
+ mWidgetCategoryExclusionFilter.getCategoryMask(),
info.widgetCategory);
}
@@ -463,7 +484,7 @@
mDesiredWidgetWidth);
}
- final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
+ final int minWidth = min(info.minResizeWidth, info.minWidth);
if (minWidth > mDesiredWidgetWidth) {
return rejectWidget(
widget,
@@ -487,7 +508,7 @@
mDesiredWidgetHeight);
}
- final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
+ final int minHeight = min(info.minResizeHeight, info.minHeight);
if (minHeight > mDesiredWidgetHeight) {
return rejectWidget(
widget,
diff --git a/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt b/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt
new file mode 100644
index 0000000..69feb4a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 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
+
+/**
+ * A filter that can be applied on the widgetCategory attribute from appwidget-provider to identify
+ * if the widget can be displayed on a specific widget surface.
+ * - Negative value (e.g. "category_a.inv() and category_b.inv()" excludes the widgets with given
+ * categories.
+ * - Positive value (e.g. "category_a or category_b" includes widgets with those categories.
+ * - 0 means no filter.
+ */
+class WidgetCategoryFilter(val categoryMask: Int) {
+ /** Applies the [categoryMask] to return if the [widgetCategory] matches. */
+ fun matches(widgetCategory: Int): Boolean {
+ return if (categoryMask > 0) { // inclusion filter
+ (widgetCategory and categoryMask) != 0
+ } else if (categoryMask < 0) { // exclusion filter
+ (widgetCategory and categoryMask) == widgetCategory
+ } else {
+ true // no filter
+ }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt
new file mode 100644
index 0000000..9b0a95a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2025 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.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WidgetCategoryFilterTest {
+
+ @Test
+ fun filterValueZero_everythingMatches() {
+ val noFilter = WidgetCategoryFilter(categoryMask = 0)
+
+ noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ noFilter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ noFilter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD)
+ noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+ noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD)
+ noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_NOT_KEYGUARD)
+ noFilter.assertMatches(
+ WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD
+ )
+ noFilter.assertMatches(
+ WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_NOT_KEYGUARD
+ )
+ noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+ }
+
+ @Test
+ fun includeHomeScreen_matchesOnlyIfHomeScreenExists() {
+ val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN)
+
+ filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX)
+ }
+
+ @Test
+ fun includeHomeScreenOrKeyguard_matchesIfEitherHomeScreenOrKeyguardExists() {
+ val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+ }
+
+ @Test
+ fun excludeNotKeyguard_doesNotMatchIfNotKeyguardExists() {
+ val filter = WidgetCategoryFilter(WIDGET_CATEGORY_NOT_KEYGUARD.inv())
+
+ filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(
+ WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN
+ )
+ }
+
+ @Test
+ fun multipleExclusions_doesNotMatchIfExcludedCategoriesExist() {
+ val filter =
+ WidgetCategoryFilter(
+ WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
+ )
+
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(
+ WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN
+ )
+ }
+
+ private fun WidgetCategoryFilter.assertMatches(category: Int) {
+ assertThat(matches(category)).isTrue()
+ }
+
+ private fun WidgetCategoryFilter.assertDoesNotMatch(category: Int) {
+ assertThat(matches(category)).isFalse()
+ }
+}