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      */
getIntent()148     @NonNull public Intent getIntent() {
149         return mIntent;
150     }
151 
BrowserActionsIntent(@onNull Intent intent)152     private BrowserActionsIntent(@NonNull Intent intent) {
153         this.mIntent = intent;
154     }
155 
156     /** @hide */
157     @VisibleForTesting
158     @RestrictTo(LIBRARY_GROUP)
159     interface BrowserActionsFallDialogListener {
onDialogShown()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          */
Builder(Context context, Uri uri)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          */
setUrlType(@rowserActionsUrlType int type)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          */
setCustomItems(ArrayList<BrowserActionItem> items)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          */
setCustomItems(BrowserActionItem... items)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          */
setOnItemSelectedAction(PendingIntent onItemSelectedPendingIntent)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          */
getBundleFromItem(BrowserActionItem item)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          */
build()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      */
openBrowserAction(Context context, Uri uri)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      */
openBrowserAction(Context context, Uri uri, int type, ArrayList<BrowserActionItem> items, PendingIntent pendingIntent)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      */
launchIntent(Context context, Intent intent)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
launchIntent(Context context, Intent intent, List<ResolveInfo> handlers)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      */
getBrowserActionsIntentHandlers(Context context)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 
openFallbackBrowserActionsMenu(Context context, Intent intent)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
setDialogShownListenter(BrowserActionsFallDialogListener dialogListener)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      */
openFallbackBrowserActionsMenu( Context context, Uri uri, int type, List<BrowserActionItem> menuItems)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      */
parseBrowserActionItems(ArrayList<Bundle> bundles)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")
getCreatorPackageName(Intent intent)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