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.v4.util.ArrayMap; 27 28import java.util.List; 29import java.util.Map; 30import java.util.NoSuchElementException; 31 32/** 33 * Abstract service class for implementing Custom Tabs related functionality. The service should 34 * be responding to the action ACTION_CUSTOM_TABS_CONNECTION. This class should be used by 35 * implementers that want to provide Custom Tabs functionality, not by clients that want to launch 36 * Custom Tabs. 37 */ 38 public abstract class CustomTabsService extends Service { 39 /** 40 * The Intent action that a CustomTabsService must respond to. 41 */ 42 public static final String ACTION_CUSTOM_TABS_CONNECTION = 43 "android.support.customtabs.action.CustomTabsService"; 44 45 /** 46 * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url, 47 * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)} 48 * to insert a new url to each bundle inside list of bundles. 49 */ 50 public static final String KEY_URL = 51 "android.support.customtabs.otherurls.URL"; 52 53 private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>(); 54 55 private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() { 56 57 @Override 58 public boolean warmup(long flags) { 59 return CustomTabsService.this.warmup(flags); 60 } 61 62 @Override 63 public boolean newSession(ICustomTabsCallback callback) { 64 final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback); 65 try { 66 DeathRecipient deathRecipient = new IBinder.DeathRecipient() { 67 @Override 68 public void binderDied() { 69 cleanUpSession(sessionToken); 70 } 71 }; 72 synchronized (mDeathRecipientMap) { 73 callback.asBinder().linkToDeath(deathRecipient, 0); 74 mDeathRecipientMap.put(callback.asBinder(), deathRecipient); 75 } 76 return CustomTabsService.this.newSession(sessionToken); 77 } catch (RemoteException e) { 78 return false; 79 } 80 } 81 82 @Override 83 public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url, 84 Bundle extras, List<Bundle> otherLikelyBundles) { 85 return CustomTabsService.this.mayLaunchUrl( 86 new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles); 87 } 88 89 @Override 90 public Bundle extraCommand(String commandName, Bundle args) { 91 return CustomTabsService.this.extraCommand(commandName, args); 92 } 93 94 @Override 95 public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) { 96 return CustomTabsService.this.updateVisuals( 97 new CustomTabsSessionToken(callback), bundle); 98 } 99 }; 100 101 @Override 102 public IBinder onBind(Intent intent) { 103 return mBinder; 104 } 105 106 /** 107 * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead. 108 * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token. 109 * @param sessionToken The session token for which the {@link DeathRecipient} call has been 110 * received. 111 * @return Whether the clean up was successful. Multiple calls with two tokens holdings the 112 * same binder will return false. 113 */ 114 protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) { 115 try { 116 synchronized (mDeathRecipientMap) { 117 IBinder binder = sessionToken.getCallbackBinder(); 118 DeathRecipient deathRecipient = 119 mDeathRecipientMap.get(binder); 120 binder.unlinkToDeath(deathRecipient, 0); 121 mDeathRecipientMap.remove(binder); 122 } 123 } catch (NoSuchElementException e) { 124 return false; 125 } 126 return true; 127 } 128 129 /** 130 * Warms up the browser process asynchronously. 131 * 132 * @param flags Reserved for future use. 133 * @return Whether warmup was/had been completed successfully. Multiple successful 134 * calls will return true. 135 */ 136 protected abstract boolean warmup(long flags); 137 138 /** 139 * Creates a new session through an ICustomTabsService with the optional callback. This session 140 * can be used to associate any related communication through the service with an intent and 141 * then later with a Custom Tab. The client can then send later service calls or intents to 142 * through same session-intent-Custom Tab association. 143 * @param sessionToken Session token to be used as a unique identifier. This also has access 144 * to the {@link CustomTabsCallback} passed from the client side through 145 * {@link CustomTabsSessionToken#getCallback()}. 146 * @return Whether a new session was successfully created. 147 */ 148 protected abstract boolean newSession(CustomTabsSessionToken sessionToken); 149 150 /** 151 * Tells the browser of a likely future navigation to a URL. 152 * 153 * The method {@link CustomTabsService#warmup(long)} has to be called beforehand. 154 * The most likely URL has to be specified explicitly. Optionally, a list of 155 * other likely URLs can be provided. They are treated as less likely than 156 * the first one, and have to be sorted in decreasing priority order. These 157 * additional URLs may be ignored. 158 * All previous calls to this method will be deprioritized. 159 * 160 * @param sessionToken The unique identifier for the session. Can not be null. 161 * @param url Most likely URL. 162 * @param extras Reserved for future use. 163 * @param otherLikelyBundles Other likely destinations, sorted in decreasing 164 * likelihood order. Each Bundle has to provide a url. 165 * @return Whether the call was successful. 166 */ 167 protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url, 168 Bundle extras, List<Bundle> otherLikelyBundles); 169 170 /** 171 * Unsupported commands that may be provided by the implementation. 172 * 173 * <p> 174 * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a 175 * defined behavior, as it is entirely implementation-defined and not supported. 176 * 177 * <p> This call can be used by implementations to add extra commands, for testing or 178 * experimental purposes. 179 * 180 * @param commandName Name of the extra command to execute. 181 * @param args Arguments for the command 182 * @return The result {@link Bundle}, or null. 183 */ 184 protected abstract Bundle extraCommand(String commandName, Bundle args); 185 186 /** 187 * Updates the visuals of custom tabs for the given session. Will only succeed if the given 188 * session matches the currently active one. 189 * @param sessionToken The currently active session that the custom tab belongs to. 190 * @param bundle The action button configuration bundle. This bundle should be constructed 191 * with the same structure in {@link CustomTabsIntent.Builder}. 192 * @return Whether the operation was successful. 193 */ 194 protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken, 195 Bundle bundle); 196 } 197