• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 package com.android.launcher3.touch;
17 
18 import static com.android.launcher3.Launcher.REQUEST_BIND_PENDING_APPWIDGET;
19 import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET;
20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
21 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
22 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
23 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
24 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
25 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
26 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
27 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
28 
29 import android.app.AlertDialog;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.pm.LauncherApps;
33 import android.content.pm.PackageInstaller.SessionInfo;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.View;
37 import android.view.View.OnClickListener;
38 import android.widget.Toast;
39 
40 import com.android.launcher3.BubbleTextView;
41 import com.android.launcher3.Launcher;
42 import com.android.launcher3.LauncherSettings;
43 import com.android.launcher3.R;
44 import com.android.launcher3.Utilities;
45 import com.android.launcher3.apppairs.AppPairIcon;
46 import com.android.launcher3.folder.Folder;
47 import com.android.launcher3.folder.FolderIcon;
48 import com.android.launcher3.logging.InstanceId;
49 import com.android.launcher3.logging.InstanceIdSequence;
50 import com.android.launcher3.logging.StatsLogManager;
51 import com.android.launcher3.model.data.AppInfo;
52 import com.android.launcher3.model.data.FolderInfo;
53 import com.android.launcher3.model.data.ItemInfo;
54 import com.android.launcher3.model.data.ItemInfoWithIcon;
55 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
56 import com.android.launcher3.model.data.WorkspaceItemInfo;
57 import com.android.launcher3.pm.InstallSessionHelper;
58 import com.android.launcher3.shortcuts.ShortcutKey;
59 import com.android.launcher3.testing.TestLogging;
60 import com.android.launcher3.testing.shared.TestProtocol;
61 import com.android.launcher3.util.ItemInfoMatcher;
62 import com.android.launcher3.util.PackageManagerHelper;
63 import com.android.launcher3.views.FloatingIconView;
64 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
65 import com.android.launcher3.widget.PendingAppWidgetHostView;
66 import com.android.launcher3.widget.WidgetAddFlowHandler;
67 import com.android.launcher3.widget.WidgetManagerHelper;
68 
69 import java.util.Collections;
70 import java.util.concurrent.CompletableFuture;
71 import java.util.function.Consumer;
72 
73 /**
74  * Class for handling clicks on workspace and all-apps items
75  */
76 public class ItemClickHandler {
77 
78     private static final String TAG = ItemClickHandler.class.getSimpleName();
79 
80     /**
81      * Instance used for click handling on items
82      */
83     public static final OnClickListener INSTANCE = ItemClickHandler::onClick;
84 
onClick(View v)85     private static void onClick(View v) {
86         // Make sure that rogue clicks don't get through while allapps is launching, or after the
87         // view has detached (it's possible for this to happen if the view is removed mid touch).
88         if (v.getWindowToken() == null) return;
89 
90         Launcher launcher = Launcher.getLauncher(v.getContext());
91         if (!launcher.getWorkspace().isFinishedSwitchingState()) return;
92 
93         Object tag = v.getTag();
94         if (tag instanceof WorkspaceItemInfo) {
95             onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
96         } else if (tag instanceof FolderInfo) {
97             if (v instanceof FolderIcon) {
98                 onClickFolderIcon(v);
99             } else if (v instanceof AppPairIcon) {
100                 onClickAppPairIcon(v);
101             }
102         } else if (tag instanceof AppInfo) {
103             startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
104         } else if (tag instanceof LauncherAppWidgetInfo) {
105             if (v instanceof PendingAppWidgetHostView) {
106                 onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
107             }
108         } else if (tag instanceof ItemClickProxy) {
109             ((ItemClickProxy) tag).onItemClicked(v);
110         }
111     }
112 
113     /**
114      * Event handler for a folder icon click.
115      *
116      * @param v The view that was clicked. Must be an instance of {@link FolderIcon}.
117      */
onClickFolderIcon(View v)118     private static void onClickFolderIcon(View v) {
119         Folder folder = ((FolderIcon) v).getFolder();
120         if (!folder.isOpen() && !folder.isDestroyed()) {
121             // Open the requested folder
122             folder.animateOpen();
123             StatsLogManager.newInstance(v.getContext()).logger().withItemInfo(folder.mInfo)
124                     .log(LAUNCHER_FOLDER_OPEN);
125         }
126     }
127 
128     /**
129      * Event handler for an app pair icon click.
130      *
131      * @param v The view that was clicked. Must be an instance of {@link AppPairIcon}.
132      */
onClickAppPairIcon(View v)133     private static void onClickAppPairIcon(View v) {
134         Launcher launcher = Launcher.getLauncher(v.getContext());
135         FolderInfo folderInfo = ((AppPairIcon) v).getInfo();
136         launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1));
137     }
138 
139     /**
140      * Event handler for the app widget view which has not fully restored.
141      */
onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher)142     private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) {
143         if (launcher.getPackageManager().isSafeMode()) {
144             Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
145             return;
146         }
147 
148         final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
149         if (v.isReadyForClickSetup()) {
150             LauncherAppWidgetProviderInfo appWidgetInfo = new WidgetManagerHelper(launcher)
151                     .findProvider(info.providerName, info.user);
152             if (appWidgetInfo == null) {
153                 return;
154             }
155             WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo);
156 
157             if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
158                 if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
159                     // This should not happen, as we make sure that an Id is allocated during bind.
160                     return;
161                 }
162                 addFlowHandler.startBindFlow(launcher, info.appWidgetId, info,
163                         REQUEST_BIND_PENDING_APPWIDGET);
164             } else {
165                 addFlowHandler.startConfigActivity(launcher, info, REQUEST_RECONFIGURE_APPWIDGET);
166             }
167         } else {
168             final String packageName = info.providerName.getPackageName();
169             onClickPendingAppItem(v, launcher, packageName, info.installProgress >= 0);
170         }
171     }
172 
onClickPendingAppItem(View v, Launcher launcher, String packageName, boolean downloadStarted)173     private static void onClickPendingAppItem(View v, Launcher launcher, String packageName,
174             boolean downloadStarted) {
175         ItemInfo item = (ItemInfo) v.getTag();
176         CompletableFuture<SessionInfo> siFuture;
177         if (Utilities.ATLEAST_Q) {
178             siFuture = CompletableFuture.supplyAsync(() ->
179                     InstallSessionHelper.INSTANCE.get(launcher)
180                             .getActiveSessionInfo(item.user, packageName),
181                     UI_HELPER_EXECUTOR);
182         } else {
183             siFuture = CompletableFuture.completedFuture(null);
184         }
185         Consumer<SessionInfo> marketLaunchAction = sessionInfo -> {
186             if (sessionInfo != null && Utilities.ATLEAST_Q) {
187                 LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
188                 try {
189                     launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
190                             launcher.getActivityLaunchOptions(v, item).toBundle());
191                     return;
192                 } catch (Exception e) {
193                     Log.e(TAG, "Unable to launch market intent for package=" + packageName, e);
194                 }
195             }
196             // Fallback to using custom market intent.
197             Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName);
198             launcher.startActivitySafely(v, intent, item);
199         };
200 
201         if (downloadStarted) {
202             // If the download has started, simply direct to the market app.
203             siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR);
204             return;
205         }
206         new AlertDialog.Builder(launcher)
207                 .setTitle(R.string.abandoned_promises_title)
208                 .setMessage(R.string.abandoned_promise_explanation)
209                 .setPositiveButton(R.string.abandoned_search,
210                         (d, i) -> siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR))
211                 .setNeutralButton(R.string.abandoned_clean_this,
212                         (d, i) -> launcher.getWorkspace()
213                                 .persistRemoveItemsByMatcher(ItemInfoMatcher.ofPackages(
214                                         Collections.singleton(packageName), item.user),
215                                         "user explicitly removes the promise app icon"))
216                 .create().show();
217     }
218 
219     /**
220      * Handles clicking on a disabled shortcut
221      *
222      * @return true iff the disabled item click has been handled.
223      */
handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context)224     public static boolean handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context) {
225         final int disabledFlags = shortcut.runtimeStatusFlags
226                 & WorkspaceItemInfo.FLAG_DISABLED_MASK;
227         // Handle the case where the disabled reason is DISABLED_REASON_VERSION_LOWER.
228         // Show an AlertDialog for the user to choose either updating the app or cancel the launch.
229         if (maybeCreateAlertDialogForShortcut(shortcut, context)) {
230             return true;
231         }
232 
233         if ((disabledFlags
234                 & ~FLAG_DISABLED_SUSPENDED
235                 & ~FLAG_DISABLED_QUIET_USER) == 0) {
236             // If the app is only disabled because of the above flags, launch activity anyway.
237             // Framework will tell the user why the app is suspended.
238             return false;
239         } else {
240             if (!TextUtils.isEmpty(shortcut.disabledMessage)) {
241                 // Use a message specific to this shortcut, if it has one.
242                 Toast.makeText(context, shortcut.disabledMessage, Toast.LENGTH_SHORT).show();
243                 return true;
244             }
245             // Otherwise just use a generic error message.
246             int error = R.string.activity_not_available;
247             if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) {
248                 error = R.string.safemode_shortcut_error;
249             } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0
250                     || (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) {
251                 error = R.string.shortcut_not_available;
252             }
253             Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
254             return true;
255         }
256     }
257 
maybeCreateAlertDialogForShortcut(final WorkspaceItemInfo shortcut, Context context)258     private static boolean maybeCreateAlertDialogForShortcut(final WorkspaceItemInfo shortcut,
259             Context context) {
260         try {
261             final Launcher launcher = Launcher.getLauncher(context);
262             if (shortcut.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
263                     && shortcut.isDisabledVersionLower()) {
264                 final Intent marketIntent = shortcut.getMarketIntent(context);
265                 // No market intent means no target package for the shortcut, which should be an
266                 // issue. Falling back to showing toast messages.
267                 if (marketIntent == null) {
268                     return false;
269                 }
270 
271                 new AlertDialog.Builder(context)
272                         .setTitle(R.string.dialog_update_title)
273                         .setMessage(R.string.dialog_update_message)
274                         .setPositiveButton(R.string.dialog_update, (d, i) -> {
275                             // Direct the user to the play store to update the app
276                             context.startActivity(marketIntent);
277                         })
278                         .setNeutralButton(R.string.dialog_remove, (d, i) -> {
279                             // Remove the icon if launcher is successfully initialized
280                             launcher.getWorkspace().persistRemoveItemsByMatcher(ItemInfoMatcher
281                                     .ofShortcutKeys(Collections.singleton(ShortcutKey
282                                             .fromItemInfo(shortcut))),
283                                     "user explicitly removes disabled shortcut");
284                         })
285                         .create()
286                         .show();
287                 return true;
288             }
289         } catch (Exception e) {
290             Log.e(TAG, "Error creating alert dialog", e);
291         }
292 
293         return false;
294     }
295 
296     /**
297      * Event handler for an app shortcut click.
298      *
299      * @param v The view that was clicked. Must be a tagged with a {@link WorkspaceItemInfo}.
300      */
onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher)301     public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
302         if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) {
303             return;
304         }
305 
306         // Check for abandoned promise
307         if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) {
308             String packageName = shortcut.getIntent().getComponent() != null
309                     ? shortcut.getIntent().getComponent().getPackageName()
310                     : shortcut.getIntent().getPackage();
311             if (!TextUtils.isEmpty(packageName)) {
312                 onClickPendingAppItem(
313                         v,
314                         launcher,
315                         packageName,
316                         (shortcut.runtimeStatusFlags
317                                 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0);
318                 return;
319             }
320         }
321 
322         // Start activities
323         startAppShortcutOrInfoActivity(v, shortcut, launcher);
324     }
325 
startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher)326     private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
327         TestLogging.recordEvent(
328                 TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity");
329         Intent intent;
330         if (item instanceof ItemInfoWithIcon
331                 && (((ItemInfoWithIcon) item).runtimeStatusFlags
332                 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
333             ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item;
334             intent = new PackageManagerHelper(launcher)
335                     .getMarketIntent(appInfo.getTargetComponent().getPackageName());
336         } else {
337             intent = item.getIntent();
338         }
339         if (intent == null) {
340             throw new IllegalArgumentException("Input must have a valid intent");
341         }
342         if (item instanceof WorkspaceItemInfo) {
343             WorkspaceItemInfo si = (WorkspaceItemInfo) item;
344             if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)
345                     && Intent.ACTION_VIEW.equals(intent.getAction())) {
346                 // make a copy of the intent that has the package set to null
347                 // we do this because the platform sometimes disables instant
348                 // apps temporarily (triggered by the user) and fallbacks to the
349                 // web ui. This only works though if the package isn't set
350                 intent = new Intent(intent);
351                 intent.setPackage(null);
352             }
353             if ((si.options & WorkspaceItemInfo.FLAG_START_FOR_RESULT) != 0) {
354                 launcher.startActivityForResult(item.getIntent(), 0);
355                 InstanceId instanceId = new InstanceIdSequence().newInstanceId();
356                 launcher.logAppLaunch(launcher.getStatsLogManager(), item, instanceId);
357                 return;
358             }
359         }
360         if (v != null && launcher.supportsAdaptiveIconAnimation(v)
361                 && !item.shouldUseBackgroundAnimation()) {
362             // Preload the icon to reduce latency b/w swapping the floating view with the original.
363             FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
364         }
365         launcher.startActivitySafely(v, intent, item);
366     }
367 
368     /**
369      * Interface to indicate that an item will handle the click itself.
370      */
371     public interface ItemClickProxy {
372 
373         /**
374          * Called when the item is clicked
375          */
onItemClicked(View view)376         void onItemClicked(View view);
377     }
378 }
379