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