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