• 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_APP_LAUNCH_TAP;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
22 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
23 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
24 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
25 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
26 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
27 
28 import android.app.AlertDialog;
29 import android.app.PendingIntent;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentSender;
33 import android.content.pm.LauncherApps;
34 import android.content.pm.PackageInstaller.SessionInfo;
35 import android.os.Process;
36 import android.os.UserHandle;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.view.View;
40 import android.view.View.OnClickListener;
41 import android.widget.Toast;
42 
43 import com.android.launcher3.BubbleTextView;
44 import com.android.launcher3.Launcher;
45 import com.android.launcher3.R;
46 import com.android.launcher3.Utilities;
47 import com.android.launcher3.folder.Folder;
48 import com.android.launcher3.folder.FolderIcon;
49 import com.android.launcher3.logging.StatsLogManager;
50 import com.android.launcher3.model.data.AppInfo;
51 import com.android.launcher3.model.data.FolderInfo;
52 import com.android.launcher3.model.data.ItemInfo;
53 import com.android.launcher3.model.data.ItemInfoWithIcon;
54 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
55 import com.android.launcher3.model.data.SearchActionItemInfo;
56 import com.android.launcher3.model.data.WorkspaceItemInfo;
57 import com.android.launcher3.pm.InstallSessionHelper;
58 import com.android.launcher3.testing.TestLogging;
59 import com.android.launcher3.testing.TestProtocol;
60 import com.android.launcher3.util.PackageManagerHelper;
61 import com.android.launcher3.views.FloatingIconView;
62 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
63 import com.android.launcher3.widget.PendingAppWidgetHostView;
64 import com.android.launcher3.widget.WidgetAddFlowHandler;
65 import com.android.launcher3.widget.WidgetManagerHelper;
66 
67 /**
68  * Class for handling clicks on workspace and all-apps items
69  */
70 public class ItemClickHandler {
71 
72     private static final String TAG = ItemClickHandler.class.getSimpleName();
73 
74     /**
75      * Instance used for click handling on items
76      */
77     public static final OnClickListener INSTANCE = ItemClickHandler::onClick;
78 
onClick(View v)79     private static void onClick(View v) {
80         // Make sure that rogue clicks don't get through while allapps is launching, or after the
81         // view has detached (it's possible for this to happen if the view is removed mid touch).
82         if (v.getWindowToken() == null) return;
83 
84         Launcher launcher = Launcher.getLauncher(v.getContext());
85         if (!launcher.getWorkspace().isFinishedSwitchingState()) return;
86 
87         Object tag = v.getTag();
88         if (tag instanceof WorkspaceItemInfo) {
89             onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
90         } else if (tag instanceof FolderInfo) {
91             if (v instanceof FolderIcon) {
92                 onClickFolderIcon(v);
93             }
94         } else if (tag instanceof AppInfo) {
95             startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher
96             );
97         } else if (tag instanceof LauncherAppWidgetInfo) {
98             if (v instanceof PendingAppWidgetHostView) {
99                 onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
100             }
101         } else if (tag instanceof SearchActionItemInfo) {
102             onClickSearchAction(launcher, (SearchActionItemInfo) tag);
103         }
104     }
105 
106     /**
107      * Event handler for a folder icon click.
108      *
109      * @param v The view that was clicked. Must be an instance of {@link FolderIcon}.
110      */
onClickFolderIcon(View v)111     private static void onClickFolderIcon(View v) {
112         Folder folder = ((FolderIcon) v).getFolder();
113         if (!folder.isOpen() && !folder.isDestroyed()) {
114             // Open the requested folder
115             folder.animateOpen();
116             StatsLogManager.newInstance(v.getContext()).logger().withItemInfo(folder.mInfo)
117                     .log(LAUNCHER_FOLDER_OPEN);
118         }
119     }
120 
121     /**
122      * Event handler for the app widget view which has not fully restored.
123      */
onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher)124     private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) {
125         if (launcher.getPackageManager().isSafeMode()) {
126             Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
127             return;
128         }
129 
130         final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
131         if (v.isReadyForClickSetup()) {
132             LauncherAppWidgetProviderInfo appWidgetInfo = new WidgetManagerHelper(launcher)
133                     .findProvider(info.providerName, info.user);
134             if (appWidgetInfo == null) {
135                 return;
136             }
137             WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo);
138 
139             if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
140                 if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
141                     // This should not happen, as we make sure that an Id is allocated during bind.
142                     return;
143                 }
144                 addFlowHandler.startBindFlow(launcher, info.appWidgetId, info,
145                         REQUEST_BIND_PENDING_APPWIDGET);
146             } else {
147                 addFlowHandler.startConfigActivity(launcher, info, REQUEST_RECONFIGURE_APPWIDGET);
148             }
149         } else {
150             final String packageName = info.providerName.getPackageName();
151             onClickPendingAppItem(v, launcher, packageName, info.installProgress >= 0);
152         }
153     }
154 
onClickPendingAppItem(View v, Launcher launcher, String packageName, boolean downloadStarted)155     private static void onClickPendingAppItem(View v, Launcher launcher, String packageName,
156             boolean downloadStarted) {
157         if (downloadStarted) {
158             // If the download has started, simply direct to the market app.
159             startMarketIntentForPackage(v, launcher, packageName);
160             return;
161         }
162         UserHandle user = v.getTag() instanceof ItemInfo
163                 ? ((ItemInfo) v.getTag()).user : Process.myUserHandle();
164         new AlertDialog.Builder(launcher)
165                 .setTitle(R.string.abandoned_promises_title)
166                 .setMessage(R.string.abandoned_promise_explanation)
167                 .setPositiveButton(R.string.abandoned_search,
168                         (d, i) -> startMarketIntentForPackage(v, launcher, packageName))
169                 .setNeutralButton(R.string.abandoned_clean_this,
170                         (d, i) -> launcher.getWorkspace()
171                                 .removeAbandonedPromise(packageName, user))
172                 .create().show();
173     }
174 
startMarketIntentForPackage(View v, Launcher launcher, String packageName)175     private static void startMarketIntentForPackage(View v, Launcher launcher, String packageName) {
176         ItemInfo item = (ItemInfo) v.getTag();
177         if (Utilities.ATLEAST_Q) {
178             SessionInfo sessionInfo = InstallSessionHelper.INSTANCE.get(launcher)
179                     .getActiveSessionInfo(item.user, packageName);
180             if (sessionInfo != null) {
181                 LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
182                 try {
183                     launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
184                             launcher.getActivityLaunchOptions(v, item).toBundle());
185                     return;
186                 } catch (Exception e) {
187                     Log.e(TAG, "Unable to launch market intent for package=" + packageName, e);
188                 }
189             }
190         }
191 
192         // Fallback to using custom market intent.
193         Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName);
194         launcher.startActivitySafely(v, intent, item);
195     }
196 
197     /**
198      * Handles clicking on a disabled shortcut
199      *
200      * @return true iff the disabled item click has been handled.
201      */
handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context)202     public static boolean handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context) {
203         final int disabledFlags = shortcut.runtimeStatusFlags
204                 & WorkspaceItemInfo.FLAG_DISABLED_MASK;
205         if ((disabledFlags
206                 & ~FLAG_DISABLED_SUSPENDED
207                 & ~FLAG_DISABLED_QUIET_USER) == 0) {
208             // If the app is only disabled because of the above flags, launch activity anyway.
209             // Framework will tell the user why the app is suspended.
210             return false;
211         } else {
212             if (!TextUtils.isEmpty(shortcut.disabledMessage)) {
213                 // Use a message specific to this shortcut, if it has one.
214                 Toast.makeText(context, shortcut.disabledMessage, Toast.LENGTH_SHORT).show();
215                 return true;
216             }
217             // Otherwise just use a generic error message.
218             int error = R.string.activity_not_available;
219             if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) {
220                 error = R.string.safemode_shortcut_error;
221             } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0
222                     || (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) {
223                 error = R.string.shortcut_not_available;
224             }
225             Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
226             return true;
227         }
228     }
229 
230     /**
231      * Event handler for an app shortcut click.
232      *
233      * @param v The view that was clicked. Must be a tagged with a {@link WorkspaceItemInfo}.
234      */
onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher)235     public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
236         if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) {
237             return;
238         }
239 
240         // Check for abandoned promise
241         if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) {
242             String packageName = shortcut.getIntent().getComponent() != null
243                     ? shortcut.getIntent().getComponent().getPackageName()
244                     : shortcut.getIntent().getPackage();
245             if (!TextUtils.isEmpty(packageName)) {
246                 onClickPendingAppItem(
247                         v,
248                         launcher,
249                         packageName,
250                         (shortcut.runtimeStatusFlags
251                                 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0);
252                 return;
253             }
254         }
255 
256         // Start activities
257         startAppShortcutOrInfoActivity(v, shortcut, launcher);
258     }
259 
260     /**
261      * Event handler for a {@link SearchActionItemInfo} click
262      */
onClickSearchAction(Launcher launcher, SearchActionItemInfo itemInfo)263     public static void onClickSearchAction(Launcher launcher, SearchActionItemInfo itemInfo) {
264         if (itemInfo.getIntent() != null) {
265             if (itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START_FOR_RESULT)) {
266                 launcher.startActivityForResult(itemInfo.getIntent(), 0);
267             } else {
268                 launcher.startActivity(itemInfo.getIntent());
269             }
270         } else if (itemInfo.getPendingIntent() != null) {
271             try {
272                 PendingIntent pendingIntent = itemInfo.getPendingIntent();
273                 if (!itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START)) {
274                     pendingIntent.send();
275                 } else if (itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START_FOR_RESULT)) {
276                     launcher.startIntentSenderForResult(pendingIntent.getIntentSender(), 0, null, 0,
277                             0, 0);
278                 } else {
279                     launcher.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
280                 }
281             } catch (PendingIntent.CanceledException | IntentSender.SendIntentException e) {
282                 Toast.makeText(launcher,
283                         launcher.getResources().getText(R.string.shortcut_not_available),
284                         Toast.LENGTH_SHORT).show();
285             }
286         }
287         launcher.getStatsLogManager().logger().withItemInfo(itemInfo).log(LAUNCHER_APP_LAUNCH_TAP);
288     }
289 
startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher)290     private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
291         TestLogging.recordEvent(
292                 TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity");
293         Intent intent;
294         if (item instanceof ItemInfoWithIcon
295                 && (((ItemInfoWithIcon) item).runtimeStatusFlags
296                 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
297             ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item;
298             intent = new PackageManagerHelper(launcher)
299                     .getMarketIntent(appInfo.getTargetComponent().getPackageName());
300         } else {
301             intent = item.getIntent();
302         }
303         if (intent == null) {
304             throw new IllegalArgumentException("Input must have a valid intent");
305         }
306         if (item instanceof WorkspaceItemInfo) {
307             WorkspaceItemInfo si = (WorkspaceItemInfo) item;
308             if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)
309                     && Intent.ACTION_VIEW.equals(intent.getAction())) {
310                 // make a copy of the intent that has the package set to null
311                 // we do this because the platform sometimes disables instant
312                 // apps temporarily (triggered by the user) and fallbacks to the
313                 // web ui. This only works though if the package isn't set
314                 intent = new Intent(intent);
315                 intent.setPackage(null);
316             }
317         }
318         if (v != null && launcher.supportsAdaptiveIconAnimation(v)) {
319             // Preload the icon to reduce latency b/w swapping the floating view with the original.
320             FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
321         }
322         launcher.startActivitySafely(v, intent, item);
323     }
324 }
325