public final class

CustomTabsSession

extends java.lang.Object

 java.lang.Object

↳androidx.browser.customtabs.CustomTabsSession

Gradle dependencies

compile group: 'androidx.browser', name: 'browser', version: '1.8.0'

  • groupId: androidx.browser
  • artifactId: browser
  • version: 1.8.0

Artifact androidx.browser:browser:1.8.0 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.browser:browser com.android.support:customtabs

Androidx class mapping:

androidx.browser.customtabs.CustomTabsSession android.support.customtabs.CustomTabsSession

Overview

A class to be used for Custom Tabs related communication. Clients that want to launch Custom Tabs can use this class exclusively to handle all related communication.

Summary

Methods
public static CustomTabsSessioncreateMockSessionForTesting(ComponentName componentName)

Provides browsers a way to generate a mock CustomTabsSession for testing purposes.

public booleanisEngagementSignalsApiAvailable(Bundle extras)

Returns whether the Engagement Signals API is available.

public booleanmayLaunchUrl(Uri url, Bundle extras, java.util.List<Bundle> otherLikelyBundles)

Tells the browser of a likely future navigation to a URL.

public intpostMessage(java.lang.String message, Bundle extras)

Sends a postMessage request using the origin communicated via CustomTabsService.requestPostMessageChannel(CustomTabsSessionToken, Uri).

public booleanreceiveFile(Uri uri, int purpose, Bundle extras)

Passes an URI of a file, e.g.

public booleanrequestPostMessageChannel(Uri postMessageOrigin)

Sends a request to create a two way postMessage channel between the client and the browser.

public booleanrequestPostMessageChannel(Uri postMessageOrigin, Uri postMessageTargetOrigin, Bundle extras)

Sends a request to create a two way postMessage channel between the client and the browser with specifying the target origin to communicate with.

public booleansetActionButton(Bitmap icon, java.lang.String description)

This sets the action button on the toolbar with ID CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID.

public booleansetEngagementSignalsCallback(EngagementSignalsCallback callback, Bundle extras)

Sets an EngagementSignalsCallback to receive callbacks for events related to the user's engagement with webpage within the tab.

public booleansetEngagementSignalsCallback(java.util.concurrent.Executor executor, EngagementSignalsCallback callback, Bundle extras)

Sets an EngagementSignalsCallback to receive callbacks for events related to the user's engagement with webpage within the tab.

public booleansetSecondaryToolbarSwipeUpGesture(PendingIntent pendingIntent)

Sets a PendingIntent object to be sent when the user swipes up from the secondary (bottom) toolbar.

public booleansetSecondaryToolbarViews(RemoteViews remoteViews, int[] clickableIDs[], PendingIntent pendingIntent)

Updates the of the secondary toolbar in an existing custom tab session.

public booleansetToolbarItem(int id, Bitmap icon, java.lang.String description)

Updates the visuals for toolbar items.

public booleanvalidateRelationship(int relation, Uri origin, Bundle extras)

Requests to validate a relationship between the application and an origin.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static CustomTabsSession createMockSessionForTesting(ComponentName componentName)

Provides browsers a way to generate a mock CustomTabsSession for testing purposes.

Parameters:

componentName: The component the session should be created for.

Returns:

A mock session with no functionality.

public boolean mayLaunchUrl(Uri url, Bundle extras, java.util.List<Bundle> otherLikelyBundles)

Tells the browser of a likely future navigation to a URL. The most likely URL has to be specified first. 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.

Parameters:

url: Most likely URL, may be null if otherLikelyBundles is provided.
extras: Reserved for future use.
otherLikelyBundles: Other likely destinations, sorted in decreasing likelihood order. Inside each Bundle, the client should provide a using CustomTabsService.KEY_URL with .

Returns:

true for success.

public boolean setActionButton(Bitmap icon, java.lang.String description)

This sets the action button on the toolbar with ID CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID.

Parameters:

icon: The new icon of the action button.
description: Content description of the action button.

See also: CustomTabsSession.setToolbarItem(int, Bitmap, String)

public boolean setSecondaryToolbarViews(RemoteViews remoteViews, int[] clickableIDs[], PendingIntent pendingIntent)

Updates the of the secondary toolbar in an existing custom tab session.

