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