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