| /* |
| * 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.os.AsyncTask; |
| 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 |
| 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(); |
| } |
| |
| static class BindToServiceAsyncTask extends 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)) { |
| 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. |
| */ |
| 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; |
| } |
| } |