Parameters:

remoteViews: The updated that will be shown in secondary toolbar. If null, the current secondary toolbar will be dismissed.
clickableIDs: The ids of clickable views. The onClick event of these views will be handled by custom tabs.
pendingIntent: The PendingIntent that will be sent when the user clicks on one of the Views in clickableIDs.

public boolean setSecondaryToolbarSwipeUpGesture(PendingIntent pendingIntent)

Sets a PendingIntent object to be sent when the user swipes up from the secondary (bottom) toolbar.

Parameters:

pendingIntent: PendingIntent to send.

Returns:

Whether the update succeeded.

public boolean setToolbarItem(int id, Bitmap icon, java.lang.String description)

Deprecated: Use CustomTabsSession#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent)

Updates the visuals for toolbar items. Will only succeed if a custom tab created using this session is in the foreground in browser and the given id is valid.

Parameters:

id: The id for the item to update.
icon: The new icon of the toolbar item.
description: Content description of the toolbar item.

Returns:

Whether the update succeeded.

public boolean requestPostMessageChannel(Uri postMessageOrigin)

Sends a request to create a two way postMessage channel between the client and the browser.

Parameters:

postMessageOrigin: A origin that the client is requesting to be identified as during the postMessage communication.

Returns:

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.

public boolean requestPostMessageChannel(Uri postMessageOrigin, Uri postMessageTargetOrigin, Bundle extras)

Sends a request to create a two way postMessage channel between the client and the browser with specifying the target origin to communicate with.

Parameters:

postMessageOrigin: A origin that the client is requesting to be identified as during the postMessage communication.
postMessageTargetOrigin: The target Origin to establish the postMessage communication with.
extras: Reserved for future use.

Returns:

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.

public int postMessage(java.lang.String message, Bundle extras)

Sends a postMessage request using the origin communicated via CustomTabsService.requestPostMessageChannel(CustomTabsSessionToken, Uri). Fails when called before PostMessageServiceConnection.notifyMessageChannelReady(Bundle) is received on the client side.

Parameters:

message: The message that is being sent.
extras: Reserved for future use.

Returns:

An integer constant about the postMessage request result. Will return CustomTabsService.RESULT_SUCCESS if successful.

public boolean validateRelationship(int relation, Uri origin, Bundle extras)

Requests to validate a relationship between the application and an origin.

See here for documentation about Digital Asset Links. This methods requests the browser to verify a relation with the calling application, to grant the associated rights.

If this method returns true, the validation result will be provided through CustomTabsCallback.onRelationshipValidationResult(int, Uri, boolean, Bundle). Otherwise the request didn't succeed. The client must call CustomTabsClient.warmup(long) before this.

Parameters:

relation: Relation to check, must be one of the CustomTabsService#RELATION_* constants.
origin: Origin.
extras: Reserved for future use.

Returns:

true if the request has been submitted successfully.

public boolean receiveFile(Uri uri, int purpose, Bundle extras)

Passes an URI of a file, e.g. in order to pass a large bitmap to be displayed in the Custom Tabs provider. Prior to calling this method, the client needs to grant a read permission to the target Custom Tabs provider via . The file is read and processed (where applicable) synchronously, therefore it's recommended to call this method on a background thread.

Parameters:

uri: of the file.
purpose: Purpose of transferring this file, one of the constants enumerated in CustomTabsService#FilePurpose.
extras: Reserved for future use.

Returns:

true if the file was received successfully.

public boolean isEngagementSignalsApiAvailable(Bundle extras)

Returns whether the Engagement Signals API is available. The availability of the Engagement Signals API may change at runtime. If an EngagementSignalsCallback has been set, an EngagementSignalsCallback.onSessionEnded(boolean, Bundle) signal will be sent if the API becomes unavailable later.

Parameters:

extras: Reserved for future use.

Returns:

Whether the Engagement Signals API is available. A false value means CustomTabsSession.setEngagementSignalsCallback(EngagementSignalsCallback, Bundle) will return false and not set the callback.

public boolean setEngagementSignalsCallback(EngagementSignalsCallback callback, Bundle extras)

