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