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