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