Home | History | Annotate | Download | only in touch
      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.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
     19 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
     20 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
     21 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
     22 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
     23 import static com.android.launcher3.Launcher.REQUEST_BIND_PENDING_APPWIDGET;
     24 import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET;
     25 
     26 import android.app.AlertDialog;
     27 import android.content.Intent;
     28 import android.os.Process;
     29 import android.text.TextUtils;
     30 import android.view.View;
     31 import android.view.View.OnClickListener;
     32 import android.widget.Toast;
     33 
     34 import com.android.launcher3.AppInfo;
     35 import com.android.launcher3.BubbleTextView;
     36 import com.android.launcher3.FolderInfo;
     37 import com.android.launcher3.ItemInfo;
     38 import com.android.launcher3.Launcher;
     39 import com.android.launcher3.LauncherAppWidgetInfo;
     40 import com.android.launcher3.LauncherAppWidgetProviderInfo;
     41 import com.android.launcher3.PromiseAppInfo;
     42 import com.android.launcher3.R;
     43 import com.android.launcher3.ShortcutInfo;
     44 import com.android.launcher3.compat.AppWidgetManagerCompat;
     45 import com.android.launcher3.folder.Folder;
     46 import com.android.launcher3.folder.FolderIcon;
     47 import com.android.launcher3.util.PackageManagerHelper;
     48 import com.android.launcher3.widget.PendingAppWidgetHostView;
     49 import com.android.launcher3.widget.WidgetAddFlowHandler;
     50 
     51 /**
     52  * Class for handling clicks on workspace and all-apps items
     53  */
     54 public class ItemClickHandler {
     55 
     56     /**
     57      * Instance used for click handling on items
     58      */
     59     public static final OnClickListener INSTANCE = ItemClickHandler::onClick;
     60 
     61     private static void onClick(View v) {
     62         // Make sure that rogue clicks don't get through while allapps is launching, or after the
     63         // view has detached (it's possible for this to happen if the view is removed mid touch).
     64         if (v.getWindowToken() == null) {
     65             return;
     66         }
     67 
     68         Launcher launcher = Launcher.getLauncher(v.getContext());
     69         if (!launcher.getWorkspace().isFinishedSwitchingState()) {
     70             return;
     71         }
     72 
     73         Object tag = v.getTag();
     74         if (tag instanceof ShortcutInfo) {
     75             onClickAppShortcut(v, (ShortcutInfo) tag, launcher);
     76         } else if (tag instanceof FolderInfo) {
     77             if (v instanceof FolderIcon) {
     78                 onClickFolderIcon(v);
     79             }
     80         } else if (tag instanceof AppInfo) {
     81             startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
     82         } else if (tag instanceof LauncherAppWidgetInfo) {
     83             if (v instanceof PendingAppWidgetHostView) {
     84                 onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
     85             }
     86         }
     87     }
     88 
     89     /**
     90      * Event handler for a folder icon click.
     91      *
     92      * @param v The view that was clicked. Must be an instance of {@link FolderIcon}.
     93      */
     94     private static void onClickFolderIcon(View v) {
     95         Folder folder = ((FolderIcon) v).getFolder();
     96         if (!folder.isOpen() && !folder.isDestroyed()) {
     97             // Open the requested folder
     98             folder.animateOpen();
     99         }
    100     }
    101 
    102     /**
    103      * Event handler for the app widget view which has not fully restored.
    104      */
    105     private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) {
    106         if (launcher.getPackageManager().isSafeMode()) {
    107             Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
    108             return;
    109         }
    110 
    111         final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
    112         if (v.isReadyForClickSetup()) {
    113             LauncherAppWidgetProviderInfo appWidgetInfo = AppWidgetManagerCompat
    114                     .getInstance(launcher).findProvider(info.providerName, info.user);
    115             if (appWidgetInfo == null) {
    116                 return;
    117             }
    118             WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo);
    119 
    120             if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
    121                 if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
    122                     // This should not happen, as we make sure that an Id is allocated during bind.
    123                     return;
    124                 }
    125                 addFlowHandler.startBindFlow(launcher, info.appWidgetId, info,
    126                         REQUEST_BIND_PENDING_APPWIDGET);
    127             } else {
    128                 addFlowHandler.startConfigActivity(launcher, info, REQUEST_RECONFIGURE_APPWIDGET);
    129             }
    130         } else {
    131             final String packageName = info.providerName.getPackageName();
    132             onClickPendingAppItem(v, launcher, packageName, info.installProgress >= 0);
    133         }
    134     }
    135 
    136     private static void onClickPendingAppItem(View v, Launcher launcher, String packageName,
    137             boolean downloadStarted) {
    138         if (downloadStarted) {
    139             // If the download has started, simply direct to the market app.
    140             startMarketIntentForPackage(v, launcher, packageName);
    141             return;
    142         }
    143         new AlertDialog.Builder(launcher)
    144                 .setTitle(R.string.abandoned_promises_title)
    145                 .setMessage(R.string.abandoned_promise_explanation)
    146                 .setPositiveButton(R.string.abandoned_search,
    147                         (d, i) -> startMarketIntentForPackage(v, launcher, packageName))
    148                 .setNeutralButton(R.string.abandoned_clean_this,
    149                         (d, i) -> launcher.getWorkspace()
    150                                 .removeAbandonedPromise(packageName, Process.myUserHandle()))
    151                 .create().show();
    152     }
    153 
    154     private static void startMarketIntentForPackage(View v, Launcher launcher, String packageName) {
    155         ItemInfo item = (ItemInfo) v.getTag();
    156         Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName);
    157         launcher.startActivitySafely(v, intent, item);
    158     }
    159 
    160     /**
    161      * Event handler for an app shortcut click.
    162      *
    163      * @param v The view that was clicked. Must be a tagged with a {@link ShortcutInfo}.
    164      */
    165     private static void onClickAppShortcut(View v, ShortcutInfo shortcut, Launcher launcher) {
    166         if (shortcut.isDisabled()) {
    167             final int disabledFlags = shortcut.runtimeStatusFlags & ShortcutInfo.FLAG_DISABLED_MASK;
    168             if ((disabledFlags &
    169                     ~FLAG_DISABLED_SUSPENDED &
    170                     ~FLAG_DISABLED_QUIET_USER) == 0) {
    171                 // If the app is only disabled because of the above flags, launch activity anyway.
    172                 // Framework will tell the user why the app is suspended.
    173             } else {
    174                 if (!TextUtils.isEmpty(shortcut.disabledMessage)) {
    175                     // Use a message specific to this shortcut, if it has one.
    176                     Toast.makeText(launcher, shortcut.disabledMessage, Toast.LENGTH_SHORT).show();
    177                     return;
    178                 }
    179                 // Otherwise just use a generic error message.
    180                 int error = R.string.activity_not_available;
    181                 if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) {
    182                     error = R.string.safemode_shortcut_error;
    183                 } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0 ||
    184                         (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) {
    185                     error = R.string.shortcut_not_available;
    186                 }
    187                 Toast.makeText(launcher, error, Toast.LENGTH_SHORT).show();
    188                 return;
    189             }
    190         }
    191 
    192         // Check for abandoned promise
    193         if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) {
    194             String packageName = shortcut.intent.getComponent() != null ?
    195                     shortcut.intent.getComponent().getPackageName() : shortcut.intent.getPackage();
    196             if (!TextUtils.isEmpty(packageName)) {
    197                 onClickPendingAppItem(v, launcher, packageName,
    198                         shortcut.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE));
    199                 return;
    200             }
    201         }
    202 
    203         // Start activities
    204         startAppShortcutOrInfoActivity(v, shortcut, launcher);
    205     }
    206 
    207     private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
    208         Intent intent;
    209         if (item instanceof PromiseAppInfo) {
    210             PromiseAppInfo promiseAppInfo = (PromiseAppInfo) item;
    211             intent = promiseAppInfo.getMarketIntent(launcher);
    212         } else {
    213             intent = item.getIntent();
    214         }
    215         if (intent == null) {
    216             throw new IllegalArgumentException("Input must have a valid intent");
    217         }
    218         if (item instanceof ShortcutInfo) {
    219             ShortcutInfo si = (ShortcutInfo) item;
    220             if (si.hasStatusFlag(ShortcutInfo.FLAG_SUPPORTS_WEB_UI)
    221                     && intent.getAction() == Intent.ACTION_VIEW) {
    222                 // make a copy of the intent that has the package set to null
    223                 // we do this because the platform sometimes disables instant
    224                 // apps temporarily (triggered by the user) and fallbacks to the
    225                 // web ui. This only works though if the package isn't set
    226                 intent = new Intent(intent);
    227                 intent.setPackage(null);
    228             }
    229         }
    230         launcher.startActivitySafely(v, intent, item);
    231     }
    232 }
    233