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