• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3;
18 
19 import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT;
20 import static android.view.WindowInsets.Type.navigationBars;
21 import static android.view.WindowInsets.Type.statusBars;
22 
23 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
25 
26 import static java.lang.Math.max;
27 import static java.lang.Math.min;
28 
29 import android.appwidget.AppWidgetManager;
30 import android.appwidget.AppWidgetProviderInfo;
31 import android.content.ClipData;
32 import android.content.ClipDescription;
33 import android.content.Intent;
34 import android.os.Bundle;
35 import android.util.Log;
36 import android.view.View;
37 import android.view.WindowInsetsController;
38 import android.view.WindowManager;
39 import android.window.BackEvent;
40 import android.window.OnBackAnimationCallback;
41 import android.window.OnBackInvokedDispatcher;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 
46 import com.android.launcher3.dragndrop.SimpleDragLayer;
47 import com.android.launcher3.model.StringCache;
48 import com.android.launcher3.model.WidgetItem;
49 import com.android.launcher3.model.WidgetPredictionsRequester;
50 import com.android.launcher3.model.WidgetsModel;
51 import com.android.launcher3.model.data.ItemInfo;
52 import com.android.launcher3.model.data.PackageItemInfo;
53 import com.android.launcher3.widget.WidgetCell;
54 import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
55 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
56 import com.android.launcher3.widget.picker.WidgetCategoryFilter;
57 import com.android.launcher3.widget.picker.WidgetsFullSheet;
58 import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
59 import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
60 
61 import java.util.ArrayList;
62 import java.util.HashSet;
63 import java.util.List;
64 import java.util.Locale;
65 import java.util.Map;
66 import java.util.Set;
67 import java.util.function.Predicate;
68 import java.util.regex.Pattern;
69 
70 /** An Activity that can host Launcher's widget picker. */
71 public class WidgetPickerActivity extends BaseActivity implements
72         WidgetPredictionsRequester.WidgetPredictionsListener {
73     private static final String TAG = "WidgetPickerActivity";
74     /**
75      * Name of the extra that indicates that a widget being dragged.
76      *
77      * <p>When set to "true" in the result of startActivityForResult, the client that launched the
78      * picker knows that activity was closed due to pending drag.
79      */
80     private static final String EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag";
81 
82     // Intent extras that specify the desired widget width and height. If these are not specified in
83     // the intent, then widgets will not be filtered for size.
84     private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
85     private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
86     // Unlike the AppWidgetManager.EXTRA_CATEGORY_FILTER, this filter removes certain categories.
87     // Filter is ignore if it is not a negative value.
88     // Example usage: WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
89     private static final String EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter";
90     /**
91      * Widgets currently added by the user in the UI surface.
92      * <p>This allows widget picker to exclude existing widgets from suggestions.</p>
93      */
94     private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets";
95     /**
96      * Intent extra for the string representing the title displayed within the picker header.
97      */
98     private static final String EXTRA_PICKER_TITLE = "picker_title";
99     /**
100      * Intent extra for the string representing the description displayed within the picker header.
101      */
102     private static final String EXTRA_PICKER_DESCRIPTION = "picker_description";
103 
104     /**
105      * A unique identifier of the surface hosting the widgets;
106      * <p>"widgets" is reserved for home screen surface.</p>
107      * <p>"widgets_hub" is reserved for glanceable hub surface.</p>
108      */
109     private static final String EXTRA_UI_SURFACE = "ui_surface";
110     private static final Pattern UI_SURFACE_PATTERN =
111             Pattern.compile("^(widgets|widgets_hub)$");
112 
113     /**
114      * User ids that should be filtered out of the widget lists created by this activity.
115      */
116     private static final String EXTRA_USER_ID_FILTER = "filtered_user_ids";
117 
118     private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
119     private WidgetsModel mModel;
120     private LauncherAppState mApp;
121     private StringCache mStringCache;
122     private WidgetPredictionsRequester mWidgetPredictionsRequester;
123     private WidgetPickerDataProvider mWidgetPickerDataProvider;
124 
125     private int mDesiredWidgetWidth;
126     private int mDesiredWidgetHeight;
127     private WidgetCategoryFilter mWidgetCategoryInclusionFilter;
128     private WidgetCategoryFilter mWidgetCategoryExclusionFilter;
129     @Nullable
130     private String mUiSurface;
131     // Widgets existing on the host surface.
132     @NonNull
133     private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>();
134     @Nullable
135     private String mTitle;
136     @Nullable
137     private String mDescription;
138 
139     /** A set of user ids that should be filtered out from the selected widgets. */
140     @NonNull
141     Set<Integer> mFilteredUserIds = new HashSet<>();
142 
143     @Nullable
144     private WidgetsFullSheet mWidgetSheet;
145 
146     private final Predicate<WidgetItem> mNoShortcutsFilter = widget -> {
147         final WidgetAcceptabilityVerdict verdict =
148                 isWidgetAcceptable(widget, /* applySizeFilter=*/ false);
149         verdict.maybeLogVerdict();
150         return verdict.isAcceptable;
151     };
152     private final Predicate<WidgetItem> mHostSizeAndNoShortcutsFilter = widget -> {
153         final WidgetAcceptabilityVerdict verdict =
154                 isWidgetAcceptable(widget, /* applySizeFilter=*/ true);
155         verdict.maybeLogVerdict();
156         return verdict.isAcceptable;
157     };
158 
159     @Override
onCreate(Bundle savedInstanceState)160     protected void onCreate(Bundle savedInstanceState) {
161         super.onCreate(savedInstanceState);
162 
163         getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
164         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER);
165 
166         mApp = LauncherAppState.getInstance(this);
167         InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
168         mDeviceProfile = idp.getDeviceProfile(this);
169         mModel = new WidgetsModel(mApp.getContext());
170         mWidgetPickerDataProvider = new WidgetPickerDataProvider(this);
171 
172         setContentView(R.layout.widget_picker_activity);
173         mDragLayer = findViewById(R.id.drag_layer);
174         mDragLayer.recreateControllers();
175 
176         WindowInsetsController wc = mDragLayer.getWindowInsetsController();
177         wc.hide(navigationBars() + statusBars());
178 
179         parseIntentExtras();
180         refreshAndBindWidgets();
181     }
182 
183     @Override
registerBackDispatcher()184     protected void registerBackDispatcher() {
185         getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
186                 OnBackInvokedDispatcher.PRIORITY_DEFAULT,
187                 new BackAnimationCallback());
188     }
189 
parseIntentExtras()190     private void parseIntentExtras() {
191         mTitle = getIntent().getStringExtra(EXTRA_PICKER_TITLE);
192         mDescription = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION);
193 
194         // A value of 0 for either size means that no filtering will occur in that dimension. If
195         // both values are 0, then no size filtering will occur.
196         mDesiredWidgetWidth =
197                 getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_WIDTH, 0);
198         mDesiredWidgetHeight =
199                 getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
200 
201         // Defaults to '0' to indicate that there isn't a category filter.
202         // Negative value indicates it's an exclusion filter (e.g. NOT_KEYGUARD_CATEGORY.inv())
203         // Positive value indicates it's inclusion filter (e.g. HOME_SCREEN or KEYGUARD)
204         // Note: A filter can either be inclusion or exclusion filter; not both.
205         int inclusionFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
206         if (inclusionFilter < 0) {
207             Log.w(TAG, "Invalid EXTRA_CATEGORY_FILTER: " + inclusionFilter);
208         }
209         mWidgetCategoryInclusionFilter = new WidgetCategoryFilter(max(0, inclusionFilter));
210         int exclusionFilter = getIntent().getIntExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, 0);
211         if (exclusionFilter > 0) {
212             Log.w(TAG, "Invalid EXTRA_CATEGORY_EXCLUSION_FILTER: " + exclusionFilter);
213         }
214         mWidgetCategoryExclusionFilter = new WidgetCategoryFilter(min(0 , exclusionFilter));
215 
216         String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE);
217         if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) {
218             mUiSurface = uiSurfaceParam;
219         }
220         ArrayList<AppWidgetProviderInfo> addedWidgets = getIntent().getParcelableArrayListExtra(
221                 EXTRA_ADDED_APP_WIDGETS, AppWidgetProviderInfo.class);
222         if (addedWidgets != null) {
223             mAddedWidgets = addedWidgets;
224         }
225         ArrayList<Integer> filteredUsers = getIntent().getIntegerArrayListExtra(
226                 EXTRA_USER_ID_FILTER);
227         mFilteredUserIds.clear();
228         if (filteredUsers != null) {
229             mFilteredUserIds.addAll(filteredUsers);
230         }
231     }
232 
233     @NonNull
234     @Override
getWidgetPickerDataProvider()235     public WidgetPickerDataProvider getWidgetPickerDataProvider() {
236         return mWidgetPickerDataProvider;
237     }
238 
239     @Override
getDragLayer()240     public SimpleDragLayer<WidgetPickerActivity> getDragLayer() {
241         return mDragLayer;
242     }
243 
244     @Override
getItemOnClickListener()245     public View.OnClickListener getItemOnClickListener() {
246         return v -> {
247             final AppWidgetProviderInfo info =
248                     (v instanceof WidgetCell) ? ((WidgetCell) v).getWidgetItem().widgetInfo : null;
249             if (info == null || info.provider == null) {
250                 return;
251             }
252 
253             setResult(RESULT_OK, new Intent()
254                     .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider)
255                     .putExtra(Intent.EXTRA_USER, info.getProfile()));
256 
257             finish();
258         };
259     }
260 
261     @Override
getAllAppsItemLongClickListener()262     public View.OnLongClickListener getAllAppsItemLongClickListener() {
263         return view -> {
264             if (!(view instanceof WidgetCell widgetCell)) return false;
265 
266             if (widgetCell.getWidgetView().getDrawable() == null
267                     && widgetCell.getAppWidgetHostViewPreview() == null) {
268                 // The widget preview hasn't been loaded; so, we abort the drag.
269                 return false;
270             }
271 
272             final AppWidgetProviderInfo info = widgetCell.getWidgetItem().widgetInfo;
273             if (info == null || info.provider == null) {
274                 return false;
275             }
276 
277             View dragView = widgetCell.getDragAndDropView();
278             if (dragView == null) {
279                 return false;
280             }
281 
282             ClipData clipData = new ClipData(
283                     new ClipDescription(
284                             /* label= */ "", // not displayed anywhere; so, set to empty.
285                             new String[]{MIMETYPE_TEXT_INTENT}
286                     ),
287                     new ClipData.Item(new Intent()
288                             .putExtra(Intent.EXTRA_USER, info.getProfile())
289                             .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider))
290             );
291 
292             // Set result indicating activity was closed due a widget being dragged.
293             setResult(RESULT_OK, new Intent()
294                     .putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true));
295 
296             // DRAG_FLAG_GLOBAL permits dragging data beyond app window.
297             return dragView.startDragAndDrop(
298                     clipData,
299                     new View.DragShadowBuilder(dragView),
300                     /* myLocalState= */ null,
301                     View.DRAG_FLAG_GLOBAL
302             );
303         };
304     }
305 
306     /**
307      * Updates the model with widgets, applies filters and launches the widgets sheet once
308      * widgets are available
309      */
310     private void refreshAndBindWidgets() {
311         MODEL_EXECUTOR.execute(() -> {
312             mModel.update(null);
313 
314             StringCache stringCache = new StringCache();
315             stringCache.loadStrings(this);
316 
317             bindStringCache(stringCache);
318             bindWidgets(mModel.getWidgetsByPackageItemForPicker());
319             // Open sheet once widgets are available, so that it doesn't interrupt the open
320             // animation.
321             openWidgetsSheet();
322             if (mUiSurface != null) {
323                 mWidgetPredictionsRequester = new WidgetPredictionsRequester(
324                         getApplicationContext(), mUiSurface,
325                         mModel.getWidgetsByComponentKeyForPicker());
326                 mWidgetPredictionsRequester.request(mAddedWidgets, this);
327             }
328         });
329     }
330 
331     private void bindStringCache(final StringCache stringCache) {
332         MAIN_EXECUTOR.execute(() -> mStringCache = stringCache);
333     }
334 
335     private void bindWidgets(Map<PackageItemInfo, List<WidgetItem>> widgets) {
336         WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder(
337                 mApp.getContext());
338         final List<WidgetsListBaseEntry> allWidgets = builder.build(widgets, mNoShortcutsFilter);
339 
340         // Default list is shown if host has additionally enforced size filtering.
341         @Nullable Predicate<WidgetItem> defaultListFilter =
342                 hasHostSizeFilters() ? mHostSizeAndNoShortcutsFilter : null;
343 
344         MAIN_EXECUTOR.execute(() -> {
345             mWidgetPickerDataProvider.setHostSpecifiedDefaultWidgetsFilter(defaultListFilter);
346             mWidgetPickerDataProvider.setWidgets(allWidgets);
347         });
348     }
349 
350     private void openWidgetsSheet() {
351         MAIN_EXECUTOR.execute(() -> {
352             mWidgetSheet = WidgetsFullSheet.show(this, true);
353             mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
354             mWidgetSheet.disableNavBarScrim(true);
355             mWidgetSheet.addOnCloseListener(this::finish);
356         });
357     }
358 
359     @Override
360     public void onPredictionsAvailable(List<ItemInfo> recommendedWidgets) {
361         // Bind recommendations once picker has finished open animation.
362         MAIN_EXECUTOR.getHandler().postDelayed(
363                 () -> mWidgetPickerDataProvider.setWidgetRecommendations(recommendedWidgets),
364                 mDeviceProfile.bottomSheetOpenDuration);
365     }
366 
367     @Override
368     protected void onDestroy() {
369         super.onDestroy();
370         mWidgetPickerDataProvider.destroy();
371         if (mWidgetPredictionsRequester != null) {
372             mWidgetPredictionsRequester.clear();
373         }
374     }
375 
376     @Nullable
377     @Override
378     public StringCache getStringCache() {
379         return mStringCache;
380     }
381 
382     /**
383      * Animation callback for different predictive back animation states for the widget picker.
384      */
385     private class BackAnimationCallback extends FlingOnBackAnimationCallback {
386         @Nullable
387         OnBackAnimationCallback mActiveOnBackAnimationCallback;
388 
389         @Override
390         public void onBackStartedCompat(@NonNull BackEvent backEvent) {
391             if (mActiveOnBackAnimationCallback != null) {
392                 mActiveOnBackAnimationCallback.onBackCancelled();
393             }
394             if (mWidgetSheet != null) {
395                 mActiveOnBackAnimationCallback = mWidgetSheet;
396                 mActiveOnBackAnimationCallback.onBackStarted(backEvent);
397             }
398         }
399 
400         @Override
401         public void onBackInvokedCompat() {
402             if (mActiveOnBackAnimationCallback == null) {
403                 return;
404             }
405             mActiveOnBackAnimationCallback.onBackInvoked();
406             mActiveOnBackAnimationCallback = null;
407         }
408 
409         @Override
410         public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
411             if (mActiveOnBackAnimationCallback == null) {
412                 return;
413             }
414             mActiveOnBackAnimationCallback.onBackProgressed(backEvent);
415         }
416 
417         @Override
418         public void onBackCancelledCompat() {
419             if (mActiveOnBackAnimationCallback == null) {
420                 return;
421             }
422             mActiveOnBackAnimationCallback.onBackCancelled();
423             mActiveOnBackAnimationCallback = null;
424         }
425     }
426 
427     private boolean hasHostSizeFilters() {
428         // If optional filters such as size filter are present, we display them as default widgets.
429         return mDesiredWidgetWidth != 0 || mDesiredWidgetHeight != 0;
430     }
431 
432     private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget,
433             boolean applySizeFilter) {
434         final AppWidgetProviderInfo info = widget.widgetInfo;
435         if (info == null) {
436             return rejectWidget(widget, "shortcut");
437         }
438 
439         if (mFilteredUserIds.contains(widget.user.getIdentifier())) {
440             return rejectWidget(
441                     widget,
442                     "widget user: %d is being filtered",
443                     widget.user.getIdentifier());
444         }
445 
446         if (!mWidgetCategoryInclusionFilter.matches(info.widgetCategory)
447                 || !mWidgetCategoryExclusionFilter.matches(info.widgetCategory)) {
448             return rejectWidget(
449                     widget,
450                     "doesn't match category filter [inclusion=%d, exclusion=%d, widget=%d]",
451                     mWidgetCategoryInclusionFilter.getCategoryMask(),
452                     mWidgetCategoryExclusionFilter.getCategoryMask(),
453                     info.widgetCategory);
454         }
455 
456         if (applySizeFilter) {
457             if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
458                 // Accept the widget if the desired dimensions are unspecified.
459                 return acceptWidget(widget);
460             }
461 
462             final boolean isHorizontallyResizable =
463                     (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
464             if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
465                 if (info.maxResizeWidth > 0
466                         && info.maxResizeWidth >= info.minWidth
467                         && info.maxResizeWidth < mDesiredWidgetWidth) {
468                     return rejectWidget(
469                             widget,
470                             "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
471                             info.maxResizeWidth,
472                             mDesiredWidgetWidth);
473                 }
474 
475                 final int minWidth = min(info.minResizeWidth, info.minWidth);
476                 if (minWidth > mDesiredWidgetWidth) {
477                     return rejectWidget(
478                             widget,
479                             "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
480                             info.minWidth,
481                             info.minResizeWidth,
482                             mDesiredWidgetWidth);
483                 }
484             }
485 
486             final boolean isVerticallyResizable =
487                     (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
488             if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
489                 if (info.maxResizeHeight > 0
490                         && info.maxResizeHeight >= info.minHeight
491                         && info.maxResizeHeight < mDesiredWidgetHeight) {
492                     return rejectWidget(
493                             widget,
494                             "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
495                             info.maxResizeHeight,
496                             mDesiredWidgetHeight);
497                 }
498 
499                 final int minHeight = min(info.minResizeHeight, info.minHeight);
500                 if (minHeight > mDesiredWidgetHeight) {
501                     return rejectWidget(
502                             widget,
503                             "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
504                             info.minHeight,
505                             info.minResizeHeight,
506                             mDesiredWidgetHeight);
507                 }
508             }
509 
510             if (!isHorizontallyResizable || !isVerticallyResizable) {
511                 return rejectWidget(widget, "not resizeable");
512             }
513         }
514 
515         return acceptWidget(widget);
516     }
517 
518     private static WidgetAcceptabilityVerdict rejectWidget(
519             WidgetItem widget, String rejectionReason, Object... args) {
520         return new WidgetAcceptabilityVerdict(
521                 false,
522                 widget.widgetInfo != null
523                         ? widget.widgetInfo.provider.flattenToShortString()
524                         : widget.label,
525                 String.format(Locale.ENGLISH, rejectionReason, args));
526     }
527 
528     private static WidgetAcceptabilityVerdict acceptWidget(WidgetItem widget) {
529         return new WidgetAcceptabilityVerdict(
530                 true, widget.widgetInfo.provider.flattenToShortString(), "");
531     }
532 
533     private record WidgetAcceptabilityVerdict(
534             boolean isAcceptable, String widgetLabel, String reason) {
535         void maybeLogVerdict() {
536             // Only log a verdict if a reason is specified.
537             if (Log.isLoggable(TAG, Log.DEBUG) && !reason.isEmpty()) {
538                 Log.i(TAG, String.format(
539                         Locale.ENGLISH,
540                         "%s: %s because %s",
541                         widgetLabel,
542                         isAcceptable ? "accepted" : "rejected",
543                         reason));
544             }
545         }
546     }
547 }
548