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 android.appwidget.AppWidgetManager; 27 import android.appwidget.AppWidgetProviderInfo; 28 import android.content.ClipData; 29 import android.content.ClipDescription; 30 import android.content.Intent; 31 import android.os.Bundle; 32 import android.util.Log; 33 import android.view.View; 34 import android.view.WindowInsetsController; 35 import android.view.WindowManager; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 40 import com.android.launcher3.dragndrop.SimpleDragLayer; 41 import com.android.launcher3.model.WidgetItem; 42 import com.android.launcher3.model.WidgetPredictionsRequester; 43 import com.android.launcher3.model.WidgetsModel; 44 import com.android.launcher3.model.data.ItemInfo; 45 import com.android.launcher3.popup.PopupDataProvider; 46 import com.android.launcher3.util.PackageUserKey; 47 import com.android.launcher3.widget.BaseWidgetSheet; 48 import com.android.launcher3.widget.WidgetCell; 49 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 50 import com.android.launcher3.widget.model.WidgetsListHeaderEntry; 51 import com.android.launcher3.widget.picker.WidgetsFullSheet; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 import java.util.Locale; 56 import java.util.Map; 57 import java.util.regex.Pattern; 58 import java.util.stream.Collectors; 59 60 /** An Activity that can host Launcher's widget picker. */ 61 public class WidgetPickerActivity extends BaseActivity { 62 private static final String TAG = "WidgetPickerActivity"; 63 /** 64 * Name of the extra that indicates that a widget being dragged. 65 * 66 * <p>When set to "true" in the result of startActivityForResult, the client that launched the 67 * picker knows that activity was closed due to pending drag. 68 */ 69 private static final String EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"; 70 71 // Intent extras that specify the desired widget width and height. If these are not specified in 72 // the intent, then widgets will not be filtered for size. 73 private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"; 74 private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"; 75 /** 76 * Widgets currently added by the user in the UI surface. 77 * <p>This allows widget picker to exclude existing widgets from suggestions.</p> 78 */ 79 private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets"; 80 /** 81 * A unique identifier of the surface hosting the widgets; 82 * <p>"widgets" is reserved for home screen surface.</p> 83 * <p>"widgets_hub" is reserved for glanceable hub surface.</p> 84 */ 85 private static final String EXTRA_UI_SURFACE = "ui_surface"; 86 private static final Pattern UI_SURFACE_PATTERN = 87 Pattern.compile("^(widgets|widgets_hub)$"); 88 private SimpleDragLayer<WidgetPickerActivity> mDragLayer; 89 private WidgetsModel mModel; 90 private LauncherAppState mApp; 91 private WidgetPredictionsRequester mWidgetPredictionsRequester; 92 private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {}); 93 94 private int mDesiredWidgetWidth; 95 private int mDesiredWidgetHeight; 96 private int mWidgetCategoryFilter; 97 @Nullable 98 private String mUiSurface; 99 // Widgets existing on the host surface. 100 @NonNull 101 private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>(); 102 103 @Override onCreate(Bundle savedInstanceState)104 protected void onCreate(Bundle savedInstanceState) { 105 super.onCreate(savedInstanceState); 106 107 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); 108 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); 109 110 mApp = LauncherAppState.getInstance(this); 111 InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile(); 112 mDeviceProfile = idp.getDeviceProfile(this); 113 mModel = new WidgetsModel(); 114 115 setContentView(R.layout.widget_picker_activity); 116 mDragLayer = findViewById(R.id.drag_layer); 117 mDragLayer.recreateControllers(); 118 119 WindowInsetsController wc = mDragLayer.getWindowInsetsController(); 120 wc.hide(navigationBars() + statusBars()); 121 122 BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true); 123 widgetSheet.disableNavBarScrim(true); 124 widgetSheet.addOnCloseListener(this::finish); 125 126 parseIntentExtras(); 127 refreshAndBindWidgets(); 128 } 129 parseIntentExtras()130 private void parseIntentExtras() { 131 // A value of 0 for either size means that no filtering will occur in that dimension. If 132 // both values are 0, then no size filtering will occur. 133 mDesiredWidgetWidth = 134 getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_WIDTH, 0); 135 mDesiredWidgetHeight = 136 getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0); 137 138 // Defaults to '0' to indicate that there isn't a category filter. 139 mWidgetCategoryFilter = 140 getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0); 141 142 String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE); 143 if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) { 144 mUiSurface = uiSurfaceParam; 145 } 146 ArrayList<AppWidgetProviderInfo> addedWidgets = getIntent().getParcelableArrayListExtra( 147 EXTRA_ADDED_APP_WIDGETS, AppWidgetProviderInfo.class); 148 if (addedWidgets != null) { 149 mAddedWidgets = addedWidgets; 150 } 151 } 152 153 @NonNull 154 @Override getPopupDataProvider()155 public PopupDataProvider getPopupDataProvider() { 156 return mPopupDataProvider; 157 } 158 159 @Override getDragLayer()160 public SimpleDragLayer<WidgetPickerActivity> getDragLayer() { 161 return mDragLayer; 162 } 163 164 @Override getItemOnClickListener()165 public View.OnClickListener getItemOnClickListener() { 166 return v -> { 167 final AppWidgetProviderInfo info = 168 (v instanceof WidgetCell) ? ((WidgetCell) v).getWidgetItem().widgetInfo : null; 169 if (info == null || info.provider == null) { 170 return; 171 } 172 173 setResult(RESULT_OK, new Intent() 174 .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider) 175 .putExtra(Intent.EXTRA_USER, info.getProfile())); 176 177 finish(); 178 }; 179 } 180 181 @Override getAllAppsItemLongClickListener()182 public View.OnLongClickListener getAllAppsItemLongClickListener() { 183 return view -> { 184 if (!(view instanceof WidgetCell widgetCell)) return false; 185 186 if (widgetCell.getWidgetView().getDrawable() == null 187 && widgetCell.getAppWidgetHostViewPreview() == null) { 188 // The widget preview hasn't been loaded; so, we abort the drag. 189 return false; 190 } 191 192 final AppWidgetProviderInfo info = widgetCell.getWidgetItem().widgetInfo; 193 if (info == null || info.provider == null) { 194 return false; 195 } 196 197 View dragView = widgetCell.getDragAndDropView(); 198 if (dragView == null) { 199 return false; 200 } 201 202 ClipData clipData = new ClipData( 203 new ClipDescription( 204 /* label= */ "", // not displayed anywhere; so, set to empty. 205 new String[]{MIMETYPE_TEXT_INTENT} 206 ), 207 new ClipData.Item(new Intent() 208 .putExtra(Intent.EXTRA_USER, info.getProfile()) 209 .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider)) 210 ); 211 212 // Set result indicating activity was closed due a widget being dragged. 213 setResult(RESULT_OK, new Intent() 214 .putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true)); 215 216 // DRAG_FLAG_GLOBAL permits dragging data beyond app window. 217 return dragView.startDragAndDrop( 218 clipData, 219 new View.DragShadowBuilder(dragView), 220 /* myLocalState= */ null, 221 View.DRAG_FLAG_GLOBAL 222 ); 223 }; 224 } 225 226 /** Updates the model with widgets and provides them after applying the provided filter. */ 227 private void refreshAndBindWidgets() { 228 MODEL_EXECUTOR.execute(() -> { 229 LauncherAppState app = LauncherAppState.getInstance(this); 230 mModel.update(app, null); 231 final List<WidgetsListBaseEntry> allWidgets = 232 mModel.getFilteredWidgetsListForPicker( 233 app.getContext(), 234 /*widgetItemFilter=*/ widget -> { 235 final WidgetAcceptabilityVerdict verdict = 236 isWidgetAcceptable(widget); 237 verdict.maybeLogVerdict(); 238 return verdict.isAcceptable; 239 } 240 ); 241 bindWidgets(allWidgets); 242 if (mUiSurface != null) { 243 Map<PackageUserKey, List<WidgetItem>> allWidgetsMap = allWidgets.stream() 244 .filter(WidgetsListHeaderEntry.class::isInstance) 245 .collect(Collectors.toMap( 246 entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem), 247 entry -> entry.mWidgets) 248 ); 249 mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(), 250 mUiSurface, allWidgetsMap); 251 mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets); 252 } 253 }); 254 } 255 256 private void bindWidgets(List<WidgetsListBaseEntry> widgets) { 257 MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); 258 } 259 260 private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) { 261 MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets)); 262 } 263 264 @Override 265 protected void onDestroy() { 266 super.onDestroy(); 267 if (mWidgetPredictionsRequester != null) { 268 mWidgetPredictionsRequester.clear(); 269 } 270 } 271 272 private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) { 273 final AppWidgetProviderInfo info = widget.widgetInfo; 274 if (info == null) { 275 return rejectWidget(widget, "shortcut"); 276 } 277 278 if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) { 279 return rejectWidget( 280 widget, 281 "doesn't match category filter [filter=%d, widget=%d]", 282 mWidgetCategoryFilter, 283 info.widgetCategory); 284 } 285 286 if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) { 287 // Accept the widget if the desired dimensions are unspecified. 288 return acceptWidget(widget); 289 } 290 291 final boolean isHorizontallyResizable = 292 (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; 293 if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) { 294 if (info.maxResizeWidth > 0 295 && info.maxResizeWidth >= info.minWidth 296 && info.maxResizeWidth < mDesiredWidgetWidth) { 297 return rejectWidget( 298 widget, 299 "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]", 300 info.maxResizeWidth, 301 mDesiredWidgetWidth); 302 } 303 304 final int minWidth = Math.min(info.minResizeWidth, info.minWidth); 305 if (minWidth > mDesiredWidgetWidth) { 306 return rejectWidget( 307 widget, 308 "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]", 309 info.minWidth, 310 info.minResizeWidth, 311 mDesiredWidgetWidth); 312 } 313 } 314 315 final boolean isVerticallyResizable = 316 (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; 317 if (mDesiredWidgetHeight > 0 && isVerticallyResizable) { 318 if (info.maxResizeHeight > 0 319 && info.maxResizeHeight >= info.minHeight 320 && info.maxResizeHeight < mDesiredWidgetHeight) { 321 return rejectWidget( 322 widget, 323 "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]", 324 info.maxResizeHeight, 325 mDesiredWidgetHeight); 326 } 327 328 final int minHeight = Math.min(info.minResizeHeight, info.minHeight); 329 if (minHeight > mDesiredWidgetHeight) { 330 return rejectWidget( 331 widget, 332 "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]", 333 info.minHeight, 334 info.minResizeHeight, 335 mDesiredWidgetHeight); 336 } 337 } 338 339 if (!isHorizontallyResizable || !isVerticallyResizable) { 340 return rejectWidget(widget, "not resizeable"); 341 } 342 343 return acceptWidget(widget); 344 } 345 346 private static WidgetAcceptabilityVerdict rejectWidget( 347 WidgetItem widget, String rejectionReason, Object... args) { 348 return new WidgetAcceptabilityVerdict( 349 false, 350 widget.widgetInfo != null 351 ? widget.widgetInfo.provider.flattenToShortString() 352 : widget.label, 353 String.format(Locale.ENGLISH, rejectionReason, args)); 354 } 355 356 private static WidgetAcceptabilityVerdict acceptWidget(WidgetItem widget) { 357 return new WidgetAcceptabilityVerdict( 358 true, widget.widgetInfo.provider.flattenToShortString(), ""); 359 } 360 361 private record WidgetAcceptabilityVerdict( 362 boolean isAcceptable, String widgetLabel, String reason) { 363 void maybeLogVerdict() { 364 // Only log a verdict if a reason is specified. 365 if (Log.isLoggable(TAG, Log.DEBUG) && !reason.isEmpty()) { 366 Log.i(TAG, String.format( 367 Locale.ENGLISH, 368 "%s: %s because %s", 369 widgetLabel, 370 isAcceptable ? "accepted" : "rejected", 371 reason)); 372 } 373 } 374 } 375 } 376