Home | History | Annotate | Download | only in browseractions
      1 /*
      2  * Copyright 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 androidx.browser.browseractions;
     18 
     19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
     20 
     21 import android.app.PendingIntent;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.pm.PackageManager;
     25 import android.content.pm.ResolveInfo;
     26 import android.net.Uri;
     27 import android.os.Build;
     28 import android.os.Bundle;
     29 import android.text.TextUtils;
     30 
     31 import androidx.annotation.DrawableRes;
     32 import androidx.annotation.IntDef;
     33 import androidx.annotation.NonNull;
     34 import androidx.annotation.RestrictTo;
     35 import androidx.annotation.VisibleForTesting;
     36 import androidx.core.content.ContextCompat;
     37 
     38 import java.lang.annotation.Retention;
     39 import java.lang.annotation.RetentionPolicy;
     40 import java.util.ArrayList;
     41 import java.util.Arrays;
     42 import java.util.List;
     43 /**
     44  * Class holding the {@link Intent} and start bundle for a Browser Actions Activity.
     45  *
     46  * <p>
     47  * <strong>Note:</strong> The constants below are public for the browser implementation's benefit.
     48  * You are strongly encouraged to use {@link BrowserActionsIntent.Builder}.</p>
     49  */
     50 public class BrowserActionsIntent {
     51     private static final String TAG = "BrowserActions";
     52     // Used to verify that an URL intent handler exists.
     53     private static final String TEST_URL = "https://www.example.com";
     54 
     55     /**
     56      * Extra that specifies {@link PendingIntent} indicating which Application sends the {@link
     57      * BrowserActionsIntent}.
     58      */
     59     public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID";
     60 
     61     /**
     62      * Indicates that the user explicitly opted out of Browser Actions in the calling application.
     63      */
     64     public static final String ACTION_BROWSER_ACTIONS_OPEN =
     65             "androidx.browser.browseractions.browser_action_open";
     66 
     67     /**
     68      * Extra resource id that specifies the icon of a custom item shown in the Browser Actions menu.
     69      */
     70     public static final String KEY_ICON_ID = "androidx.browser.browseractions.ICON_ID";
     71 
     72     /**
     73      * Extra string that specifies the title of a custom item shown in the Browser Actions menu.
     74      */
     75     public static final String KEY_TITLE = "androidx.browser.browseractions.TITLE";
     76 
     77     /**
     78      * Extra PendingIntent to be launched when a custom item is selected in the Browser Actions
     79      * menu.
     80      */
     81     public static final String KEY_ACTION = "androidx.browser.browseractions.ACTION";
     82 
     83     /**
     84      * Extra that specifies the type of url for the Browser Actions menu.
     85      */
     86     public static final String EXTRA_TYPE = "androidx.browser.browseractions.extra.TYPE";
     87 
     88     /**
     89      * Extra that specifies List<Bundle> used for adding custom items to the Browser Actions menu.
     90      */
     91     public static final String EXTRA_MENU_ITEMS =
     92             "androidx.browser.browseractions.extra.MENU_ITEMS";
     93 
     94     /**
     95      * Extra that specifies the PendingIntent to be launched when a browser specified menu item is
     96      * selected. The id of the chosen item will be notified through the data of its Intent.
     97      */
     98     public static final String EXTRA_SELECTED_ACTION_PENDING_INTENT =
     99             "androidx.browser.browseractions.extra.SELECTED_ACTION_PENDING_INTENT";
    100 
    101     /**
    102      * The maximum allowed number of custom items.
    103      */
    104     public static final int MAX_CUSTOM_ITEMS = 5;
    105 
    106     /**
    107      * Defines the types of url for Browser Actions menu.
    108      */
    109     /** @hide */
    110     @RestrictTo(LIBRARY_GROUP)
    111     @IntDef({URL_TYPE_NONE, URL_TYPE_IMAGE, URL_TYPE_VIDEO, URL_TYPE_AUDIO, URL_TYPE_FILE,
    112             URL_TYPE_PLUGIN})
    113     @Retention(RetentionPolicy.SOURCE)
    114     public @interface BrowserActionsUrlType {}
    115     public static final int URL_TYPE_NONE = 0;
    116     public static final int URL_TYPE_IMAGE = 1;
    117     public static final int URL_TYPE_VIDEO = 2;
    118     public static final int URL_TYPE_AUDIO = 3;
    119     public static final int URL_TYPE_FILE = 4;
    120     public static final int URL_TYPE_PLUGIN = 5;
    121 
    122     /**
    123      * Defines the the ids of the browser specified menu items in Browser Actions.
    124      * TODO(ltian): A long term solution need, since other providers might have customized menus.
    125      */
    126     /** @hide */
    127     @RestrictTo(LIBRARY_GROUP)
    128     @IntDef({ITEM_INVALID_ITEM, ITEM_OPEN_IN_NEW_TAB, ITEM_OPEN_IN_INCOGNITO, ITEM_DOWNLOAD,
    129             ITEM_COPY, ITEM_SHARE})
    130     @Retention(RetentionPolicy.SOURCE)
    131     public @interface BrowserActionsItemId {}
    132     public static final int ITEM_INVALID_ITEM = -1;
    133     public static final int ITEM_OPEN_IN_NEW_TAB = 0;
    134     public static final int ITEM_OPEN_IN_INCOGNITO = 1;
    135     public static final int ITEM_DOWNLOAD = 2;
    136     public static final int ITEM_COPY = 3;
    137     public static final int ITEM_SHARE = 4;
    138 
    139     /**
    140      * An {@link Intent} used to start the Browser Actions Activity.
    141      */
    142     @NonNull private final Intent mIntent;
    143 
    144     /**
    145      * Gets the Intent of {@link BrowserActionsIntent}.
    146      * @return the Intent of {@link BrowserActionsIntent}.
    147      */
    148     @NonNull public Intent getIntent() {
    149         return mIntent;
    150     }
    151 
    152     private BrowserActionsIntent(@NonNull Intent intent) {
    153         this.mIntent = intent;
    154     }
    155 
    156     /** @hide */
    157     @VisibleForTesting
    158     @RestrictTo(LIBRARY_GROUP)
    159     interface BrowserActionsFallDialogListener {
    160         void onDialogShown();
    161     }
    162 
    163     private static BrowserActionsFallDialogListener sDialogListenter;
    164 
    165     /**
    166      * Builder class for opening a Browser Actions context menu.
    167      */
    168     public static final class Builder {
    169         private final Intent mIntent = new Intent(BrowserActionsIntent.ACTION_BROWSER_ACTIONS_OPEN);
    170         private Context mContext;
    171         private Uri mUri;
    172         @BrowserActionsUrlType
    173         private int mType;
    174         private ArrayList<Bundle> mMenuItems = null;
    175         private PendingIntent mOnItemSelectedPendingIntent = null;
    176 
    177         /**
    178          * Constructs a {@link BrowserActionsIntent.Builder} object associated with default setting
    179          * for a selected url.
    180          * @param context The context requesting the Browser Actions context menu.
    181          * @param uri The selected url for Browser Actions menu.
    182          */
    183         public Builder(Context context, Uri uri) {
    184             mContext = context;
    185             mUri = uri;
    186             mType = URL_TYPE_NONE;
    187             mMenuItems = new ArrayList<>();
    188         }
    189 
    190         /**
    191          * Sets the type of Browser Actions context menu.
    192          * @param type The type of url.
    193          */
    194         public Builder setUrlType(@BrowserActionsUrlType int type) {
    195             mType = type;
    196             return this;
    197         }
    198 
    199         /**
    200          * Sets the custom items list.
    201          * Only maximum MAX_CUSTOM_ITEMS custom items are allowed,
    202          * otherwise throws an {@link IllegalStateException}.
    203          * @param items The list of {@link BrowserActionItem} for custom items.
    204          */
    205         public Builder setCustomItems(ArrayList<BrowserActionItem> items) {
    206             if (items.size() > MAX_CUSTOM_ITEMS) {
    207                 throw new IllegalStateException(
    208                         "Exceeded maximum toolbar item count of " + MAX_CUSTOM_ITEMS);
    209             }
    210             for (int i = 0; i < items.size(); i++) {
    211                 if (TextUtils.isEmpty(items.get(i).getTitle())
    212                         || items.get(i).getAction() == null) {
    213                     throw new IllegalArgumentException(
    214                             "Custom item should contain a non-empty title and non-null intent.");
    215                 } else {
    216                     mMenuItems.add(getBundleFromItem(items.get(i)));
    217                 }
    218             }
    219             return this;
    220         }
    221 
    222         /**
    223          * Sets the custom items list.
    224          * Only maximum MAX_CUSTOM_ITEMS custom items are allowed,
    225          * otherwise throws an {@link IllegalStateException}.
    226          * @param items The varargs of {@link BrowserActionItem} for custom items.
    227          */
    228         public Builder setCustomItems(BrowserActionItem... items) {
    229             return setCustomItems(new ArrayList<BrowserActionItem>(Arrays.asList(items)));
    230         }
    231 
    232         /**
    233          * Set the PendingIntent to be launched when a a browser specified menu item is selected.
    234          * @param onItemSelectedPendingIntent The PendingIntent to be launched.
    235          */
    236         public Builder setOnItemSelectedAction(PendingIntent onItemSelectedPendingIntent) {
    237             mOnItemSelectedPendingIntent = onItemSelectedPendingIntent;
    238             return this;
    239         }
    240 
    241         /**
    242          * Populates a {@link Bundle} to hold a custom item for Browser Actions menu.
    243          * @param item A custom item for Browser Actions menu.
    244          * @return The Bundle of custom item.
    245          */
    246         private Bundle getBundleFromItem(BrowserActionItem item) {
    247             Bundle bundle = new Bundle();
    248             bundle.putString(KEY_TITLE, item.getTitle());
    249             bundle.putParcelable(KEY_ACTION, item.getAction());
    250             if (item.getIconId() != 0) bundle.putInt(KEY_ICON_ID, item.getIconId());
    251             return bundle;
    252         }
    253 
    254         /**
    255          * Combines all the options that have been set and returns a new {@link
    256          * BrowserActionsIntent} object.
    257          */
    258         public BrowserActionsIntent build() {
    259             mIntent.setData(mUri);
    260             mIntent.putExtra(EXTRA_TYPE, mType);
    261             mIntent.putParcelableArrayListExtra(EXTRA_MENU_ITEMS, mMenuItems);
    262             PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
    263             mIntent.putExtra(EXTRA_APP_ID, pendingIntent);
    264             if (mOnItemSelectedPendingIntent != null) {
    265                 mIntent.putExtra(
    266                         EXTRA_SELECTED_ACTION_PENDING_INTENT, mOnItemSelectedPendingIntent);
    267             }
    268             return new BrowserActionsIntent(mIntent);
    269         }
    270     }
    271 
    272     /**
    273      * Construct a BrowserActionsIntent with default settings and launch it to open a Browser
    274      * Actions menu.
    275      * @param context The context requesting for a Browser Actions menu.
    276      * @param uri The url for Browser Actions menu.
    277      */
    278     public static void openBrowserAction(Context context, Uri uri) {
    279         BrowserActionsIntent intent = new BrowserActionsIntent.Builder(context, uri).build();
    280         launchIntent(context, intent.getIntent());
    281     }
    282 
    283     /**
    284      * Construct a BrowserActionsIntent with custom settings and launch it to open a Browser Actions
    285      * menu.
    286      * @param context The context requesting for a Browser Actions menu.
    287      * @param uri The url for Browser Actions menu.
    288      * @param type The type of the url for context menu to be opened.
    289      * @param items List of custom items to be added to Browser Actions menu.
    290      * @param pendingIntent The PendingIntent to be launched when a browser specified menu item is
    291      * selected.
    292      */
    293     public static void openBrowserAction(Context context, Uri uri, int type,
    294             ArrayList<BrowserActionItem> items, PendingIntent pendingIntent) {
    295         BrowserActionsIntent intent = new BrowserActionsIntent.Builder(context, uri)
    296                 .setUrlType(type)
    297                 .setCustomItems(items)
    298                 .setOnItemSelectedAction(pendingIntent)
    299                 .build();
    300         launchIntent(context, intent.getIntent());
    301     }
    302 
    303     /**
    304      * Launch an Intent to open a Browser Actions menu.
    305      * It first checks if any Browser Actions provider is available to create the menu.
    306      * If the default Browser supports Browser Actions, menu will be opened by the default Browser,
    307      * otherwise show a intent picker.
    308      * If not provider, a Browser Actions menu is opened locally from support library.
    309      * @param context The context requesting for a Browser Actions menu.
    310      * @param intent The {@link Intent} holds the setting for Browser Actions menu.
    311      */
    312     public static void launchIntent(Context context, Intent intent) {
    313         List<ResolveInfo> handlers = getBrowserActionsIntentHandlers(context);
    314         launchIntent(context, intent, handlers);
    315     }
    316 
    317     /** @hide */
    318     @RestrictTo(LIBRARY_GROUP)
    319     @VisibleForTesting
    320     static void launchIntent(Context context, Intent intent, List<ResolveInfo> handlers) {
    321         if (handlers == null || handlers.size() == 0) {
    322             openFallbackBrowserActionsMenu(context, intent);
    323             return;
    324         } else if (handlers.size() == 1) {
    325             intent.setPackage(handlers.get(0).activityInfo.packageName);
    326         } else {
    327             Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(TEST_URL));
    328             PackageManager pm = context.getPackageManager();
    329             ResolveInfo defaultHandler =
    330                     pm.resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
    331             if (defaultHandler != null) {
    332                 String defaultPackageName = defaultHandler.activityInfo.packageName;
    333                 for (int i = 0; i < handlers.size(); i++) {
    334                     if (defaultPackageName.equals(handlers.get(i).activityInfo.packageName)) {
    335                         intent.setPackage(defaultPackageName);
    336                         break;
    337                     }
    338                 }
    339             }
    340         }
    341         ContextCompat.startActivity(context, intent, null);
    342     }
    343 
    344     /**
    345      * Returns a list of Browser Actions providers available to handle the {@link
    346      * BrowserActionsIntent}.
    347      * @param context The context requesting for a Browser Actions menu.
    348      * @return List of Browser Actions providers available to handle the intent.
    349      */
    350     private static List<ResolveInfo> getBrowserActionsIntentHandlers(Context context) {
    351         Intent intent =
    352                 new Intent(BrowserActionsIntent.ACTION_BROWSER_ACTIONS_OPEN, Uri.parse(TEST_URL));
    353         PackageManager pm = context.getPackageManager();
    354         return pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
    355     }
    356 
    357     private static void openFallbackBrowserActionsMenu(Context context, Intent intent) {
    358         Uri uri = intent.getData();
    359         int type = intent.getIntExtra(EXTRA_TYPE, URL_TYPE_NONE);
    360         ArrayList<Bundle> bundles = intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS);
    361         List<BrowserActionItem> items = bundles != null ? parseBrowserActionItems(bundles) : null;
    362         openFallbackBrowserActionsMenu(context, uri, type, items);
    363     }
    364 
    365     /** @hide */
    366     @RestrictTo(LIBRARY_GROUP)
    367     @VisibleForTesting
    368     static void setDialogShownListenter(BrowserActionsFallDialogListener dialogListener) {
    369         sDialogListenter = dialogListener;
    370     }
    371 
    372     /**
    373      * Open a Browser Actions menu from support library.
    374      * @param context The context requesting for a Browser Actions menu.
    375      * @param uri The url for Browser Actions menu.
    376      * @param type The type of the url for context menu to be opened.
    377      * @param menuItems List of custom items to add to Browser Actions menu.
    378      */
    379     private static void openFallbackBrowserActionsMenu(
    380             Context context, Uri uri, int type, List<BrowserActionItem> menuItems) {
    381         BrowserActionsFallbackMenuUi menuUi =
    382                 new BrowserActionsFallbackMenuUi(context, uri, menuItems);
    383         menuUi.displayMenu();
    384         if (sDialogListenter != null) {
    385             sDialogListenter.onDialogShown();
    386         }
    387     }
    388 
    389     /**
    390      * Gets custom item list for browser action menu.
    391      * @param bundles Data for custom items from {@link BrowserActionsIntent}.
    392      * @return List of {@link BrowserActionItem}
    393      */
    394     public static List<BrowserActionItem> parseBrowserActionItems(ArrayList<Bundle> bundles) {
    395         List<BrowserActionItem> mActions = new ArrayList<>();
    396         for (int i = 0; i < bundles.size(); i++) {
    397             Bundle bundle = bundles.get(i);
    398             String title = bundle.getString(BrowserActionsIntent.KEY_TITLE);
    399             PendingIntent action = bundle.getParcelable(BrowserActionsIntent.KEY_ACTION);
    400             @DrawableRes
    401             int iconId = bundle.getInt(BrowserActionsIntent.KEY_ICON_ID);
    402             if (TextUtils.isEmpty(title) || action == null) {
    403                 throw new IllegalArgumentException(
    404                         "Custom item should contain a non-empty title and non-null intent.");
    405             } else {
    406                 BrowserActionItem item = new BrowserActionItem(title, action, iconId);
    407                 mActions.add(item);
    408             }
    409         }
    410         return mActions;
    411     }
    412 
    413     /**
    414      * Get the package name of the creator application.
    415      * @param intent The {@link BrowserActionsIntent}.
    416      * @return The creator package name.
    417      */
    418     @SuppressWarnings("deprecation")
    419     public static String getCreatorPackageName(Intent intent) {
    420         PendingIntent pendingIntent = intent.getParcelableExtra(BrowserActionsIntent.EXTRA_APP_ID);
    421         if (pendingIntent != null) {
    422             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    423                 return pendingIntent.getCreatorPackage();
    424             } else {
    425                 return pendingIntent.getTargetPackage();
    426             }
    427         }
    428         return null;
    429     }
    430 }
    431