Sets an EngagementSignalsCallback to receive callbacks for events related to the user's engagement with webpage within the tab. Note that the callback will be executed on the main thread using . To specify the execution thread, use CustomTabsSession.setEngagementSignalsCallback(Executor, EngagementSignalsCallback, Bundle).

Parameters:

callback: The EngagementSignalsCallback to receive the user engagement signals.
extras: Reserved for future use.

Returns:

Whether the callback connection is allowed. If false, no callbacks will be called for this session.

public boolean setEngagementSignalsCallback(java.util.concurrent.Executor executor, EngagementSignalsCallback callback, Bundle extras)

Sets an EngagementSignalsCallback to receive callbacks for events related to the user's engagement with webpage within the tab.

Parameters:

executor: The java.util.concurrent.Executor to be used to execute the callbacks.
callback: The EngagementSignalsCallback to receive the user engagement signals.
extras: Reserved for future use.

Returns:

Whether the callback connection is allowed. If false, no callbacks will be called for this session.

Source

/*
 * 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.content.ComponentName;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.support.customtabs.ICustomTabsCallback;
import android.support.customtabs.ICustomTabsService;
import android.support.customtabs.IEngagementSignalsCallback;
import android.view.View;
import android.widget.RemoteViews;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsService.Relation;
import androidx.browser.customtabs.CustomTabsService.Result;

import java.util.List;
import java.util.concurrent.Executor;

/**
 * A class to be used for Custom Tabs related communication. Clients that want to launch Custom Tabs
 * can use this class exclusively to handle all related communication.
 */
public final class CustomTabsSession {
    private static final String TAG = "CustomTabsSession";
    static final String TARGET_ORIGIN_KEY = "target_origin";
    private final Object mLock = new Object();
    private final ICustomTabsService mService;
    private final ICustomTabsCallback mCallback;
    private final ComponentName mComponentName;

    /**
     * The session ID is represented by {@link PendingIntent}. Other apps cannot
     * forge {@link PendingIntent}. The {@link PendingIntent#equals(Object)} method
     * considers two {@link PendingIntent} objects equal if their action, data, type,
     * class and category are the same (even across a process being killed).
     *
     * {@see Intent#filterEquals()}
     */
    @Nullable
    private final PendingIntent mId;

    /**
     * Provides browsers a way to generate a mock {@link CustomTabsSession} for testing
     * purposes.
     *
     * @param componentName The component the session should be created for.
     * @return A mock session with no functionality.
     */
    @VisibleForTesting
    @NonNull
    public static CustomTabsSession createMockSessionForTesting(
            @NonNull ComponentName componentName) {
        return new CustomTabsSession(
                new MockSession(), new CustomTabsSessionToken.MockCallback(), componentName, null);
    }

    /* package */ CustomTabsSession(
            ICustomTabsService service, ICustomTabsCallback callback, ComponentName componentName,
            @Nullable PendingIntent sessionId) {
        mService = service;
        mCallback = callback;
        mComponentName = componentName;
        mId = sessionId;
    }

