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.Service;
20import android.content.Intent;
21import android.net.Uri;
22import android.os.Bundle;
23import android.os.IBinder;
24import android.os.IBinder.DeathRecipient;
25import android.os.RemoteException;
26import android.support.customtabs.ICustomTabsCallback;
27import android.support.customtabs.ICustomTabsService;
28
29import androidx.annotation.IntDef;
30import androidx.collection.ArrayMap;
31
32import java.lang.annotation.Retention;
33import java.lang.annotation.RetentionPolicy;
34import java.util.List;
35import java.util.Map;
36import java.util.NoSuchElementException;
37
38/**
39 * Abstract service class for implementing Custom Tabs related functionality. The service should
40 * be responding to the action ACTION_CUSTOM_TABS_CONNECTION. This class should be used by
41 * implementers that want to provide Custom Tabs functionality, not by clients that want to launch
42 * Custom Tabs.
43 */
44public abstract class CustomTabsService extends Service {
45    /**
46     * The Intent action that a CustomTabsService must respond to.
47     */
48    public static final String ACTION_CUSTOM_TABS_CONNECTION =
49            "android.support.customtabs.action.CustomTabsService";
50
51    /**
52     * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
53     * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
54     * to insert a new url to each bundle inside list of bundles.
55     */
56    public static final String KEY_URL =
57            "android.support.customtabs.otherurls.URL";
58
59    @Retention(RetentionPolicy.SOURCE)
60    @IntDef({RESULT_SUCCESS, RESULT_FAILURE_DISALLOWED,
61            RESULT_FAILURE_REMOTE_ERROR, RESULT_FAILURE_MESSAGING_ERROR})
62    public @interface Result {
63    }
64
65    /**
66     * Indicates that the postMessage request was accepted.
67     */
68    public static final int RESULT_SUCCESS = 0;
69    /**
70     * Indicates that the postMessage request was not allowed due to a bad argument or requesting
71     * at a disallowed time like when in background.
72     */
73    public static final int RESULT_FAILURE_DISALLOWED = -1;
74    /**
75     * Indicates that the postMessage request has failed due to a {@link RemoteException} .
76     */
77    public static final int RESULT_FAILURE_REMOTE_ERROR = -2;
78    /**
79     * Indicates that the postMessage request has failed due to an internal error on the browser
80     * message channel.
81     */
82    public static final int RESULT_FAILURE_MESSAGING_ERROR = -3;
83
84    @Retention(RetentionPolicy.SOURCE)
85    @IntDef({RELATION_USE_AS_ORIGIN, RELATION_HANDLE_ALL_URLS})
86    public @interface Relation {
87    }
88
89    /**
90     * Used for {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. For
91     * App -> Web transitions, requests the app to use the declared origin to be used as origin for
92     * the client app in the web APIs context.
93     */
94    public static final int RELATION_USE_AS_ORIGIN = 1;
95    /**
96     * Used for {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. Requests the
97     * ability to handle all URLs from a given origin.
98     */
99    public static final int RELATION_HANDLE_ALL_URLS = 2;
100
101    private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
102
103    private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
104
105        @Override
106        public boolean warmup(long flags) {
107            return CustomTabsService.this.warmup(flags);
108        }
109
110        @Override
111        public boolean newSession(ICustomTabsCallback callback) {
112            final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
113            try {
114                DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
115                    @Override
116                    public void binderDied() {
117                        cleanUpSession(sessionToken);
118                    }
119                };
120                synchronized (mDeathRecipientMap) {
121                    callback.asBinder().linkToDeath(deathRecipient, 0);
122                    mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
123                }
124                return CustomTabsService.this.newSession(sessionToken);
125            } catch (RemoteException e) {
126                return false;
127            }
128        }
129
130        @Override
131        public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
132                                    Bundle extras, List<Bundle> otherLikelyBundles) {
133            return CustomTabsService.this.mayLaunchUrl(
134                    new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
135        }
136
137        @Override
138        public Bundle extraCommand(String commandName, Bundle args) {
139            return CustomTabsService.this.extraCommand(commandName, args);
140        }
141
142        @Override
143        public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
144            return CustomTabsService.this.updateVisuals(
145                    new CustomTabsSessionToken(callback), bundle);
146        }
147
148        @Override
149        public boolean requestPostMessageChannel(ICustomTabsCallback callback,
150                                                 Uri postMessageOrigin) {
151            return CustomTabsService.this.requestPostMessageChannel(
152                    new CustomTabsSessionToken(callback), postMessageOrigin);
153        }
154
155        @Override
156        public int postMessage(ICustomTabsCallback callback, String message, Bundle extras) {
157            return CustomTabsService.this.postMessage(
158                    new CustomTabsSessionToken(callback), message, extras);
159        }
160
161        @Override
162        public boolean validateRelationship(
163                ICustomTabsCallback callback, @Relation int relation, Uri origin, Bundle extras) {
164            return CustomTabsService.this.validateRelationship(
165                    new CustomTabsSessionToken(callback), relation, origin, extras);
166        }
167    };
168
169    @Override
170    public IBinder onBind(Intent intent) {
171        return mBinder;
172    }
173
174    /**
175     * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead.
176     * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
177     *
178     * @param sessionToken The session token for which the {@link DeathRecipient} call has been
179     *                     received.
180     * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
181     * same binder will return false.
182     */
183    protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) {
184        try {
185            synchronized (mDeathRecipientMap) {
186                IBinder binder = sessionToken.getCallbackBinder();
187                DeathRecipient deathRecipient =
188                        mDeathRecipientMap.get(binder);
189                binder.unlinkToDeath(deathRecipient, 0);
190                mDeathRecipientMap.remove(binder);
191            }
192        } catch (NoSuchElementException e) {
193            return false;
194        }
195        return true;
196    }
197
198    /**
199     * Warms up the browser process asynchronously.
200     *
201     * @param flags Reserved for future use.
202     * @return Whether warmup was/had been completed successfully. Multiple successful
203     * calls will return true.
204     */
205    protected abstract boolean warmup(long flags);
206
207    /**
208     * Creates a new session through an ICustomTabsService with the optional callback. This session
209     * can be used to associate any related communication through the service with an intent and
210     * then later with a Custom Tab. The client can then send later service calls or intents to
211     * through same session-intent-Custom Tab association.
212     *
213     * @param sessionToken Session token to be used as a unique identifier. This also has access
214     *                     to the {@link CustomTabsCallback} passed from the client side through
215     *                     {@link CustomTabsSessionToken#getCallback()}.
216     * @return Whether a new session was successfully created.
217     */
218    protected abstract boolean newSession(CustomTabsSessionToken sessionToken);
219
220    /**
221     * Tells the browser of a likely future navigation to a URL.
222     * <p>
223     * The method {@link CustomTabsService#warmup(long)} has to be called beforehand.
224     * The most likely URL has to be specified explicitly. Optionally, a list of
225     * other likely URLs can be provided. They are treated as less likely than
226     * the first one, and have to be sorted in decreasing priority order. These
227     * additional URLs may be ignored.
228     * All previous calls to this method will be deprioritized.
229     *
230     * @param sessionToken       The unique identifier for the session. Can not be null.
231     * @param url                Most likely URL.
232     * @param extras             Reserved for future use.
233     * @param otherLikelyBundles Other likely destinations, sorted in decreasing
234     *                           likelihood order. Each Bundle has to provide a url.
235     * @return Whether the call was successful.
236     */
237    protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
238                                            Bundle extras, List<Bundle> otherLikelyBundles);
239
240    /**
241     * Unsupported commands that may be provided by the implementation.
242     * <p>
243     * <p>
244     * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
245     * defined behavior, as it is entirely implementation-defined and not supported.
246     * <p>
247     * <p> This call can be used by implementations to add extra commands, for testing or
248     * experimental purposes.
249     *
250     * @param commandName Name of the extra command to execute.
251     * @param args        Arguments for the command
252     * @return The result {@link Bundle}, or null.
253     */
254    protected abstract Bundle extraCommand(String commandName, Bundle args);
255
256    /**
257     * Updates the visuals of custom tabs for the given session. Will only succeed if the given
258     * session matches the currently active one.
259     *
260     * @param sessionToken The currently active session that the custom tab belongs to.
261     * @param bundle       The action button configuration bundle. This bundle should be constructed
262     *                     with the same structure in {@link CustomTabsIntent.Builder}.
263     * @return Whether the operation was successful.
264     */
265    protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
266                                             Bundle bundle);
267
268    /**
269     * Sends a request to create a two way postMessage channel between the client and the browser
270     * linked with the given {@link CustomTabsSession}.
271     *
272     * @param sessionToken      The unique identifier for the session. Can not be null.
273     * @param postMessageOrigin A origin that the client is requesting to be identified as
274     *                          during the postMessage communication.
275     * @return Whether the implementation accepted the request. Note that returning true
276     * here doesn't mean an origin has already been assigned as the validation is
277     * asynchronous.
278     */
279    protected abstract boolean requestPostMessageChannel(CustomTabsSessionToken sessionToken,
280                                                         Uri postMessageOrigin);
281
282    /**
283     * Sends a postMessage request using the origin communicated via
284     * {@link CustomTabsService#requestPostMessageChannel(
285     *CustomTabsSessionToken, Uri)}. Fails when called before
286     * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on the
287     * client side.
288     *
289     * @param sessionToken The unique identifier for the session. Can not be null.
290     * @param message      The message that is being sent.
291     * @param extras       Reserved for future use.
292     * @return An integer constant about the postMessage request result. Will return
293     * {@link CustomTabsService#RESULT_SUCCESS} if successful.
294     */
295    @Result
296    protected abstract int postMessage(
297            CustomTabsSessionToken sessionToken, String message, Bundle extras);
298
299    /**
300     * Request to validate a relationship between the application and an origin.
301     *
302     * If this method returns true, the validation result will be provided through
303     * {@link CustomTabsCallback#onRelationshipValidationResult(int, Uri, boolean, Bundle)}.
304     * Otherwise the request didn't succeed. The client must call
305     * {@link CustomTabsClient#warmup(long)} before this.
306     *
307     * @param sessionToken The unique identifier for the session. Can not be null.
308     * @param relation Relation to check, must be one of the {@code CustomTabsService#RELATION_* }
309     *                 constants.
310     * @param origin Origin for the relation query.
311     * @param extras Reserved for future use.
312     * @return true if the request has been submitted successfully.
313     */
314    protected abstract boolean validateRelationship(
315            CustomTabsSessionToken sessionToken, @Relation int relation, Uri origin,
316            Bundle extras);
317}
318