1/*
2 * Copyright 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
17package androidx.browser.customtabs;
18
19import android.app.Activity;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.graphics.Color;
25import android.net.Uri;
26import android.os.Bundle;
27import android.view.View;
28import android.widget.RemoteViews;
29
30import androidx.annotation.AnimRes;
31import androidx.annotation.ColorInt;
32import androidx.annotation.NonNull;
33import androidx.annotation.Nullable;
34import androidx.core.app.ActivityOptionsCompat;
35import androidx.core.app.BundleCompat;
36import androidx.core.content.ContextCompat;
37
38import java.util.ArrayList;
39
40/**
41 * Class holding the {@link Intent} and start bundle for a Custom Tabs Activity.
42 *
43 * <p>
44 * <strong>Note:</strong> The constants below are public for the browser implementation's benefit.
45 * You are strongly encouraged to use {@link CustomTabsIntent.Builder}.</p>
46 */
47public final class CustomTabsIntent {
48
49    /**
50     * Indicates that the user explicitly opted out of Custom Tabs in the calling application.
51     * <p>
52     * If an application provides a mechanism for users to opt out of Custom Tabs, this extra should
53     * be provided with {@link Intent#FLAG_ACTIVITY_NEW_TASK} to ensure the browser does not attempt
54     * to trigger any Custom Tab-like experiences as a result of the VIEW intent.
55     * <p>
56     * If this extra is present with {@link Intent#FLAG_ACTIVITY_NEW_TASK}, all Custom Tabs
57     * customizations will be ignored.
58     */
59    private static final String EXTRA_USER_OPT_OUT_FROM_CUSTOM_TABS =
60            "android.support.customtabs.extra.user_opt_out";
61
62    /**
63     * Extra used to match the session. This has to be included in the intent to open in
64     * a custom tab. This is the same IBinder that gets passed to ICustomTabsService#newSession.
65     * Null if there is no need to match any service side sessions with the intent.
66     */
67    public static final String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
68
69    /**
70     * Extra that changes the background color for the toolbar. colorRes is an int that specifies a
71     * {@link Color}, not a resource id.
72     */
73    public static final String EXTRA_TOOLBAR_COLOR =
74            "android.support.customtabs.extra.TOOLBAR_COLOR";
75
76    /**
77     * Boolean extra that enables the url bar to hide as the user scrolls down the page
78     */
79    public static final String EXTRA_ENABLE_URLBAR_HIDING =
80            "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
81
82    /**
83     * Extra bitmap that specifies the icon of the back button on the toolbar. If the client chooses
84     * not to customize it, a default close button will be used.
85     */
86    public static final String EXTRA_CLOSE_BUTTON_ICON =
87            "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
88
89    /**
90     * Extra (int) that specifies state for showing the page title. Default is {@link #NO_TITLE}.
91     */
92    public static final String EXTRA_TITLE_VISIBILITY_STATE =
93            "android.support.customtabs.extra.TITLE_VISIBILITY";
94
95    /**
96     * Don't show any title. Shows only the domain.
97     */
98    public static final int NO_TITLE = 0;
99
100    /**
101     * Shows the page title and the domain.
102     */
103    public static final int SHOW_PAGE_TITLE = 1;
104
105    /**
106     * Bundle used for adding a custom action button to the custom tab toolbar. The client should
107     * provide a description, an icon {@link Bitmap} and a {@link PendingIntent} for the button.
108     * All three keys must be present.
109     */
110    public static final String EXTRA_ACTION_BUTTON_BUNDLE =
111            "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
112
113    /**
114     * List<Bundle> used for adding items to the top and bottom toolbars. The client should
115     * provide an ID, a description, an icon {@link Bitmap} for each item. They may also provide a
116     * {@link PendingIntent} if the item is a button.
117     */
118    public static final String EXTRA_TOOLBAR_ITEMS =
119            "android.support.customtabs.extra.TOOLBAR_ITEMS";
120
121    /**
122     * Extra that changes the background color for the secondary toolbar. The value should be an
123     * int that specifies a {@link Color}, not a resource id.
124     */
125    public static final String EXTRA_SECONDARY_TOOLBAR_COLOR =
126            "android.support.customtabs.extra.SECONDARY_TOOLBAR_COLOR";
127
128    /**
129     * Key that specifies the {@link Bitmap} to be used as the image source for the action button.
130     *  The icon should't be more than 24dp in height (No padding needed. The button itself will be
131     *  48dp in height) and have a width/height ratio of less than 2.
132     */
133    public static final String KEY_ICON = "android.support.customtabs.customaction.ICON";
134
135    /**
136     * Key that specifies the content description for the custom action button.
137     */
138    public static final String KEY_DESCRIPTION =
139            "android.support.customtabs.customaction.DESCRIPTION";
140
141    /**
142     * Key that specifies the PendingIntent to launch when the action button or menu item was
143     * clicked. The custom tab will be calling {@link PendingIntent#send()} on clicks after adding
144     * the url as data. The client app can call {@link Intent#getDataString()} to get the url.
145     */
146    public static final String KEY_PENDING_INTENT =
147            "android.support.customtabs.customaction.PENDING_INTENT";
148
149    /**
150     * Extra boolean that specifies whether the custom action button should be tinted. Default is
151     * false and the action button will not be tinted.
152     */
153    public static final String EXTRA_TINT_ACTION_BUTTON =
154            "android.support.customtabs.extra.TINT_ACTION_BUTTON";
155
156    /**
157     * Use an {@code ArrayList<Bundle>} for specifying menu related params. There should be a
158     * separate {@link Bundle} for each custom menu item.
159     */
160    public static final String EXTRA_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
161
162    /**
163     * Key for specifying the title of a menu item.
164     */
165    public static final String KEY_MENU_ITEM_TITLE =
166            "android.support.customtabs.customaction.MENU_ITEM_TITLE";
167
168    /**
169     * Bundle constructed out of {@link ActivityOptionsCompat} that will be running when the
170     * {@link Activity} that holds the custom tab gets finished. A similar ActivityOptions
171     * for creation should be constructed and given to the startActivity() call that
172     * launches the custom tab.
173     */
174    public static final String EXTRA_EXIT_ANIMATION_BUNDLE =
175            "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
176
177    /**
178     * Boolean extra that specifies whether a default share button will be shown in the menu.
179     */
180    public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM =
181            "android.support.customtabs.extra.SHARE_MENU_ITEM";
182
183    /**
184     * Extra that specifies the {@link RemoteViews} showing on the secondary toolbar. If this extra
185     * is set, the other secondary toolbar configurations will be overriden. The height of the
186     * {@link RemoteViews} should not exceed 56dp.
187     * @see CustomTabsIntent.Builder#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent).
188     */
189    public static final String EXTRA_REMOTEVIEWS =
190            "android.support.customtabs.extra.EXTRA_REMOTEVIEWS";
191
192    /**
193     * Extra that specifies an array of {@link View} ids. When these {@link View}s are clicked, a
194     * {@link PendingIntent} will be sent, carrying the current url of the custom tab as data.
195     * <p>
196     * Note that Custom Tabs will override the default onClick behavior of the listed {@link View}s.
197     * If you do not care about the current url, you can safely ignore this extra and use
198     * {@link RemoteViews#setOnClickPendingIntent(int, PendingIntent)} instead.
199     * @see CustomTabsIntent.Builder#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent).
200     */
201    public static final String EXTRA_REMOTEVIEWS_VIEW_IDS =
202            "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_VIEW_IDS";
203
204    /**
205     * Extra that specifies the {@link PendingIntent} to be sent when the user clicks on the
206     * {@link View}s that is listed by {@link #EXTRA_REMOTEVIEWS_VIEW_IDS}.
207     * <p>
208     * Note when this {@link PendingIntent} is triggered, it will have the current url as data
209     * field, also the id of the clicked {@link View}, specified by
210     * {@link #EXTRA_REMOTEVIEWS_CLICKED_ID}.
211     * @see CustomTabsIntent.Builder#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent).
212     */
213    public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT =
214            "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
215
216    /**
217     * Extra that specifies which {@link View} has been clicked. This extra will be put to the
218     * {@link PendingIntent} sent from Custom Tabs when a view in the {@link RemoteViews} is clicked
219     * @see CustomTabsIntent.Builder#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent).
220     */
221    public static final String EXTRA_REMOTEVIEWS_CLICKED_ID =
222            "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_CLICKED_ID";
223
224    /**
225     * Extra that specifies whether Instant Apps is enabled.
226     */
227    public static final String EXTRA_ENABLE_INSTANT_APPS =
228            "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
229
230    /**
231     * Key that specifies the unique ID for an action button. To make a button to show on the
232     * toolbar, use {@link #TOOLBAR_ACTION_BUTTON_ID} as its ID.
233     */
234    public static final String KEY_ID = "android.support.customtabs.customaction.ID";
235
236    /**
237     * The ID allocated to the custom action button that is shown on the toolbar.
238     */
239    public static final int TOOLBAR_ACTION_BUTTON_ID = 0;
240
241    /**
242     * The maximum allowed number of toolbar items.
243     */
244    private static final int MAX_TOOLBAR_ITEMS = 5;
245
246    /**
247     * An {@link Intent} used to start the Custom Tabs Activity.
248     */
249    @NonNull public final Intent intent;
250
251    /**
252     * A {@link Bundle} containing the start animation for the Custom Tabs Activity.
253     */
254    @Nullable public final Bundle startAnimationBundle;
255
256    /**
257     * Convenience method to launch a Custom Tabs Activity.
258     * @param context The source Context.
259     * @param url The URL to load in the Custom Tab.
260     */
261    public void launchUrl(Context context, Uri url) {
262        intent.setData(url);
263        ContextCompat.startActivity(context, intent, startAnimationBundle);
264    }
265
266    private CustomTabsIntent(Intent intent, Bundle startAnimationBundle) {
267        this.intent = intent;
268        this.startAnimationBundle = startAnimationBundle;
269    }
270
271    /**
272     * Builder class for {@link CustomTabsIntent} objects.
273     */
274    public static final class Builder {
275        private final Intent mIntent = new Intent(Intent.ACTION_VIEW);
276        private ArrayList<Bundle> mMenuItems = null;
277        private Bundle mStartAnimationBundle = null;
278        private ArrayList<Bundle> mActionButtons = null;
279        private boolean mInstantAppsEnabled = true;
280
281        /**
282         * Creates a {@link CustomTabsIntent.Builder} object associated with no
283         * {@link CustomTabsSession}.
284         */
285        public Builder() {
286            this(null);
287        }
288
289        /**
290         * Creates a {@link CustomTabsIntent.Builder} object associated with a given
291         * {@link CustomTabsSession}.
292         *
293         * Guarantees that the {@link Intent} will be sent to the same component as the one the
294         * session is associated with.
295         *
296         * @param session The session to associate this Builder with.
297         */
298        public Builder(@Nullable CustomTabsSession session) {
299            if (session != null) mIntent.setPackage(session.getComponentName().getPackageName());
300            Bundle bundle = new Bundle();
301            BundleCompat.putBinder(
302                    bundle, EXTRA_SESSION, session == null ? null : session.getBinder());
303            mIntent.putExtras(bundle);
304        }
305
306        /**
307         * Sets the toolbar color.
308         *
309         * @param color {@link Color}
310         */
311        public Builder setToolbarColor(@ColorInt int color) {
312            mIntent.putExtra(EXTRA_TOOLBAR_COLOR, color);
313            return this;
314        }
315
316        /**
317         * Enables the url bar to hide as the user scrolls down on the page.
318         */
319        public Builder enableUrlBarHiding() {
320            mIntent.putExtra(EXTRA_ENABLE_URLBAR_HIDING, true);
321            return this;
322        }
323
324        /**
325         * Sets the Close button icon for the custom tab.
326         *
327         * @param icon The icon {@link Bitmap}
328         */
329        public Builder setCloseButtonIcon(@NonNull Bitmap icon) {
330            mIntent.putExtra(EXTRA_CLOSE_BUTTON_ICON, icon);
331            return this;
332        }
333
334        /**
335         * Sets whether the title should be shown in the custom tab.
336         *
337         * @param showTitle Whether the title should be shown.
338         */
339        public Builder setShowTitle(boolean showTitle) {
340            mIntent.putExtra(EXTRA_TITLE_VISIBILITY_STATE,
341                    showTitle ? SHOW_PAGE_TITLE : NO_TITLE);
342            return this;
343        }
344
345        /**
346         * Adds a menu item.
347         *
348         * @param label Menu label.
349         * @param pendingIntent Pending intent delivered when the menu item is clicked.
350         */
351        public Builder addMenuItem(@NonNull String label, @NonNull PendingIntent pendingIntent) {
352            if (mMenuItems == null) mMenuItems = new ArrayList<>();
353            Bundle bundle = new Bundle();
354            bundle.putString(KEY_MENU_ITEM_TITLE, label);
355            bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent);
356            mMenuItems.add(bundle);
357            return this;
358        }
359
360        /**
361         * Adds a default share item to the menu.
362         */
363        public Builder addDefaultShareMenuItem() {
364            mIntent.putExtra(EXTRA_DEFAULT_SHARE_MENU_ITEM, true);
365            return this;
366        }
367
368        /**
369         * Sets the action button that is displayed in the Toolbar.
370         * <p>
371         * This is equivalent to calling
372         * {@link CustomTabsIntent.Builder#addToolbarItem(int, Bitmap, String, PendingIntent)}
373         * with {@link #TOOLBAR_ACTION_BUTTON_ID} as id.
374         *
375         * @param icon The icon.
376         * @param description The description for the button. To be used for accessibility.
377         * @param pendingIntent pending intent delivered when the button is clicked.
378         * @param shouldTint Whether the action button should be tinted.
379         *
380         * @see CustomTabsIntent.Builder#addToolbarItem(int, Bitmap, String, PendingIntent)
381         */
382        public Builder setActionButton(@NonNull Bitmap icon, @NonNull String description,
383                @NonNull PendingIntent pendingIntent, boolean shouldTint) {
384            Bundle bundle = new Bundle();
385            bundle.putInt(KEY_ID, TOOLBAR_ACTION_BUTTON_ID);
386            bundle.putParcelable(KEY_ICON, icon);
387            bundle.putString(KEY_DESCRIPTION, description);
388            bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent);
389            mIntent.putExtra(EXTRA_ACTION_BUTTON_BUNDLE, bundle);
390            mIntent.putExtra(EXTRA_TINT_ACTION_BUTTON, shouldTint);
391            return this;
392        }
393
394        /**
395         * Sets the action button that is displayed in the Toolbar with default tinting behavior.
396         *
397         * @see CustomTabsIntent.Builder#setActionButton(
398         * Bitmap, String, PendingIntent, boolean)
399         */
400        public Builder setActionButton(@NonNull Bitmap icon, @NonNull String description,
401                @NonNull PendingIntent pendingIntent) {
402            return setActionButton(icon, description, pendingIntent, false);
403        }
404
405        /**
406         * Adds an action button to the custom tab. Multiple buttons can be added via this method.
407         * If the given id equals {@link #TOOLBAR_ACTION_BUTTON_ID}, the button will be placed on
408         * the toolbar; if the bitmap is too wide, it will be put to the bottom bar instead. If
409         * the id is not {@link #TOOLBAR_ACTION_BUTTON_ID}, it will be directly put on secondary
410         * toolbar. The maximum number of allowed toolbar items in a single intent is
411         * {@link CustomTabsIntent#getMaxToolbarItems()}. Throws an
412         * {@link IllegalStateException} when that number is exceeded per intent.
413         *
414         * @param id The unique id of the action button. This should be non-negative.
415         * @param icon The icon.
416         * @param description The description for the button. To be used for accessibility.
417         * @param pendingIntent The pending intent delivered when the button is clicked.
418         *
419         * @see CustomTabsIntent#getMaxToolbarItems()
420         * @deprecated Use
421         * CustomTabsIntent.Builder#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent).
422         */
423        @Deprecated
424        public Builder addToolbarItem(int id, @NonNull Bitmap icon, @NonNull String description,
425                PendingIntent pendingIntent) throws IllegalStateException {
426            if (mActionButtons == null) {
427                mActionButtons = new ArrayList<>();
428            }
429            if (mActionButtons.size() >= MAX_TOOLBAR_ITEMS) {
430                throw new IllegalStateException(
431                        "Exceeded maximum toolbar item count of " + MAX_TOOLBAR_ITEMS);
432            }
433            Bundle bundle = new Bundle();
434            bundle.putInt(KEY_ID, id);
435            bundle.putParcelable(KEY_ICON, icon);
436            bundle.putString(KEY_DESCRIPTION, description);
437            bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent);
438            mActionButtons.add(bundle);
439            return this;
440        }
441
442        /**
443         * Sets the color of the secondary toolbar.
444         * @param color The color for the secondary toolbar.
445         */
446        public Builder setSecondaryToolbarColor(@ColorInt int color) {
447            mIntent.putExtra(EXTRA_SECONDARY_TOOLBAR_COLOR, color);
448            return this;
449        }
450
451        /**
452         * Sets the remote views displayed in the secondary toolbar in a custom tab.
453         *
454         * @param remoteViews   The {@link RemoteViews} that will be shown on the secondary toolbar.
455         * @param clickableIDs  The IDs of clickable views. The onClick event of these views will be
456         *                      handled by custom tabs.
457         * @param pendingIntent The {@link PendingIntent} that will be sent when the user clicks on
458         *                      one of the {@link View}s in clickableIDs. When the
459         *                      {@link PendingIntent} is sent, it will have the current URL as its
460         *                      intent data.
461         * @see CustomTabsIntent#EXTRA_REMOTEVIEWS
462         * @see CustomTabsIntent#EXTRA_REMOTEVIEWS_VIEW_IDS
463         * @see CustomTabsIntent#EXTRA_REMOTEVIEWS_PENDINGINTENT
464         * @see CustomTabsIntent#EXTRA_REMOTEVIEWS_CLICKED_ID
465         */
466        public Builder setSecondaryToolbarViews(@NonNull RemoteViews remoteViews,
467                @Nullable int[] clickableIDs, @Nullable PendingIntent pendingIntent) {
468            mIntent.putExtra(EXTRA_REMOTEVIEWS, remoteViews);
469            mIntent.putExtra(EXTRA_REMOTEVIEWS_VIEW_IDS, clickableIDs);
470            mIntent.putExtra(EXTRA_REMOTEVIEWS_PENDINGINTENT, pendingIntent);
471            return this;
472        }
473
474        /**
475         * Sets whether Instant Apps is enabled for this Custom Tab.
476
477         * @param enabled Whether Instant Apps should be enabled.
478         */
479        public Builder setInstantAppsEnabled(boolean enabled) {
480            mInstantAppsEnabled = enabled;
481            return this;
482        }
483
484        /**
485         * Sets the start animations.
486         *
487         * @param context Application context.
488         * @param enterResId Resource ID of the "enter" animation for the browser.
489         * @param exitResId Resource ID of the "exit" animation for the application.
490         */
491        public Builder setStartAnimations(
492                @NonNull Context context, @AnimRes int enterResId, @AnimRes int exitResId) {
493            mStartAnimationBundle = ActivityOptionsCompat.makeCustomAnimation(
494                    context, enterResId, exitResId).toBundle();
495            return this;
496        }
497
498        /**
499         * Sets the exit animations.
500         *
501         * @param context Application context.
502         * @param enterResId Resource ID of the "enter" animation for the application.
503         * @param exitResId Resource ID of the "exit" animation for the browser.
504         */
505        public Builder setExitAnimations(
506                @NonNull Context context, @AnimRes int enterResId, @AnimRes int exitResId) {
507            Bundle bundle = ActivityOptionsCompat.makeCustomAnimation(
508                    context, enterResId, exitResId).toBundle();
509            mIntent.putExtra(EXTRA_EXIT_ANIMATION_BUNDLE, bundle);
510            return this;
511        }
512
513        /**
514         * Combines all the options that have been set and returns a new {@link CustomTabsIntent}
515         * object.
516         */
517        public CustomTabsIntent build() {
518            if (mMenuItems != null) {
519                mIntent.putParcelableArrayListExtra(CustomTabsIntent.EXTRA_MENU_ITEMS, mMenuItems);
520            }
521            if (mActionButtons != null) {
522                mIntent.putParcelableArrayListExtra(EXTRA_TOOLBAR_ITEMS, mActionButtons);
523            }
524            mIntent.putExtra(EXTRA_ENABLE_INSTANT_APPS, mInstantAppsEnabled);
525            return new CustomTabsIntent(mIntent, mStartAnimationBundle);
526        }
527    }
528
529    /**
530     * @return The maximum number of allowed toolbar items for
531     * {@link CustomTabsIntent.Builder#addToolbarItem(int, Bitmap, String, PendingIntent)} and
532     * {@link CustomTabsIntent#EXTRA_TOOLBAR_ITEMS}.
533     */
534    public static int getMaxToolbarItems() {
535        return MAX_TOOLBAR_ITEMS;
536    }
537
538    /**
539     * Adds the necessary flags and extras to signal any browser supporting custom tabs to use the
540     * browser UI at all times and avoid showing custom tab like UI. Calling this with an intent
541     * will override any custom tabs related customizations.
542     * @param intent The intent to modify for always showing browser UI.
543     * @return The same intent with the necessary flags and extras added.
544     */
545    public static Intent setAlwaysUseBrowserUI(Intent intent) {
546        if (intent == null) intent = new Intent(Intent.ACTION_VIEW);
547        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
548        intent.putExtra(EXTRA_USER_OPT_OUT_FROM_CUSTOM_TABS, true);
549        return intent;
550    }
551
552    /**
553     * Whether a browser receiving the given intent should always use browser UI and avoid using any
554     * custom tabs UI.
555     *
556     * @param intent The intent to check for the required flags and extras.
557     * @return Whether the browser UI should be used exclusively.
558     */
559    public static boolean shouldAlwaysUseBrowserUI(Intent intent) {
560        return intent.getBooleanExtra(EXTRA_USER_OPT_OUT_FROM_CUSTOM_TABS, false)
561                && (intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0;
562    }
563}
564