public final class

TrustedWebActivityServiceConnectionPool

extends java.lang.Object

 java.lang.Object

↳androidx.browser.trusted.TrustedWebActivityServiceConnectionPool

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

Overview

A TrustedWebActivityServiceConnectionPool will be used by a Trusted Web Activity provider and takes care of connecting to and communicating with TrustedWebActivityServices. This is done through the TrustedWebActivityServiceConnectionPool.connect(Uri, Set, Executor) method.

Multiple Trusted Web Activity client apps may be suitable for a given scope. These are passed in to TrustedWebActivityServiceConnectionPool.connect(Uri, Set, Executor) and TrustedWebActivityServiceConnectionPool.serviceExistsForScope(Uri, Set) and the most appropriate one for the scope is chosen.

Summary

Methods
public <any>connect(Uri scope, java.util.Set<Token> possiblePackages, java.util.concurrent.Executor executor)

Connects to the appropriate TrustedWebActivityService or uses an existing connection if available and runs code once connected.

public static TrustedWebActivityServiceConnectionPoolcreate(Context context)

Creates a TrustedWebActivityServiceConnectionPool.

public booleanserviceExistsForScope(Uri scope, java.util.Set<Token> possiblePackages)

Checks if a TrustedWebActivityService exists to handle requests for the given scope and origin.

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

Methods

public static TrustedWebActivityServiceConnectionPool create(Context context)

Creates a TrustedWebActivityServiceConnectionPool.

Parameters:

context: A Context used for accessing SharedPreferences.

public <any> connect(Uri scope, java.util.Set<Token> possiblePackages, java.util.concurrent.Executor executor)

Connects to the appropriate TrustedWebActivityService or uses an existing connection if available and runs code once connected.

To find a Service to connect to, this method attempts to resolve an Intent with the scope as data. The first of the resolved packages to be contained in the possiblePackages set will be chosen. Finally, an Intent with the action TrustedWebActivityService.ACTION_TRUSTED_WEB_ACTIVITY_SERVICE will be used to find the Service.

This method should be called on the UI thread.

Parameters:

scope: The scope used in an Intent to find packages that may have a TrustedWebActivityService.
possiblePackages: A collection of packages to consider. These would be the packages that have previously launched a Trusted Web Activity for the origin.
executor: The java.util.concurrent.Executor to connect to the Service on if a new connection is required.

Returns:

A for the resulting TrustedWebActivityServiceConnection. This may be set to an java.lang.IllegalArgumentException if no service exists for the scope (you can check for this beforehand by calling TrustedWebActivityServiceConnectionPool.serviceExistsForScope(Uri, Set)). It may be set to a java.lang.SecurityException if the Service does not accept connections from this app. It may be set to an java.lang.IllegalStateException if connecting to the Service fails.

public boolean serviceExistsForScope(Uri scope, java.util.Set<Token> possiblePackages)

Checks if a TrustedWebActivityService exists to handle requests for the given scope and origin. This method uses the same logic as TrustedWebActivityServiceConnectionPool.connect(Uri, Set, Executor). If this method returns false, TrustedWebActivityServiceConnectionPool.connect(Uri, Set, Executor) will return a Future containing an java.lang.IllegalStateException.

This method should be called on the UI thread.

Parameters:

scope: The scope used in an Intent to find packages that may have a TrustedWebActivityService.
possiblePackages: A collection of packages to consider. These would be the packages that have previously launched a Trusted Web Activity for the origin.

Returns:

