Home | History | Annotate | Download | only in customtabs
      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 
     17 package androidx.browser.customtabs;
     18 
     19 import android.app.Service;
     20 import android.content.Intent;
     21 import android.net.Uri;
     22 import android.os.Bundle;
     23 import android.os.IBinder;
     24 import android.os.IBinder.DeathRecipient;
     25 import android.os.RemoteException;
     26 import android.support.customtabs.ICustomTabsCallback;
     27 import android.support.customtabs.ICustomTabsService;
     28 
     29 import androidx.annotation.IntDef;
     30 import androidx.collection.ArrayMap;
     31 
     32 import java.lang.annotation.Retention;
     33 import java.lang.annotation.RetentionPolicy;
     34 import java.util.List;
     35 import java.util.Map;
     36 import 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  */
     44 public 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