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.logging.LoggerUtils.newCommandAction; 20 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; 21 import static com.android.launcher3.logging.LoggerUtils.newItemTarget; 22 import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; 23 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 24 25 import android.annotation.TargetApi; 26 import android.app.ActivityOptions; 27 import android.appwidget.AppWidgetManager; 28 import android.content.ClipData; 29 import android.content.ClipDescription; 30 import android.content.Intent; 31 import android.content.pm.LauncherApps.PinItemRequest; 32 import android.graphics.Canvas; 33 import android.graphics.Point; 34 import android.graphics.PointF; 35 import android.graphics.Rect; 36 import android.os.AsyncTask; 37 import android.os.Build; 38 import android.os.Bundle; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.View.DragShadowBuilder; 42 import android.view.View.OnLongClickListener; 43 import android.view.View.OnTouchListener; 44 45 import com.android.launcher3.BaseActivity; 46 import com.android.launcher3.InstallShortcutReceiver; 47 import com.android.launcher3.InvariantDeviceProfile; 48 import com.android.launcher3.LauncherAppState; 49 import com.android.launcher3.LauncherAppWidgetHost; 50 import com.android.launcher3.LauncherAppWidgetProviderInfo; 51 import com.android.launcher3.R; 52 import com.android.launcher3.compat.AppWidgetManagerCompat; 53 import com.android.launcher3.compat.LauncherAppsCompatVO; 54 import com.android.launcher3.model.WidgetItem; 55 import com.android.launcher3.userevent.nano.LauncherLogProto.Action; 56 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 57 import com.android.launcher3.util.InstantAppResolver; 58 import com.android.launcher3.views.BaseDragLayer; 59 import com.android.launcher3.widget.PendingAddShortcutInfo; 60 import com.android.launcher3.widget.PendingAddWidgetInfo; 61 import com.android.launcher3.widget.WidgetHostViewLoader; 62 import com.android.launcher3.widget.WidgetImageView; 63 64 import java.util.function.Supplier; 65 66 @TargetApi(Build.VERSION_CODES.O) 67 public class AddItemActivity extends BaseActivity implements OnLongClickListener, OnTouchListener { 68 69 private static final int SHADOW_SIZE = 10; 70 71 private static final int REQUEST_BIND_APPWIDGET = 1; 72 private static final String STATE_EXTRA_WIDGET_ID = "state.widget.id"; 73 74 private final PointF mLastTouchPos = new PointF(); 75 76 private PinItemRequest mRequest; 77 private LauncherAppState mApp; 78 private InvariantDeviceProfile mIdp; 79 80 private LivePreviewWidgetCell mWidgetCell; 81 82 // Widget request specific options. 83 private LauncherAppWidgetHost mAppWidgetHost; 84 private AppWidgetManagerCompat mAppWidgetManager; 85 private int mPendingBindWidgetId; 86 private Bundle mWidgetOptions; 87 88 private boolean mFinishOnPause = false; 89 private InstantAppResolver mInstantAppResolver; 90 91 @Override onCreate(Bundle savedInstanceState)92 protected void onCreate(Bundle savedInstanceState) { 93 super.onCreate(savedInstanceState); 94 95 mRequest = LauncherAppsCompatVO.getPinItemRequest(getIntent()); 96 if (mRequest == null) { 97 finish(); 98 return; 99 } 100 101 mApp = LauncherAppState.getInstance(this); 102 mIdp = mApp.getInvariantDeviceProfile(); 103 mInstantAppResolver = InstantAppResolver.newInstance(this); 104 105 // Use the application context to get the device profile, as in multiwindow-mode, the 106 // confirmation activity might be rotated. 107 mDeviceProfile = mIdp.getDeviceProfile(getApplicationContext()); 108 109 setContentView(R.layout.add_item_confirmation_activity); 110 mWidgetCell = findViewById(R.id.widget_cell); 111 112 if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) { 113 setupShortcut(); 114 } else { 115 if (!setupWidget()) { 116 // TODO: show error toast? 117 finish(); 118 } 119 } 120 121 mWidgetCell.setOnTouchListener(this); 122 mWidgetCell.setOnLongClickListener(this); 123 124 // savedInstanceState is null when the activity is created the first time (i.e., avoids 125 // duplicate logging during rotation) 126 if (savedInstanceState == null) { 127 logCommand(Action.Command.ENTRY); 128 } 129 } 130 131 @Override onTouch(View view, MotionEvent motionEvent)132 public boolean onTouch(View view, MotionEvent motionEvent) { 133 mLastTouchPos.set(motionEvent.getX(), motionEvent.getY()); 134 return false; 135 } 136 137 @Override onLongClick(View view)138 public boolean onLongClick(View view) { 139 // Find the position of the preview relative to the touch location. 140 WidgetImageView img = mWidgetCell.getWidgetView(); 141 142 // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and 143 // we abort the drag. 144 if (img.getBitmap() == null) { 145 return false; 146 } 147 148 Rect bounds = img.getBitmapBounds(); 149 bounds.offset(img.getLeft() - (int) mLastTouchPos.x, img.getTop() - (int) mLastTouchPos.y); 150 151 // Start home and pass the draw request params 152 PinItemDragListener listener = new PinItemDragListener(mRequest, bounds, 153 img.getBitmap().getWidth(), img.getWidth()); 154 155 156 // Start a system drag and drop. We use a transparent bitmap as preview for system drag 157 // as the preview is handled internally by launcher. 158 ClipDescription description = new ClipDescription("", new String[]{listener.getMimeType()}); 159 ClipData data = new ClipData(description, new ClipData.Item("")); 160 view.startDragAndDrop(data, new DragShadowBuilder(view) { 161 162 @Override 163 public void onDrawShadow(Canvas canvas) { } 164 165 @Override 166 public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) { 167 outShadowSize.set(SHADOW_SIZE, SHADOW_SIZE); 168 outShadowTouchPoint.set(SHADOW_SIZE / 2, SHADOW_SIZE / 2); 169 } 170 }, null, View.DRAG_FLAG_GLOBAL); 171 172 173 Intent homeIntent = listener.addToIntent( 174 new Intent(Intent.ACTION_MAIN) 175 .addCategory(Intent.CATEGORY_HOME) 176 .setPackage(getPackageName()) 177 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 178 179 listener.initWhenReady(); 180 startActivity(homeIntent, 181 ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out).toBundle()); 182 mFinishOnPause = true; 183 return false; 184 } 185 186 @Override onPause()187 protected void onPause() { 188 super.onPause(); 189 if (mFinishOnPause) { 190 finish(); 191 } 192 } 193 setupShortcut()194 private void setupShortcut() { 195 PinShortcutRequestActivityInfo shortcutInfo = 196 new PinShortcutRequestActivityInfo(mRequest, this); 197 mWidgetCell.getWidgetView().setTag(new PendingAddShortcutInfo(shortcutInfo)); 198 applyWidgetItemAsync( 199 () -> new WidgetItem(shortcutInfo, mApp.getIconCache(), getPackageManager())); 200 } 201 setupWidget()202 private boolean setupWidget() { 203 LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo 204 .fromProviderInfo(this, mRequest.getAppWidgetProviderInfo(this)); 205 if (widgetInfo.minSpanX > mIdp.numColumns || widgetInfo.minSpanY > mIdp.numRows) { 206 // Cannot add widget 207 return false; 208 } 209 mWidgetCell.setPreview(PinItemDragListener.getPreview(mRequest)); 210 211 mAppWidgetManager = AppWidgetManagerCompat.getInstance(this); 212 mAppWidgetHost = new LauncherAppWidgetHost(this); 213 214 PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(widgetInfo); 215 pendingInfo.spanX = Math.min(mIdp.numColumns, widgetInfo.spanX); 216 pendingInfo.spanY = Math.min(mIdp.numRows, widgetInfo.spanY); 217 mWidgetOptions = WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo); 218 mWidgetCell.getWidgetView().setTag(pendingInfo); 219 220 applyWidgetItemAsync(() -> new WidgetItem(widgetInfo, mIdp, mApp.getIconCache())); 221 return true; 222 } 223 applyWidgetItemAsync(final Supplier<WidgetItem> itemProvider)224 private void applyWidgetItemAsync(final Supplier<WidgetItem> itemProvider) { 225 new AsyncTask<Void, Void, WidgetItem>() { 226 @Override 227 protected WidgetItem doInBackground(Void... voids) { 228 return itemProvider.get(); 229 } 230 231 @Override 232 protected void onPostExecute(WidgetItem item) { 233 mWidgetCell.applyFromCellItem(item, mApp.getWidgetCache()); 234 mWidgetCell.ensurePreview(); 235 } 236 }.executeOnExecutor(MODEL_EXECUTOR); 237 // TODO: Create a worker looper executor and reuse that everywhere. 238 } 239 240 /** 241 * Called when the cancel button is clicked. 242 */ onCancelClick(View v)243 public void onCancelClick(View v) { 244 logCommand(Action.Command.CANCEL); 245 finish(); 246 } 247 248 /** 249 * Called when place-automatically button is clicked. 250 */ onPlaceAutomaticallyClick(View v)251 public void onPlaceAutomaticallyClick(View v) { 252 if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) { 253 InstallShortcutReceiver.queueShortcut(mRequest.getShortcutInfo(), this); 254 logCommand(Action.Command.CONFIRM); 255 mRequest.accept(); 256 finish(); 257 return; 258 } 259 260 mPendingBindWidgetId = mAppWidgetHost.allocateAppWidgetId(); 261 boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( 262 mPendingBindWidgetId, mRequest.getAppWidgetProviderInfo(this), mWidgetOptions); 263 if (success) { 264 acceptWidget(mPendingBindWidgetId); 265 return; 266 } 267 268 // request bind widget 269 mAppWidgetHost.startBindFlow(this, mPendingBindWidgetId, 270 mRequest.getAppWidgetProviderInfo(this), REQUEST_BIND_APPWIDGET); 271 } 272 acceptWidget(int widgetId)273 private void acceptWidget(int widgetId) { 274 InstallShortcutReceiver.queueWidget(mRequest.getAppWidgetProviderInfo(this), widgetId, this); 275 mWidgetOptions.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); 276 mRequest.accept(mWidgetOptions); 277 logCommand(Action.Command.CONFIRM); 278 finish(); 279 } 280 281 @Override onBackPressed()282 public void onBackPressed() { 283 logCommand(Action.Command.BACK); 284 super.onBackPressed(); 285 } 286 287 @Override onActivityResult(int requestCode, int resultCode, Intent data)288 public void onActivityResult(int requestCode, int resultCode, Intent data) { 289 if (requestCode == REQUEST_BIND_APPWIDGET) { 290 int widgetId = data != null 291 ? data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mPendingBindWidgetId) 292 : mPendingBindWidgetId; 293 if (resultCode == RESULT_OK) { 294 acceptWidget(widgetId); 295 } else { 296 // Simply wait it out. 297 mAppWidgetHost.deleteAppWidgetId(widgetId); 298 mPendingBindWidgetId = -1; 299 } 300 return; 301 } 302 super.onActivityResult(requestCode, resultCode, data); 303 } 304 305 @Override onSaveInstanceState(Bundle outState)306 protected void onSaveInstanceState(Bundle outState) { 307 super.onSaveInstanceState(outState); 308 outState.putInt(STATE_EXTRA_WIDGET_ID, mPendingBindWidgetId); 309 } 310 311 @Override onRestoreInstanceState(Bundle savedInstanceState)312 protected void onRestoreInstanceState(Bundle savedInstanceState) { 313 super.onRestoreInstanceState(savedInstanceState); 314 mPendingBindWidgetId = savedInstanceState 315 .getInt(STATE_EXTRA_WIDGET_ID, mPendingBindWidgetId); 316 } 317 318 @Override getDragLayer()319 public BaseDragLayer getDragLayer() { 320 throw new UnsupportedOperationException(); 321 } 322 logCommand(int command)323 private void logCommand(int command) { 324 getUserEventDispatcher().dispatchUserEvent(newLauncherEvent( 325 newCommandAction(command), 326 newItemTarget(mWidgetCell.getWidgetView(), mInstantAppResolver), 327 newContainerTarget(ContainerType.PINITEM)), null); 328 } 329 } 330