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