Whether a TrustedWebActivityService was found.

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.trusted;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.Log;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.common.util.concurrent.ListenableFuture;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * A TrustedWebActivityServiceConnectionPool will be used by a Trusted Web Activity provider and
 * takes care of connecting to and communicating with {@link TrustedWebActivityService}s.
 * This is done through the {@link #connect} method.
 * <p>
 * Multiple Trusted Web Activity client apps may be suitable for a given scope.
 * These are passed in to {@link #connect} and {@link #serviceExistsForScope} and the most
 * appropriate one for the scope is chosen.
 */
public final class TrustedWebActivityServiceConnectionPool {
    private static final String TAG = "TWAConnectionPool";

    /** Application context, used to connect to the services. */
    private final Context mContext;

    /** Map from ServiceWorker scope to Connection. */
    private final Map<Uri, ConnectionHolder> mConnections = new HashMap<>();

    private TrustedWebActivityServiceConnectionPool(@NonNull Context context) {
        mContext = context.getApplicationContext();
    }

    /**
     * Creates a TrustedWebActivityServiceConnectionPool.
     * @param context A Context used for accessing SharedPreferences.
     */
    @NonNull
    public static TrustedWebActivityServiceConnectionPool create(@NonNull Context context) {
        return new TrustedWebActivityServiceConnectionPool(context);
    }

    /**
     * Connects to the appropriate {@link TrustedWebActivityService} or uses an existing connection
     * if available and runs code once connected.
     * <p>
     * To find a Service to connect to, this method attempts to resolve an
     * {@link Intent#ACTION_VIEW} Intent with the {@code scope} as data.
     * The first of the resolved packages to be contained in the {@code possiblePackages} set will
     * be chosen.
     * Finally, an Intent with the action
     * {@link TrustedWebActivityService#ACTION_TRUSTED_WEB_ACTIVITY_SERVICE} will be used to find
     * the Service.
     * <p>
     * This method should be called on the UI thread.
     *
     * @param scope The scope used in an Intent to find packages that may have a
     *              {@link TrustedWebActivityService}.
     * @param possiblePackages A collection of packages to consider.
     *                         These would be the packages that have previously launched a
     *                         Trusted Web Activity for the origin.
     * @param executor The {@link Executor} to connect to the Service on if a new connection is
     *                 required.
     * @return A {@link ListenableFuture} for the resulting
     *         {@link TrustedWebActivityServiceConnection}.
     *         This may be set to an {@link IllegalArgumentException} if no service exists for
     *         the scope (you can check for this beforehand by calling
     *         {@link #serviceExistsForScope}).
     *         It may be set to a {@link SecurityException} if the Service does not accept
     *         connections from this app.
     *         It may be set to an {@link IllegalStateException} if connecting to the Service fails.
     */
    @MainThread
    @NonNull
    @SuppressWarnings("deprecation") /* AsyncTask */
    public ListenableFuture<TrustedWebActivityServiceConnection> connect(
            @NonNull final Uri scope,
            @NonNull Set<Token> possiblePackages,
            @NonNull Executor executor) {
        // If we have an existing connection, use it.
        ConnectionHolder connection = mConnections.get(scope);
        if (connection != null) {
            return connection.getServiceWrapper();
        }

        // Check that this is a notification we want to handle.
        final Intent bindServiceIntent =
                createServiceIntent(mContext, scope, possiblePackages, true);
        if (bindServiceIntent == null) {
            return FutureUtils.immediateFailedFuture(
                    new IllegalArgumentException("No service exists for scope"));
        }

        ConnectionHolder newConnection = new ConnectionHolder(() -> mConnections.remove(scope));
        mConnections.put(scope, newConnection);

        // Create a new connection.
        new BindToServiceAsyncTask(mContext, bindServiceIntent, newConnection)
                .executeOnExecutor(executor);

        return newConnection.getServiceWrapper();
    }

    @SuppressWarnings("deprecation") /* AsyncTask */
    static class BindToServiceAsyncTask extends android.os.AsyncTask<Void, Void, Exception> {
        private final Context mAppContext;
        private final Intent mIntent;
        private final ConnectionHolder mConnection;

        BindToServiceAsyncTask(Context context, Intent intent, ConnectionHolder connection) {
            mAppContext = context.getApplicationContext();
            mIntent = intent;
            mConnection = connection;
        }

        @Nullable
        @Override
        protected Exception doInBackground(Void... voids) {
            try {
                // We can pass newConnection to bindService here on a background thread because
                // bindService assures us it will use newConnection on the UI thread.
                if (mAppContext.bindService(mIntent, mConnection, Context.BIND_AUTO_CREATE
                        | Context.BIND_INCLUDE_CAPABILITIES)) {
                    return null;
                }

                mAppContext.unbindService(mConnection);
                return new IllegalStateException("Could not bind to the service");
            } catch (SecurityException e) {
                Log.w(TAG, "SecurityException while binding.", e);
                return e;
            }
        }

        @Override
        protected void onPostExecute(Exception bindingException) {
            if (bindingException != null) mConnection.cancel(bindingException);
        }
    }

    /**
     * Checks if a TrustedWebActivityService exists to handle requests for the given scope and
     * origin.
     * This method uses the same logic as {@link #connect}.
     * If this method returns {@code false}, {@link #connect} will return a Future containing an
     * {@link IllegalStateException}.
     * <p>
     * This method should be called on the UI thread.
     *
     * @param scope The scope used in an Intent to find packages that may have a
     *              {@link TrustedWebActivityService}.
     * @param possiblePackages A collection of packages to consider.
     *                         These would be the packages that have previously launched a
     *                         Trusted Web Activity for the origin.
     * @return Whether a {@link TrustedWebActivityService} was found.
     */
    @MainThread
    public boolean serviceExistsForScope(@NonNull Uri scope,
            @NonNull Set<Token> possiblePackages) {
        // If we have an existing connection, we can deal with the scope.
        if (mConnections.get(scope) != null) return true;

        return createServiceIntent(mContext, scope, possiblePackages, false) != null;
    }

    /**
     * Unbinds all open connections to Trusted Web Activity clients.
     */
    void unbindAllConnections() {
        for (ConnectionHolder connection : mConnections.values()) {
            mContext.unbindService(connection);
        }
        mConnections.clear();
    }

    /**
     * Creates an Intent to launch the Service for the given scope and to an app contained in
     * {@code possiblePackages}.
     * Will return {@code null} if there is no applicable Service.
     */
    @SuppressWarnings("deprecation")
    private @Nullable Intent createServiceIntent(Context appContext, Uri scope,
            Set<Token> possiblePackages, boolean shouldLog) {
        if (possiblePackages == null || possiblePackages.size() == 0) {
            return null;
        }

        // Get a list of installed packages that would match the scope.
        Intent scopeResolutionIntent = new Intent();
        scopeResolutionIntent.setData(scope);
        scopeResolutionIntent.setAction(Intent.ACTION_VIEW);
        List<ResolveInfo> candidateActivities = appContext.getPackageManager()
                .queryIntentActivities(scopeResolutionIntent, PackageManager.MATCH_DEFAULT_ONLY);

        // Choose the first of the installed packages that is verified.
        String resolvedPackage = null;
        for (ResolveInfo info : candidateActivities) {
            String packageName = info.activityInfo.packageName;

            for (Token possiblePackage : possiblePackages) {
                if (possiblePackage.matches(packageName, appContext.getPackageManager())) {
                    resolvedPackage = packageName;
                    break;
                }
            }
        }

        if (resolvedPackage == null) {
            if (shouldLog) Log.w(TAG, "No TWA candidates for " + scope + " have been registered.");
            return null;
        }

        // Find the TrustedWebActivityService within that package.
        Intent serviceResolutionIntent = new Intent();
        serviceResolutionIntent.setPackage(resolvedPackage);
        serviceResolutionIntent.setAction(
                TrustedWebActivityService.ACTION_TRUSTED_WEB_ACTIVITY_SERVICE);
        ResolveInfo info = appContext.getPackageManager().resolveService(serviceResolutionIntent,
                PackageManager.MATCH_ALL);

        if (info == null) {
            if (shouldLog) Log.w(TAG, "Could not find TWAService for " + resolvedPackage);
            return null;
        }

        if (shouldLog) {
            Log.i(TAG, "Found " + info.serviceInfo.name + " to handle request for " + scope);
        }
        Intent finalIntent = new Intent();
        finalIntent.setComponent(new ComponentName(resolvedPackage, info.serviceInfo.name));
        return finalIntent;
    }
}