1 /* 2 * Copyright (C) 2017 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.dragndrop; 18 19 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PIN_WIDGETS; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_BACK; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_CANCELLED; 22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_START; 25 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 26 import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY; 27 28 import android.annotation.TargetApi; 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.content.pm.ApplicationInfo; 35 import android.content.pm.LauncherApps.PinItemRequest; 36 import android.content.pm.ShortcutInfo; 37 import android.content.res.Configuration; 38 import android.graphics.Canvas; 39 import android.graphics.Point; 40 import android.graphics.PointF; 41 import android.graphics.Rect; 42 import android.os.AsyncTask; 43 import android.os.Build; 44 import android.os.Bundle; 45 import android.text.TextUtils; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.View.DragShadowBuilder; 49 import android.view.View.OnLongClickListener; 50 import android.view.View.OnTouchListener; 51 import android.view.WindowManager; 52 import android.view.accessibility.AccessibilityEvent; 53 import android.view.accessibility.AccessibilityManager; 54 import android.widget.TextView; 55 56 import androidx.annotation.Nullable; 57 58 import com.android.launcher3.BaseActivity; 59 import com.android.launcher3.InvariantDeviceProfile; 60 import com.android.launcher3.Launcher; 61 import com.android.launcher3.LauncherAppState; 62 import com.android.launcher3.R; 63 import com.android.launcher3.logging.StatsLogManager; 64 import com.android.launcher3.model.ItemInstallQueue; 65 import com.android.launcher3.model.WidgetItem; 66 import com.android.launcher3.model.WidgetsModel; 67 import com.android.launcher3.model.data.ItemInfo; 68 import com.android.launcher3.model.data.PackageItemInfo; 69 import com.android.launcher3.pm.PinRequestHelper; 70 import com.android.launcher3.util.ApiWrapper; 71 import com.android.launcher3.util.PackageManagerHelper; 72 import com.android.launcher3.util.SystemUiController; 73 import com.android.launcher3.views.AbstractSlideInView; 74 import com.android.launcher3.views.BaseDragLayer; 75 import com.android.launcher3.widget.AddItemWidgetsBottomSheet; 76 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 77 import com.android.launcher3.widget.LauncherWidgetHolder; 78 import com.android.launcher3.widget.NavigableAppWidgetHostView; 79 import com.android.launcher3.widget.PendingAddShortcutInfo; 80 import com.android.launcher3.widget.PendingAddWidgetInfo; 81 import com.android.launcher3.widget.WidgetCell; 82 import com.android.launcher3.widget.WidgetCellPreview; 83 import com.android.launcher3.widget.WidgetImageView; 84 import com.android.launcher3.widget.WidgetManagerHelper; 85 import com.android.launcher3.widget.WidgetSections; 86 87 import java.util.function.Supplier; 88 89 /** 90 * Activity to show pin widget dialog. 91 */ 92 @TargetApi(Build.VERSION_CODES.O) 93 public class AddItemActivity extends BaseActivity 94 implements OnLongClickListener, OnTouchListener, AbstractSlideInView.OnCloseListener, 95 WidgetCell.PreviewReadyListener { 96 97 private static final int SHADOW_SIZE = 10; 98 99 private static final int REQUEST_BIND_APPWIDGET = 1; 100 private static final String STATE_EXTRA_WIDGET_ID = "state.widget.id"; 101 102 private final PointF mLastTouchPos = new PointF(); 103 104 private PinItemRequest mRequest; 105 private LauncherAppState mApp; 106 private InvariantDeviceProfile mIdp; 107 private BaseDragLayer<AddItemActivity> mDragLayer; 108 private AddItemWidgetsBottomSheet mSlideInView; 109 private AccessibilityManager mAccessibilityManager; 110 111 private WidgetCell mWidgetCell; 112 113 // Widget request specific options. 114 @Nullable 115 private LauncherWidgetHolder mAppWidgetHolder = null; 116 private WidgetManagerHelper mAppWidgetManager; 117 private int mPendingBindWidgetId; 118 private Bundle mWidgetOptions; 119 120 private boolean mFinishOnPause = false; 121 122 @Override onCreate(Bundle savedInstanceState)123 protected void onCreate(Bundle savedInstanceState) { 124 super.onCreate(savedInstanceState); 125 126 mRequest = PinRequestHelper.getPinItemRequest(getIntent()); 127 if (mRequest == null) { 128 finish(); 129 return; 130 } 131 132 mApp = LauncherAppState.getInstance(this); 133 mIdp = mApp.getInvariantDeviceProfile(); 134 135 // Use the application context to get the device profile, as in multiwindow-mode, the 136 // confirmation activity might be rotated. 137 mDeviceProfile = mIdp.getDeviceProfile(getApplicationContext()); 138 139 setContentView(R.layout.add_item_confirmation_activity); 140 // Set flag to allow activity to draw over navigation and status bar. 141 getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 142 WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); 143 mDragLayer = findViewById(R.id.add_item_drag_layer); 144 mDragLayer.recreateControllers(); 145 mWidgetCell = findViewById(R.id.widget_cell); 146 mWidgetCell.addPreviewReadyListener(this); 147 mAccessibilityManager = 148 getApplicationContext().getSystemService(AccessibilityManager.class); 149 150 final PackageItemInfo targetApp; 151 switch (mRequest.getRequestType()) { 152 case PinItemRequest.REQUEST_TYPE_SHORTCUT: 153 targetApp = setupShortcut(); 154 break; 155 case PinItemRequest.REQUEST_TYPE_APPWIDGET: 156 targetApp = setupWidget(); 157 break; 158 default: 159 targetApp = null; 160 break; 161 } 162 if (targetApp == null) { 163 // TODO: show error toast? 164 finish(); 165 return; 166 } 167 ApplicationInfo info = PackageManagerHelper.INSTANCE.get(this) 168 .getApplicationInfo(targetApp.packageName, targetApp.user, 0); 169 if (info == null) { 170 finish(); 171 return; 172 } 173 174 WidgetCellPreview previewContainer = mWidgetCell.findViewById( 175 R.id.widget_preview_container); 176 previewContainer.setOnTouchListener(this); 177 previewContainer.setOnLongClickListener(this); 178 179 // savedInstanceState is null when the activity is created the first time (i.e., avoids 180 // duplicate logging during rotation) 181 if (savedInstanceState == null) { 182 logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_START); 183 } 184 185 // Set the label synchronously instead of via IconCache as this is the first thing 186 // user sees 187 TextView widgetAppName = findViewById(R.id.widget_appName); 188 WidgetSections.WidgetSection section = targetApp.widgetCategory == NO_CATEGORY ? null 189 : WidgetSections.getWidgetSections(this).get(targetApp.widgetCategory); 190 widgetAppName.setText(section == null ? info.loadLabel(getPackageManager()) 191 : getString(section.mSectionTitle)); 192 193 mSlideInView = findViewById(R.id.add_item_bottom_sheet); 194 mSlideInView.addOnCloseListener(this); 195 mSlideInView.show(); 196 setupNavBarColor(); 197 } 198 199 @Override onTouch(View view, MotionEvent motionEvent)200 public boolean onTouch(View view, MotionEvent motionEvent) { 201 mLastTouchPos.set(motionEvent.getX(), motionEvent.getY()); 202 return false; 203 } 204 205 @Override onLongClick(View view)206 public boolean onLongClick(View view) { 207 // Find the position of the preview relative to the touch location. 208 WidgetImageView img = mWidgetCell.getWidgetView(); 209 NavigableAppWidgetHostView appWidgetHostView = mWidgetCell.getAppWidgetHostViewPreview(); 210 211 // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and 212 // we abort the drag. 213 if (img.getDrawable() == null && appWidgetHostView == null) { 214 return false; 215 } 216 217 final Rect bounds; 218 // Start home and pass the draw request params 219 final PinItemDragListener listener; 220 if (appWidgetHostView != null) { 221 bounds = new Rect(); 222 appWidgetHostView.getSourceVisualDragBounds(bounds); 223 float appWidgetHostViewScale = mWidgetCell.getAppWidgetHostViewScale(); 224 int xOffset = 225 appWidgetHostView.getLeft() - (int) (mLastTouchPos.x * appWidgetHostViewScale); 226 int yOffset = 227 appWidgetHostView.getTop() - (int) (mLastTouchPos.y * appWidgetHostViewScale); 228 bounds.offset(xOffset, yOffset); 229 listener = new PinItemDragListener( 230 mRequest, 231 bounds, 232 appWidgetHostView.getMeasuredWidth(), 233 appWidgetHostView.getMeasuredWidth(), 234 appWidgetHostViewScale); 235 } else { 236 bounds = img.getBitmapBounds(); 237 bounds.offset(img.getLeft() - (int) mLastTouchPos.x, 238 img.getTop() - (int) mLastTouchPos.y); 239 listener = new PinItemDragListener(mRequest, bounds, 240 img.getDrawable().getIntrinsicWidth(), img.getWidth()); 241 } 242 243 // Start a system drag and drop. We use a transparent bitmap as preview for system drag 244 // as the preview is handled internally by launcher. 245 ClipDescription description = new ClipDescription("", new String[]{listener.getMimeType()}); 246 ClipData data = new ClipData(description, new ClipData.Item("")); 247 view.startDragAndDrop(data, new DragShadowBuilder(view) { 248 249 @Override 250 public void onDrawShadow(Canvas canvas) { } 251 252 @Override 253 public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) { 254 outShadowSize.set(SHADOW_SIZE, SHADOW_SIZE); 255 outShadowTouchPoint.set(SHADOW_SIZE / 2, SHADOW_SIZE / 2); 256 } 257 }, null, View.DRAG_FLAG_GLOBAL); 258 259 Intent homeIntent = new Intent(Intent.ACTION_MAIN) 260 .addCategory(Intent.CATEGORY_HOME) 261 .setPackage(getPackageName()) 262 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 263 Launcher.ACTIVITY_TRACKER.registerCallback(listener, "AddItemActivity.onLongClick"); 264 startActivity(homeIntent, 265 ApiWrapper.INSTANCE.get(this).createFadeOutAnimOptions().toBundle()); 266 logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED); 267 mFinishOnPause = true; 268 return false; 269 } 270 271 @Override onPause()272 protected void onPause() { 273 super.onPause(); 274 if (mFinishOnPause) { 275 finish(); 276 } 277 } 278 setupShortcut()279 private PackageItemInfo setupShortcut() { 280 PinShortcutRequestActivityInfo shortcutInfo = 281 new PinShortcutRequestActivityInfo(mRequest, this); 282 mWidgetCell.getWidgetView().setTag(new PendingAddShortcutInfo(shortcutInfo)); 283 applyWidgetItemAsync( 284 () -> new WidgetItem(shortcutInfo, mApp.getIconCache(), getPackageManager())); 285 return new PackageItemInfo(mRequest.getShortcutInfo().getPackage(), 286 mRequest.getShortcutInfo().getUserHandle()); 287 } 288 setupWidget()289 private PackageItemInfo setupWidget() { 290 final LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo 291 .fromProviderInfo(this, mRequest.getAppWidgetProviderInfo(this)); 292 if (widgetInfo.minSpanX > mIdp.numColumns || widgetInfo.minSpanY > mIdp.numRows) { 293 // Cannot add widget 294 return null; 295 } 296 mWidgetCell.setRemoteViewsPreview(PinItemDragListener.getPreview(mRequest)); 297 298 mAppWidgetManager = new WidgetManagerHelper(this); 299 mAppWidgetHolder = LauncherWidgetHolder.newInstance(this); 300 301 PendingAddWidgetInfo pendingInfo = 302 new PendingAddWidgetInfo(widgetInfo, CONTAINER_PIN_WIDGETS); 303 pendingInfo.spanX = Math.min(mIdp.numColumns, widgetInfo.spanX); 304 pendingInfo.spanY = Math.min(mIdp.numRows, widgetInfo.spanY); 305 mWidgetOptions = pendingInfo.getDefaultSizeOptions(this); 306 mWidgetCell.getWidgetView().setTag(pendingInfo); 307 308 applyWidgetItemAsync(() -> new WidgetItem( 309 widgetInfo, mIdp, mApp.getIconCache(), mApp.getContext())); 310 return WidgetsModel.newPendingItemInfo(this, widgetInfo.getComponent(), 311 widgetInfo.getUser()); 312 } 313 applyWidgetItemAsync(final Supplier<WidgetItem> itemProvider)314 private void applyWidgetItemAsync(final Supplier<WidgetItem> itemProvider) { 315 new AsyncTask<Void, Void, WidgetItem>() { 316 @Override 317 protected WidgetItem doInBackground(Void... voids) { 318 return itemProvider.get(); 319 } 320 321 @Override 322 protected void onPostExecute(WidgetItem item) { 323 mWidgetCell.applyFromCellItem(item); 324 } 325 }.executeOnExecutor(MODEL_EXECUTOR); 326 // TODO: Create a worker looper executor and reuse that everywhere. 327 } 328 329 /** 330 * Called when the cancel button is clicked. 331 */ onCancelClick(View v)332 public void onCancelClick(View v) { 333 logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_CANCELLED); 334 mSlideInView.close(/* animate= */ true); 335 } 336 337 /** 338 * Called when place-automatically button is clicked. 339 */ onPlaceAutomaticallyClick(View v)340 public void onPlaceAutomaticallyClick(View v) { 341 if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) { 342 ShortcutInfo shortcutInfo = mRequest.getShortcutInfo(); 343 ItemInstallQueue.INSTANCE.get(this).queueItem(shortcutInfo); 344 logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY); 345 mRequest.accept(); 346 CharSequence label = shortcutInfo.getLongLabel(); 347 if (TextUtils.isEmpty(label)) { 348 label = shortcutInfo.getShortLabel(); 349 } 350 sendWidgetAddedToScreenAccessibilityEvent(label.toString()); 351 mSlideInView.close(/* animate= */ true); 352 return; 353 } 354 355 mPendingBindWidgetId = mAppWidgetHolder.allocateAppWidgetId(); 356 AppWidgetProviderInfo widgetProviderInfo = mRequest.getAppWidgetProviderInfo(this); 357 boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( 358 mPendingBindWidgetId, widgetProviderInfo, mWidgetOptions); 359 if (success) { 360 sendWidgetAddedToScreenAccessibilityEvent(widgetProviderInfo.label); 361 acceptWidget(mPendingBindWidgetId); 362 return; 363 } 364 365 // request bind widget 366 mAppWidgetHolder.startBindFlow(this, mPendingBindWidgetId, 367 mRequest.getAppWidgetProviderInfo(this), REQUEST_BIND_APPWIDGET); 368 } 369 acceptWidget(int widgetId)370 private void acceptWidget(int widgetId) { 371 ItemInstallQueue.INSTANCE.get(this) 372 .queueItem(mRequest.getAppWidgetProviderInfo(this), widgetId); 373 mWidgetOptions.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); 374 mRequest.accept(mWidgetOptions); 375 logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY); 376 mSlideInView.close(/* animate= */ true); 377 } 378 379 @Override onDestroy()380 public void onDestroy() { 381 super.onDestroy(); 382 if (mAppWidgetHolder != null) { 383 // Necessary to destroy the holder to free up possible activity context 384 mAppWidgetHolder.destroy(); 385 } 386 } 387 388 @Override onBackPressed()389 public void onBackPressed() { 390 logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_BACK); 391 mSlideInView.close(/* animate= */ true); 392 } 393 394 @Override onActivityResult(int requestCode, int resultCode, Intent data)395 public void onActivityResult(int requestCode, int resultCode, Intent data) { 396 if (requestCode == REQUEST_BIND_APPWIDGET) { 397 int widgetId = data != null 398 ? data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mPendingBindWidgetId) 399 : mPendingBindWidgetId; 400 if (resultCode == RESULT_OK) { 401 acceptWidget(widgetId); 402 } else { 403 // Simply wait it out. 404 mAppWidgetHolder.deleteAppWidgetId(widgetId); 405 mPendingBindWidgetId = -1; 406 } 407 return; 408 } 409 super.onActivityResult(requestCode, resultCode, data); 410 } 411 412 @Override onSaveInstanceState(Bundle outState)413 protected void onSaveInstanceState(Bundle outState) { 414 super.onSaveInstanceState(outState); 415 outState.putInt(STATE_EXTRA_WIDGET_ID, mPendingBindWidgetId); 416 } 417 418 @Override onRestoreInstanceState(Bundle savedInstanceState)419 protected void onRestoreInstanceState(Bundle savedInstanceState) { 420 super.onRestoreInstanceState(savedInstanceState); 421 mPendingBindWidgetId = savedInstanceState 422 .getInt(STATE_EXTRA_WIDGET_ID, mPendingBindWidgetId); 423 } 424 425 @Override getDragLayer()426 public BaseDragLayer getDragLayer() { 427 return mDragLayer; 428 } 429 430 @Override onSlideInViewClosed()431 public void onSlideInViewClosed() { 432 finish(); 433 } 434 setupNavBarColor()435 protected void setupNavBarColor() { 436 boolean isSheetDark = (getApplicationContext().getResources().getConfiguration().uiMode 437 & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; 438 getSystemUiController().updateUiState( 439 SystemUiController.UI_STATE_BASE_WINDOW, 440 isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV); 441 } 442 sendWidgetAddedToScreenAccessibilityEvent(String widgetName)443 private void sendWidgetAddedToScreenAccessibilityEvent(String widgetName) { 444 if (mAccessibilityManager.isEnabled()) { 445 AccessibilityEvent event = 446 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT); 447 event.setContentDescription( 448 getApplicationContext().getResources().getString( 449 R.string.added_to_home_screen_accessibility_text, widgetName)); 450 mAccessibilityManager.sendAccessibilityEvent(event); 451 } 452 } 453 logCommand(StatsLogManager.EventEnum command)454 private void logCommand(StatsLogManager.EventEnum command) { 455 getStatsLogManager().logger() 456 .withItemInfo((ItemInfo) mWidgetCell.getWidgetView().getTag()) 457 .log(command); 458 } 459 460 @Override onPreviewAvailable()461 public void onPreviewAvailable() { 462 // Set the preview height based on "the only" widget's preview. 463 mWidgetCell.setParentAlignedPreviewHeight(mWidgetCell.getPreviewContentHeight()); 464 mWidgetCell.post(mWidgetCell::requestLayout); 465 } 466 } 467