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