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