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.PendingIntent;
20import android.content.ComponentName;
21import android.graphics.Bitmap;
22import android.net.Uri;
23import android.os.Bundle;
24import android.os.IBinder;
25import android.os.RemoteException;
26import android.support.customtabs.ICustomTabsCallback;
27import android.support.customtabs.ICustomTabsService;
28import android.view.View;
29import android.widget.RemoteViews;
30
31import androidx.annotation.NonNull;
32import androidx.annotation.Nullable;
33import androidx.annotation.VisibleForTesting;
34
35import java.util.List;
36
37/**
38 * A class to be used for Custom Tabs related communication. Clients that want to launch Custom Tabs
39 * can use this class exclusively to handle all related communication.
40 */
41public final class CustomTabsSession {
42    private static final String TAG = "CustomTabsSession";
43    private final Object mLock = new Object();
44    private final ICustomTabsService mService;
45    private final ICustomTabsCallback mCallback;
46    private final ComponentName mComponentName;
47
48    /**
49     * Provides browsers a way to generate a mock {@link CustomTabsSession} for testing
50     * purposes.
51     *
52     * @param componentName The component the session should be created for.
53     * @return A mock session with no functionality.
54     */
55    @VisibleForTesting
56    @NonNull
57    public static CustomTabsSession createMockSessionForTesting(
58            @NonNull ComponentName componentName) {
59        return new CustomTabsSession(
60                null, new CustomTabsSessionToken.MockCallback(), componentName);
61    }
62
63    /* package */ CustomTabsSession(
64            ICustomTabsService service, ICustomTabsCallback callback, ComponentName componentName) {
65        mService = service;
66        mCallback = callback;
67        mComponentName = componentName;
68    }
69
70    /**
71     * Tells the browser of a likely future navigation to a URL.
72     * The most likely URL has to be specified first. Optionally, a list of
73     * other likely URLs can be provided. They are treated as less likely than
74     * the first one, and have to be sorted in decreasing priority order. These
75     * additional URLs may be ignored.
76     * All previous calls to this method will be deprioritized.
77     *
78     * @param url                Most likely URL.
79     * @param extras             Reserved for future use.
80     * @param otherLikelyBundles Other likely destinations, sorted in decreasing
81     *                           likelihood order. Inside each Bundle, the client should provide a
82     *                           {@link Uri} using {@link CustomTabsService#KEY_URL} with
83     *                           {@link Bundle#putParcelable(String, android.os.Parcelable)}.
84     * @return                   true for success.
85     */
86    public boolean mayLaunchUrl(Uri url, Bundle extras, List<Bundle> otherLikelyBundles) {
87        try {
88            return mService.mayLaunchUrl(mCallback, url, extras, otherLikelyBundles);
89        } catch (RemoteException e) {
90            return false;
91        }
92    }
93
94    /**
95     * This sets the action button on the toolbar with ID
96     * {@link CustomTabsIntent#TOOLBAR_ACTION_BUTTON_ID}.
97     *
98     * @param icon          The new icon of the action button.
99     * @param description   Content description of the action button.
100     *
101     * @see CustomTabsSession#setToolbarItem(int, Bitmap, String)
102     */
103    public boolean setActionButton(@NonNull Bitmap icon, @NonNull String description) {
104        Bundle bundle = new Bundle();
105        bundle.putParcelable(CustomTabsIntent.KEY_ICON, icon);
106        bundle.putString(CustomTabsIntent.KEY_DESCRIPTION, description);
107
108        Bundle metaBundle = new Bundle();
109        metaBundle.putBundle(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, bundle);
110        try {
111            return mService.updateVisuals(mCallback, metaBundle);
112        } catch (RemoteException e) {
113            return false;
114        }
115    }
116
117    /**
118     * Updates the {@link RemoteViews} of the secondary toolbar in an existing custom tab session.
119     * @param remoteViews   The updated {@link RemoteViews} that will be shown in secondary toolbar.
120     *                      If null, the current secondary toolbar will be dismissed.
121     * @param clickableIDs  The ids of clickable views. The onClick event of these views will be
122     *                      handled by custom tabs.
123     * @param pendingIntent The {@link PendingIntent} that will be sent when the user clicks on one
124     *                      of the {@link View}s in clickableIDs.
125     */
126    public boolean setSecondaryToolbarViews(@Nullable RemoteViews remoteViews,
127            @Nullable int[] clickableIDs, @Nullable PendingIntent pendingIntent) {
128        Bundle bundle = new Bundle();
129        bundle.putParcelable(CustomTabsIntent.EXTRA_REMOTEVIEWS, remoteViews);
130        bundle.putIntArray(CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS, clickableIDs);
131        bundle.putParcelable(CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT, pendingIntent);
132        try {
133            return mService.updateVisuals(mCallback, bundle);
134        } catch (RemoteException e) {
135            return false;
136        }
137    }
138
139    /**
140     * Updates the visuals for toolbar items. Will only succeed if a custom tab created using this
141     * session is in the foreground in browser and the given id is valid.
142     * @param id            The id for the item to update.
143     * @param icon          The new icon of the toolbar item.
144     * @param description   Content description of the toolbar item.
145     * @return              Whether the update succeeded.
146     * @deprecated Use
147     * CustomTabsSession#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent)
148     */
149    @Deprecated
150    public boolean setToolbarItem(int id, @NonNull Bitmap icon, @NonNull String description) {
151        Bundle bundle = new Bundle();
152        bundle.putInt(CustomTabsIntent.KEY_ID, id);
153        bundle.putParcelable(CustomTabsIntent.KEY_ICON, icon);
154        bundle.putString(CustomTabsIntent.KEY_DESCRIPTION, description);
155
156        Bundle metaBundle = new Bundle();
157        metaBundle.putBundle(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, bundle);
158        try {
159            return mService.updateVisuals(mCallback, metaBundle);
160        } catch (RemoteException e) {
161            return false;
162        }
163    }
164
165    /**
166     * Sends a request to create a two way postMessage channel between the client and the browser.
167     *
168     * @param postMessageOrigin      A origin that the client is requesting to be identified as
169     *                               during the postMessage communication.
170     * @return Whether the implementation accepted the request. Note that returning true
171     *         here doesn't mean an origin has already been assigned as the validation is
172     *         asynchronous.
173     */
174    public boolean requestPostMessageChannel(Uri postMessageOrigin) {
175        try {
176            return mService.requestPostMessageChannel(
177                    mCallback, postMessageOrigin);
178        } catch (RemoteException e) {
179            return false;
180        }
181    }
182
183    /**
184     * Sends a postMessage request using the origin communicated via
185     * {@link CustomTabsService#requestPostMessageChannel(
186     * CustomTabsSessionToken, Uri)}. Fails when called before
187     * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on
188     * the client side.
189     *
190     * @param message The message that is being sent.
191     * @param extras Reserved for future use.
192     * @return An integer constant about the postMessage request result. Will return
193      *        {@link CustomTabsService#RESULT_SUCCESS} if successful.
194     */
195    @CustomTabsService.Result
196    public int postMessage(String message, Bundle extras) {
197        synchronized (mLock) {
198            try {
199                return mService.postMessage(mCallback, message, extras);
200            } catch (RemoteException e) {
201                return CustomTabsService.RESULT_FAILURE_REMOTE_ERROR;
202            }
203        }
204    }
205
206    /**
207     * Requests to validate a relationship between the application and an origin.
208     *
209     * <p>
210     * See <a href="https://developers.google.com/digital-asset-links/v1/getting-started">here</a>
211     * for documentation about Digital Asset Links. This methods requests the browser to verify
212     * a relation with the calling application, to grant the associated rights.
213     *
214     * <p>
215     * If this method returns {@code true}, the validation result will be provided through
216     * {@link CustomTabsCallback#onRelationshipValidationResult(int, Uri, boolean, Bundle)}.
217     * Otherwise the request didn't succeed. The client must call
218     * {@link CustomTabsClient#warmup(long)} before this.
219     *
220     * @param relation Relation to check, must be one of the {@code CustomTabsService#RELATION_* }
221     *                 constants.
222     * @param origin Origin.
223     * @param extras Reserved for future use.
224     * @return {@code true} if the request has been submitted successfully.
225     */
226    public boolean validateRelationship(@CustomTabsService.Relation int relation, @NonNull Uri origin,
227                                        @Nullable Bundle extras) {
228        if (relation < CustomTabsService.RELATION_USE_AS_ORIGIN
229                || relation > CustomTabsService.RELATION_HANDLE_ALL_URLS) {
230            return false;
231        }
232        try {
233            return mService.validateRelationship(mCallback, relation, origin, extras);
234        } catch (RemoteException e) {
235            return false;
236        }
237    }
238
239    /* package */ IBinder getBinder() {
240        return mCallback.asBinder();
241    }
242
243    /* package */ ComponentName getComponentName() {
244        return mComponentName;
245    }
246}
247