| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.browser.customtabs; |
| |
| import android.app.PendingIntent; |
| import android.app.Service; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.IBinder.DeathRecipient; |
| import android.os.RemoteException; |
| import android.support.customtabs.ICustomTabsCallback; |
| import android.support.customtabs.ICustomTabsService; |
| |
| import androidx.annotation.IntDef; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.collection.SimpleArrayMap; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.List; |
| import java.util.NoSuchElementException; |
| |
| /** |
| * Abstract service class for implementing Custom Tabs related functionality. The service should |
| * be responding to the action ACTION_CUSTOM_TABS_CONNECTION. This class should be used by |
| * implementers that want to provide Custom Tabs functionality, not by clients that want to launch |
| * Custom Tabs. |
| */ |
| public abstract class CustomTabsService extends Service { |
| /** |
| * The Intent action that a CustomTabsService must respond to. |
| */ |
| public static final String ACTION_CUSTOM_TABS_CONNECTION = |
| "android.support.customtabs.action.CustomTabsService"; |
| |
| /** |
| * An Intent filter category to signify that the Custom Tabs provider supports customizing |
| * the color of the navigation bar ({@link CustomTabsIntent.Builder#setNavigationBarColor}). |
| */ |
| public static final String CATEGORY_NAVBAR_COLOR_CUSTOMIZATION = |
| "androidx.browser.customtabs.category.NavBarColorCustomization"; |
| |
| /** |
| * An Intent filter category to signify that the Custom Tabs provider supports selecting and |
| * customizing color schemes via {@link CustomTabsIntent.Builder#setColorScheme} and |
| * {@link CustomTabsIntent.Builder#setColorSchemeParams}. |
| */ |
| public static final String CATEGORY_COLOR_SCHEME_CUSTOMIZATION = |
| "androidx.browser.customtabs.category.ColorSchemeCustomization"; |
| |
| /** |
| * An Intent filter category to signify that the Custom Tabs provider supports Trusted Web |
| * Activities (see {@link TrustedWebUtils} for more details). |
| */ |
| public static final String TRUSTED_WEB_ACTIVITY_CATEGORY = |
| "androidx.browser.trusted.category.TrustedWebActivities"; |
| |
| /** |
| * An Intent filter category to signify that the Trusted Web Activity provider supports |
| * sending shared data according to the Web Share Target v2 protocol defined in |
| * https://wicg.github.io/web-share-target/level-2/. |
| */ |
| public static final String CATEGORY_WEB_SHARE_TARGET_V2 = |
| "androidx.browser.trusted.category.WebShareTargetV2"; |
| |
| /** |
| * An Intent filter category to signify that the Trusted Web Activity provider supports |
| * immersive mode. |
| */ |
| public static final String CATEGORY_TRUSTED_WEB_ACTIVITY_IMMERSIVE_MODE = |
| "androidx.browser.trusted.category.ImmersiveMode"; |
| |
| /** |
| * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url, |
| * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)} |
| * to insert a new url to each bundle inside list of bundles. |
| */ |
| public static final String KEY_URL = |
| "android.support.customtabs.otherurls.URL"; |
| |
| /** |
| * The key to use to store a boolean in the returns bundle of {@link #extraCommand} method, |
| * to indicate the command is executed successfully. |
| */ |
| public static final String KEY_SUCCESS = "androidx.browser.customtabs.SUCCESS"; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({RESULT_SUCCESS, RESULT_FAILURE_DISALLOWED, |
| RESULT_FAILURE_REMOTE_ERROR, RESULT_FAILURE_MESSAGING_ERROR}) |
| public @interface Result { |
| } |
| |
| /** |
| * Indicates that the postMessage request was accepted. |
| */ |
| public static final int RESULT_SUCCESS = 0; |
| /** |
| * Indicates that the postMessage request was not allowed due to a bad argument or requesting |
| * at a disallowed time like when in background. |
| */ |
| public static final int RESULT_FAILURE_DISALLOWED = -1; |
| /** |
| * Indicates that the postMessage request has failed due to a {@link RemoteException} . |
| */ |
| public static final int RESULT_FAILURE_REMOTE_ERROR = -2; |
| /** |
| * Indicates that the postMessage request has failed due to an internal error on the browser |
| * message channel. |
| */ |
| public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({RELATION_USE_AS_ORIGIN, RELATION_HANDLE_ALL_URLS}) |
| public @interface Relation { |
| } |
| |
| /** |
| * Used for {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. For |
| * App -> Web transitions, requests the app to use the declared origin to be used as origin for |
| * the client app in the web APIs context. |
| */ |
| public static final int RELATION_USE_AS_ORIGIN = 1; |
| /** |
| * Used for {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. Requests the |
| * ability to handle all URLs from a given origin. |
| */ |
| public static final int RELATION_HANDLE_ALL_URLS = 2; |
| |
| |
| /** |
| * Enumerates the possible purposes of files received in {@link #receiveFile}. |
| * |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE}) |
| public @interface FilePurpose { |
| } |
| |
| /** |
| * A constant to be used with {@link CustomTabsSession#receiveFile} indicating that the file |
| * is a splash image to be shown on top of a Trusted Web Activity while the web contents |
| * are loading. |
| */ |
| public static final int FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE = 1; |
| |
| final SimpleArrayMap<IBinder, DeathRecipient> mDeathRecipientMap = new SimpleArrayMap<>(); |
| |
| private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() { |
| |
| @Override |
| public boolean warmup(long flags) { |
| return CustomTabsService.this.warmup(flags); |
| } |
| |
| @Override |
| public boolean newSession(@NonNull ICustomTabsCallback callback) { |
| return newSessionInternal(callback, null); |
| } |
| |
| @Override |
| public boolean newSessionWithExtras(@NonNull ICustomTabsCallback callback, |
| @Nullable Bundle extras) { |
| return newSessionInternal(callback, getSessionIdFromBundle(extras)); |
| } |
| |
| private boolean newSessionInternal(@NonNull ICustomTabsCallback callback, |
| @Nullable PendingIntent sessionId) { |
| final CustomTabsSessionToken sessionToken = |
| new CustomTabsSessionToken(callback, sessionId); |
| try { |
| DeathRecipient deathRecipient = () -> cleanUpSession(sessionToken); |
| synchronized (mDeathRecipientMap) { |
| callback.asBinder().linkToDeath(deathRecipient, 0); |
| mDeathRecipientMap.put(callback.asBinder(), deathRecipient); |
| } |
| return CustomTabsService.this.newSession(sessionToken); |
| } catch (RemoteException e) { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean mayLaunchUrl(@Nullable ICustomTabsCallback callback, @Nullable Uri url, |
| @Nullable Bundle extras, @Nullable List<Bundle> otherLikelyBundles) { |
| return CustomTabsService.this.mayLaunchUrl( |
| new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)), |
| url, extras, otherLikelyBundles); |
| } |
| |
| @SuppressWarnings("NullAway") // TODO: b/142938599 |
| @Override |
| public Bundle extraCommand(@NonNull String commandName, @Nullable Bundle args) { |
| return CustomTabsService.this.extraCommand(commandName, args); |
| } |
| |
| @Override |
| public boolean updateVisuals(@NonNull ICustomTabsCallback callback, |
| @Nullable Bundle bundle) { |
| return CustomTabsService.this.updateVisuals( |
| new CustomTabsSessionToken(callback, getSessionIdFromBundle(bundle)), bundle); |
| } |
| |
| @Override |
| public boolean requestPostMessageChannel(@NonNull ICustomTabsCallback callback, |
| @NonNull Uri postMessageOrigin) { |
| return CustomTabsService.this.requestPostMessageChannel( |
| new CustomTabsSessionToken(callback, null), postMessageOrigin); |
| } |
| |
| @Override |
| public boolean requestPostMessageChannelWithExtras(@NonNull ICustomTabsCallback callback, |
| @NonNull Uri postMessageOrigin, @NonNull Bundle extras) { |
| return CustomTabsService.this.requestPostMessageChannel( |
| new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)), |
| postMessageOrigin); |
| } |
| |
| @Override |
| public int postMessage(@NonNull ICustomTabsCallback callback, @NonNull String message, |
| @Nullable Bundle extras) { |
| return CustomTabsService.this.postMessage( |
| new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)), |
| message, extras); |
| } |
| |
| @Override |
| public boolean validateRelationship( |
| @NonNull ICustomTabsCallback callback, @Relation int relation, |
| @NonNull Uri origin, @Nullable Bundle extras) { |
| return CustomTabsService.this.validateRelationship( |
| new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)), |
| relation, origin, extras); |
| } |
| |
| @Override |
| public boolean receiveFile(@NonNull ICustomTabsCallback callback, @NonNull Uri uri, |
| @FilePurpose int purpose, @Nullable Bundle extras) { |
| return CustomTabsService.this.receiveFile( |
| new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)), |
| uri, purpose, extras); |
| } |
| |
| private @Nullable PendingIntent getSessionIdFromBundle(@Nullable Bundle bundle) { |
| if (bundle == null) return null; |
| |
| PendingIntent sessionId = bundle.getParcelable(CustomTabsIntent.EXTRA_SESSION_ID); |
| bundle.remove(CustomTabsIntent.EXTRA_SESSION_ID); |
| return sessionId; |
| } |
| }; |
| |
| @Override |
| @NonNull |
| public IBinder onBind(@Nullable Intent intent) { |
| return mBinder; |
| } |
| |
| /** |
| * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead. |
| * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token. |
| * |
| * @param sessionToken The session token for which the {@link DeathRecipient} call has been |
| * received. |
| * @return Whether the clean up was successful. Multiple calls with two tokens holdings the |
| * same binder will return false. |
| */ |
| protected boolean cleanUpSession(@NonNull CustomTabsSessionToken sessionToken) { |
| try { |
| synchronized (mDeathRecipientMap) { |
| IBinder binder = sessionToken.getCallbackBinder(); |
| if (binder == null) return false; |
| DeathRecipient deathRecipient = mDeathRecipientMap.get(binder); |
| binder.unlinkToDeath(deathRecipient, 0); |
| mDeathRecipientMap.remove(binder); |
| } |
| } catch (NoSuchElementException e) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Warms up the browser process asynchronously. |
| * |
| * @param flags Reserved for future use. |
| * @return Whether warmup was/had been completed successfully. Multiple successful |
| * calls will return true. |
| */ |
| protected abstract boolean warmup(long flags); |
| |
| /** |
| * Creates a new session through an ICustomTabsService with the optional callback. This session |
| * can be used to associate any related communication through the service with an intent and |
| * then later with a Custom Tab. The client can then send later service calls or intents to |
| * through same session-intent-Custom Tab association. |
| * |
| * @param sessionToken Session token to be used as a unique identifier. This also has access |
| * to the {@link CustomTabsCallback} passed from the client side through |
| * {@link CustomTabsSessionToken#getCallback()}. |
| * @return Whether a new session was successfully created. |
| */ |
| protected abstract boolean newSession(@NonNull CustomTabsSessionToken sessionToken); |
| |
| /** |
| * Tells the browser of a likely future navigation to a URL. |
| * <p> |
| * The method {@link CustomTabsService#warmup(long)} has to be called beforehand. |
| * The most likely URL has to be specified explicitly. Optionally, a list of |
| * other likely URLs can be provided. They are treated as less likely than |
| * the first one, and have to be sorted in decreasing priority order. These |
| * additional URLs may be ignored. |
| * All previous calls to this method will be deprioritized. |
| * |
| * @param sessionToken The unique identifier for the session. Can not be null. |
| * @param url Most likely URL. |
| * @param extras Reserved for future use. |
| * @param otherLikelyBundles Other likely destinations, sorted in decreasing |
| * likelihood order. Each Bundle has to provide a url. |
| * @return Whether the call was successful. |
| */ |
| protected abstract boolean mayLaunchUrl(@NonNull CustomTabsSessionToken sessionToken, |
| @Nullable Uri url, @Nullable Bundle extras, @Nullable List<Bundle> otherLikelyBundles); |
| |
| /** |
| * Unsupported commands that may be provided by the implementation. |
| * <p> |
| * <p> |
| * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a |
| * defined behavior, as it is entirely implementation-defined and not supported. |
| * <p> |
| * <p> This call can be used by implementations to add extra commands, for testing or |
| * experimental purposes. |
| * |
| * A return value of {@code null} will be used to signify that the client does not know how to |
| * handle the request. |
| * |
| * As optional best practices, {@link #KEY_SUCCESS} could be use to identify |
| * that command was *successfully* handled. For example, when returning a message with result: |
| * <pre><code> |
| * Bundle result = new Bundle(); |
| * result.putString("message", message); |
| * if (success) |
| * result.putBoolean(KEY_SUCCESS, true); |
| * return result; |
| * </code></pre> |
| * The caller side: |
| * <pre><code> |
| * Bundle result = service.extraCommand(commandName, args); |
| * if (result.getBoolean(service.KEY_SUCCESS)) { |
| * // Command was successfully handled |
| * } |
| * </code></pre> |
| * |
| * @param commandName Name of the extra command to execute. |
| * @param args Arguments for the command |
| * @return The result {@link Bundle}, or {@code null}. |
| */ |
| @Nullable |
| protected abstract Bundle extraCommand(@NonNull String commandName, @Nullable Bundle args); |
| |
| /** |
| * Updates the visuals of custom tabs for the given session. Will only succeed if the given |
| * session matches the currently active one. |
| * |
| * @param sessionToken The currently active session that the custom tab belongs to. |
| * @param bundle The action button configuration bundle. This bundle should be constructed |
| * with the same structure in {@link CustomTabsIntent.Builder}. |
| * @return Whether the operation was successful. |
| */ |
| protected abstract boolean updateVisuals(@NonNull CustomTabsSessionToken sessionToken, |
| @Nullable Bundle bundle); |
| |
| /** |
| * Sends a request to create a two way postMessage channel between the client and the browser |
| * linked with the given {@link CustomTabsSession}. |
| * |
| * @param sessionToken The unique identifier for the session. Can not be null. |
| * @param postMessageOrigin A origin that the client is requesting to be identified as |
| * during the postMessage communication. |
| * @return Whether the implementation accepted the request. Note that returning true |
| * here doesn't mean an origin has already been assigned as the validation is |
| * asynchronous. |
| */ |
| protected abstract boolean requestPostMessageChannel( |
| @NonNull CustomTabsSessionToken sessionToken, @NonNull Uri postMessageOrigin); |
| |
| /** |
| * Sends a postMessage request using the origin communicated via |
| * {@link CustomTabsService#requestPostMessageChannel( |
| *CustomTabsSessionToken, Uri)}. Fails when called before |
| * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on the |
| * client side. |
| * |
| * @param sessionToken The unique identifier for the session. Can not be null. |
| * @param message The message that is being sent. |
| * @param extras Reserved for future use. |
| * @return An integer constant about the postMessage request result. Will return |
| * {@link CustomTabsService#RESULT_SUCCESS} if successful. |
| */ |
| @Result |
| protected abstract int postMessage(@NonNull CustomTabsSessionToken sessionToken, |
| @NonNull String message, @Nullable Bundle extras); |
| |
| /** |
| * Request to validate a relationship between the application and an origin. |
| * |
| * If this method returns true, the validation result will be provided through |
| * {@link CustomTabsCallback#onRelationshipValidationResult(int, Uri, boolean, Bundle)}. |
| * Otherwise the request didn't succeed. The client must call |
| * {@link CustomTabsClient#warmup(long)} before this. |
| * |
| * @param sessionToken The unique identifier for the session. Can not be null. |
| * @param relation Relation to check, must be one of the {@code CustomTabsService#RELATION_* } |
| * constants. |
| * @param origin Origin for the relation query. |
| * @param extras Reserved for future use. |
| * @return true if the request has been submitted successfully. |
| */ |
| protected abstract boolean validateRelationship(@NonNull CustomTabsSessionToken sessionToken, |
| @Relation int relation, @NonNull Uri origin, @Nullable Bundle extras); |
| |
| /** |
| * Receive a file from client by given Uri, e.g. in order to display a large bitmap in a Custom |
| * Tab. |
| * |
| * Prior to calling this method, the client grants a read permission to the target |
| * Custom Tabs provider via {@link android.content.Context#grantUriPermission}. |
| * |
| * The file is read and processed (where applicable) synchronously. |
| * |
| * @param sessionToken The unique identifier for the session. |
| * @param uri {@link Uri} of the file. |
| * @param purpose Purpose of transferring this file, one of the constants enumerated in |
| * {@code CustomTabsService#FilePurpose}. |
| * @param extras Reserved for future use. |
| * @return {@code true} if the file was received successfully. |
| */ |
| protected abstract boolean receiveFile(@NonNull CustomTabsSessionToken sessionToken, |
| @NonNull Uri uri, @FilePurpose int purpose, @Nullable Bundle extras); |
| } |