    /**
     * Tells the browser of a likely future navigation to a URL.
     * The most likely URL has to be specified first. 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 url                Most likely URL, may be {@code null} if {@code otherLikelyBundles}
     *                           is provided.
     * @param extras             Reserved for future use.
     * @param otherLikelyBundles Other likely destinations, sorted in decreasing
     *                           likelihood order. Inside each Bundle, the client should provide a
     *                           {@link Uri} using {@link CustomTabsService#KEY_URL} with
     *                           {@link Bundle#putParcelable(String, android.os.Parcelable)}.
     * @return true for success.
     */
    @SuppressWarnings("NullAway")  // TODO: b/142938599
    public boolean mayLaunchUrl(@Nullable Uri url, @Nullable Bundle extras,
            @Nullable List<Bundle> otherLikelyBundles) {
        extras = createBundleWithId(extras);
        try {
            return mService.mayLaunchUrl(mCallback, url, extras, otherLikelyBundles);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * This sets the action button on the toolbar with ID
     * {@link CustomTabsIntent#TOOLBAR_ACTION_BUTTON_ID}.
     *
     * @param icon        The new icon of the action button.
     * @param description Content description of the action button.
     * @see CustomTabsSession#setToolbarItem(int, Bitmap, String)
     */
    public boolean setActionButton(@NonNull Bitmap icon, @NonNull String description) {
        Bundle bundle = new Bundle();
        bundle.putParcelable(CustomTabsIntent.KEY_ICON, icon);
        bundle.putString(CustomTabsIntent.KEY_DESCRIPTION, description);

        Bundle metaBundle = new Bundle();
        metaBundle.putBundle(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, bundle);
        addIdToBundle(bundle);
        try {
            return mService.updateVisuals(mCallback, metaBundle);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Updates the {@link RemoteViews} of the secondary toolbar in an existing custom tab session.
     *
     * @param remoteViews   The updated {@link RemoteViews} that will be shown in secondary toolbar.
     *                      If null, the current secondary toolbar will be dismissed.
     * @param clickableIDs  The ids of clickable views. The onClick event of these views will be
     *                      handled by custom tabs.
     * @param pendingIntent The {@link PendingIntent} that will be sent when the user clicks on one
     *                      of the {@link View}s in clickableIDs.
     */
    public boolean setSecondaryToolbarViews(@Nullable RemoteViews remoteViews,
            @Nullable int[] clickableIDs, @Nullable PendingIntent pendingIntent) {
        Bundle bundle = new Bundle();
        bundle.putParcelable(CustomTabsIntent.EXTRA_REMOTEVIEWS, remoteViews);
        bundle.putIntArray(CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS, clickableIDs);
        bundle.putParcelable(CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT, pendingIntent);
        addIdToBundle(bundle);
        try {
            return mService.updateVisuals(mCallback, bundle);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Sets a {@link PendingIntent} object to be sent when the user swipes up from the secondary
     * (bottom) toolbar.
     *
     * @param pendingIntent {@link PendingIntent} to send.
     * @return Whether the update succeeded.
     */
    public boolean setSecondaryToolbarSwipeUpGesture(@Nullable PendingIntent pendingIntent) {
        Bundle bundle = new Bundle();
        bundle.putParcelable(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE,
                pendingIntent);
        addIdToBundle(bundle);
        try {
            return mService.updateVisuals(mCallback, bundle);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Updates the visuals for toolbar items. Will only succeed if a custom tab created using this
     * session is in the foreground in browser and the given id is valid.
     *
     * @param id          The id for the item to update.
     * @param icon        The new icon of the toolbar item.
     * @param description Content description of the toolbar item.
     * @return Whether the update succeeded.
     * @deprecated Use
     * CustomTabsSession#setSecondaryToolbarViews(RemoteViews, int[], PendingIntent)
     */
    @Deprecated
    public boolean setToolbarItem(int id, @NonNull Bitmap icon, @NonNull String description) {
        Bundle bundle = new Bundle();
        bundle.putInt(CustomTabsIntent.KEY_ID, id);
        bundle.putParcelable(CustomTabsIntent.KEY_ICON, icon);
        bundle.putString(CustomTabsIntent.KEY_DESCRIPTION, description);

        Bundle metaBundle = new Bundle();
        metaBundle.putBundle(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, bundle);
        addIdToBundle(metaBundle);
        try {
            return mService.updateVisuals(mCallback, metaBundle);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Sends a request to create a two way postMessage channel between the client and the browser.
     *
     * @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.
     */
    public boolean requestPostMessageChannel(@NonNull Uri postMessageOrigin) {
        return requestPostMessageChannel(postMessageOrigin, null, new Bundle());
    }

    /**
     * Sends a request to create a two way postMessage channel between the client and the browser
     * with specifying the target origin to communicate with.
     *
     * @param postMessageOrigin       A origin that the client is requesting to be identified as
     *                                during the postMessage communication.
     * @param postMessageTargetOrigin The target Origin to establish the postMessage communication
     *                                with.
     * @param extras  Reserved for future use.
     * @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.
     */
    public boolean requestPostMessageChannel(@NonNull Uri postMessageOrigin,
            @Nullable Uri postMessageTargetOrigin, @NonNull Bundle extras) {
        try {
            Bundle targetOriginWithIdBundle =
                    createPostMessageExtraBundle(postMessageTargetOrigin);
            if (targetOriginWithIdBundle != null) {
                extras.putAll(targetOriginWithIdBundle);
                return mService.requestPostMessageChannelWithExtras(
                        mCallback, postMessageOrigin, extras);
            } else {
                return mService.requestPostMessageChannel(mCallback, postMessageOrigin);
            }
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * 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 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
    public int postMessage(@NonNull String message, @Nullable Bundle extras) {
        extras = createBundleWithId(extras);
        synchronized (mLock) {
            try {
                return mService.postMessage(mCallback, message, extras);
            } catch (RemoteException e) {
                return CustomTabsService.RESULT_FAILURE_REMOTE_ERROR;
            }
        }
    }

    /**
     * Requests to validate a relationship between the application and an origin.
     *
     * <p>
     * See <a href="https://developers.google.com/digital-asset-links/v1/getting-started">here</a>
     * for documentation about Digital Asset Links. This methods requests the browser to verify
     * a relation with the calling application, to grant the associated rights.
     *
     * <p>
     * If this method returns {@code 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 relation Relation to check, must be one of the {@code CustomTabsService#RELATION_* }
     *                 constants.
     * @param origin   Origin.
     * @param extras   Reserved for future use.
     * @return {@code true} if the request has been submitted successfully.
     */
    public boolean validateRelationship(@Relation int relation, @NonNull Uri origin,
            @Nullable Bundle extras) {
        if (relation < CustomTabsService.RELATION_USE_AS_ORIGIN
                || relation > CustomTabsService.RELATION_HANDLE_ALL_URLS) {
            return false;
        }
        extras = createBundleWithId(extras);
        try {
            return mService.validateRelationship(mCallback, relation, origin, extras);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Passes an URI of a file, e.g. in order to pass a large bitmap to be displayed in the
     * Custom Tabs provider.
     *
     * Prior to calling this method, the client needs to grant a read permission to the target
     * Custom Tabs provider via {@link android.content.Context#grantUriPermission}.
     *
     * The file is read and processed (where applicable) synchronously, therefore it's recommended
     * to call this method on a background thread.
     *
     * @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.
     */
    public boolean receiveFile(@NonNull Uri uri, @CustomTabsService.FilePurpose int purpose,
            @Nullable Bundle extras) {
        extras = createBundleWithId(extras);
        try {
            return mService.receiveFile(mCallback, uri, purpose, extras);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Returns whether the Engagement Signals API is available. The availability of the Engagement
     * Signals API may change at runtime. If an {@link EngagementSignalsCallback} has been set, an
     * {@link EngagementSignalsCallback#onSessionEnded} signal will be sent if the API becomes
     * unavailable later.
     *
     * @param extras Reserved for future use.
     * @return Whether the Engagement Signals API is available. A false value means
     *         {@link #setEngagementSignalsCallback} will return false and not set the callback.
     * @throws RemoteException If the Service dies while responding to the request.
     * @throws UnsupportedOperationException If this method isn't supported by the Custom Tabs
     *                                       implementation.
     */
    public boolean isEngagementSignalsApiAvailable(@NonNull Bundle extras) throws RemoteException {
        Bundle extrasWithId = createBundleWithId(extras);
        try {
            return mService.isEngagementSignalsApiAvailable(mCallback, extrasWithId);
        } catch (SecurityException e) {
            throw new UnsupportedOperationException("This method isn't supported by the "
                    + "Custom Tabs implementation.", e);
        }
    }

    /**
     * Sets an {@link EngagementSignalsCallback} to receive callbacks for events related to the
     * user's engagement with webpage within the tab.
     *
     * Note that the callback will be executed on the main thread using
     * {@link Looper#getMainLooper()}. To specify the execution thread, use
     * {@link #setEngagementSignalsCallback(Executor, EngagementSignalsCallback, Bundle)}.
     *
     * @param callback The {@link EngagementSignalsCallback} to receive the user engagement signals.
     * @param extras   Reserved for future use.
     * @return Whether the callback connection is allowed. If false, no callbacks will be called for
     * this session.
     * @throws RemoteException               If the Service dies while responding to the request.
     * @throws UnsupportedOperationException If this method isn't supported by the Custom Tabs
     *                                       implementation.
     */
    @RequiresFeature(name = CustomTabsFeatures.ENGAGEMENT_SIGNALS, enforcement =
            "androidx.browser.customtabs.CustomTabsSession#isEngagementSignalsApiAvailable")
    public boolean setEngagementSignalsCallback(@NonNull EngagementSignalsCallback callback,
            @NonNull Bundle extras) throws RemoteException {
        Bundle extrasWithId = createBundleWithId(extras);
        IEngagementSignalsCallback wrapper = createEngagementSignalsCallbackWrapper(callback);
        try {
            return mService.setEngagementSignalsCallback(mCallback, wrapper.asBinder(),
                    extrasWithId);
        } catch (SecurityException e) {
            throw new UnsupportedOperationException("This method isn't supported by the "
                    + "Custom Tabs implementation.", e);
        }
    }

    private IEngagementSignalsCallback.Stub createEngagementSignalsCallbackWrapper(
            @NonNull final EngagementSignalsCallback callback) {
        return new IEngagementSignalsCallback.Stub() {
            private final Handler mHandler = new Handler(Looper.getMainLooper());

            @Override
            public void onVerticalScrollEvent(boolean isDirectionUp, Bundle extras) {
                mHandler.post(() -> callback.onVerticalScrollEvent(isDirectionUp, extras));
            }

            @Override
            public void onGreatestScrollPercentageIncreased(int scrollPercentage, Bundle extras) {
                mHandler.post(() -> callback.onGreatestScrollPercentageIncreased(
                        scrollPercentage, extras));
            }

            @Override
            public void onSessionEnded(boolean didUserInteract, Bundle extras) {
                mHandler.post(() -> callback.onSessionEnded(didUserInteract, extras));
            }
        };
    }

    /**
     * Sets an {@link EngagementSignalsCallback} to receive callbacks for events related to the
     * user's engagement with webpage within the tab.
     *
     * @param executor The {@link Executor} to be used to execute the callbacks.
     * @param callback The {@link EngagementSignalsCallback} to receive the user engagement signals.
     * @param extras   Reserved for future use.
     * @return Whether the callback connection is allowed. If false, no callbacks will be called for
     * this session.
     * @throws RemoteException               If the Service dies while responding to the request.
     * @throws UnsupportedOperationException If this method isn't supported by the Custom Tabs
     *                                       implementation.
     */
    @RequiresFeature(name = CustomTabsFeatures.ENGAGEMENT_SIGNALS, enforcement =
            "androidx.browser.customtabs.CustomTabsSession#isEngagementSignalsApiAvailable")
    public boolean setEngagementSignalsCallback(@NonNull Executor executor,
            @NonNull EngagementSignalsCallback callback,
            @NonNull Bundle extras) throws RemoteException {
        Bundle extrasWithId = createBundleWithId(extras);
        IEngagementSignalsCallback wrapper =
                createEngagementSignalsCallbackWrapper(callback, executor);
        try {
            return mService.setEngagementSignalsCallback(mCallback, wrapper.asBinder(),
                    extrasWithId);
        } catch (SecurityException e) {
            throw new UnsupportedOperationException("This method isn't supported by the "
                    + "Custom Tabs implementation.", e);
        }
    }

    private IEngagementSignalsCallback.Stub createEngagementSignalsCallbackWrapper(
            @NonNull final EngagementSignalsCallback callback, @NonNull Executor executor) {
        return new IEngagementSignalsCallback.Stub() {
            private final Executor mExecutor = executor;

            @Override
            public void onVerticalScrollEvent(boolean isDirectionUp, Bundle extras) {
                long identity = Binder.clearCallingIdentity();
                try {
                    mExecutor.execute(() -> callback.onVerticalScrollEvent(isDirectionUp, extras));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }

            @Override
            public void onGreatestScrollPercentageIncreased(int scrollPercentage, Bundle extras) {
                long identity = Binder.clearCallingIdentity();
                try {
                    mExecutor.execute(() -> callback.onGreatestScrollPercentageIncreased(
                            scrollPercentage, extras));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }

            @Override
            public void onSessionEnded(boolean didUserInteract, Bundle extras) {
                long identity = Binder.clearCallingIdentity();
                try {
                    mExecutor.execute(() -> callback.onSessionEnded(didUserInteract, extras));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        };
    }

    private @Nullable Bundle createPostMessageExtraBundle(@Nullable Uri targetOrigin) {
        Bundle toReturn = new Bundle();
        if (targetOrigin != null) {
            toReturn.putParcelable(CustomTabsSession.TARGET_ORIGIN_KEY, targetOrigin);
        }
        // If mId is not null we know that the CustomTabsService supports
        // requestPostMessageChannelWithExtras. That is because non-null mId means that
        // CustomTabsSession was created with CustomTabsClient#newSession(Callback int), which
        // can succeed only when browsers supporting CustomTabsService#newSessionWithExtras.
        // This was added at the same time as requestPostMessageChannelWithExtras.
        if (mId != null) {
            addIdToBundle(toReturn);
        }
        return toReturn.isEmpty() ? null : toReturn;
    }

    private Bundle createBundleWithId(@Nullable Bundle bundle) {
        Bundle bundleWithId = new Bundle();
        if (bundle != null) bundleWithId.putAll(bundle);
        addIdToBundle(bundleWithId);
        return bundleWithId;
    }

    private void addIdToBundle(Bundle bundle) {
        if (mId != null) bundle.putParcelable(CustomTabsIntent.EXTRA_SESSION_ID, mId);
    }

    /* package */ IBinder getBinder() {
        return mCallback.asBinder();
    }

    /* package */ ComponentName getComponentName() {
        return mComponentName;
    }

    @Nullable
        /* package */ PendingIntent getId() {
        return mId;
    }

    /**
     * A class to be used instead of {@link CustomTabsSession} before we are connected
     * {@link CustomTabsService}.
     *
     * Use {@link CustomTabsClient#attachSession(PendingSession)} to get {@link CustomTabsSession}.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static class PendingSession {
        @Nullable
        private final CustomTabsCallback mCallback;
        @Nullable
        private final PendingIntent mId;

        /* package */ PendingSession(
                @Nullable CustomTabsCallback callback, @Nullable PendingIntent sessionId) {
            mCallback = callback;
            mId = sessionId;
        }

        @Nullable
            /* package */ PendingIntent getId() {
            return mId;
        }

        @Nullable
            /* package */ CustomTabsCallback getCallback() {
            return mCallback;
        }
    }

    // For use in testing only.
    static class MockSession extends ICustomTabsService.Stub {
        @Override
        public boolean warmup(long flags) throws RemoteException {
            return false;
        }

        @Override
        public boolean newSession(ICustomTabsCallback callback) throws RemoteException {
            return false;
        }

        @Override
        public boolean newSessionWithExtras(ICustomTabsCallback callback, Bundle extras)
                throws RemoteException {
            return false;
        }

        @Override
        public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url, Bundle extras,
                List<Bundle> otherLikelyBundles) throws RemoteException {
            return false;
        }

        @SuppressWarnings("NullAway")  // TODO: b/142938599
        @Override
        public Bundle extraCommand(String commandName, Bundle args) throws RemoteException {
            return null;
        }

        @Override
        public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle)
                throws RemoteException {
            return false;
        }

        @Override
        public boolean requestPostMessageChannel(ICustomTabsCallback callback,
                Uri postMessageOrigin) throws RemoteException {
            return false;
        }

        @Override
        public boolean requestPostMessageChannelWithExtras(ICustomTabsCallback callback,
                Uri postMessageOrigin, Bundle extras) throws RemoteException {
            return false;
        }

        @Override
        public int postMessage(ICustomTabsCallback callback, String message, Bundle extras)
                throws RemoteException {
            return 0;
        }

        @Override
        public boolean validateRelationship(ICustomTabsCallback callback, int relation, Uri origin,
                Bundle extras) throws RemoteException {
            return false;
        }

        @Override
        public boolean receiveFile(ICustomTabsCallback callback, Uri uri, int purpose,
                Bundle extras) throws RemoteException {
            return false;
        }

        @Override
        public boolean isEngagementSignalsApiAvailable(ICustomTabsCallback customTabsCallback,
                Bundle extras) throws RemoteException {
            return false;
        }

        @Override
        public boolean setEngagementSignalsCallback(ICustomTabsCallback customTabsCallback,
                IBinder callback, Bundle extras) throws RemoteException {
            return false;
        }
    }
}