Home | History | Annotate | Download | only in customtabs
      1 /*
      2  * Copyright 2018 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 
     17 package androidx.browser.customtabs;
     18 
     19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
     20 
     21 import android.content.ComponentName;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.ServiceConnection;
     25 import android.content.pm.PackageManager;
     26 import android.content.pm.ResolveInfo;
     27 import android.net.Uri;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.Looper;
     31 import android.os.RemoteException;
     32 import android.support.customtabs.ICustomTabsCallback;
     33 import android.support.customtabs.ICustomTabsService;
     34 import android.text.TextUtils;
     35 
     36 import androidx.annotation.Nullable;
     37 import androidx.annotation.RestrictTo;
     38 
     39 import java.util.ArrayList;
     40 import java.util.List;
     41 
     42 /**
     43  * Class to communicate with a {@link CustomTabsService} and create
     44  * {@link CustomTabsSession} from it.
     45  */
     46 public class CustomTabsClient {
     47     private final ICustomTabsService mService;
     48     private final ComponentName mServiceComponentName;
     49 
     50     /** @hide */
     51     @RestrictTo(LIBRARY_GROUP)
     52     CustomTabsClient(ICustomTabsService service, ComponentName componentName) {
     53         mService = service;
     54         mServiceComponentName = componentName;
     55     }
     56 
     57     /**
     58      * Bind to a {@link CustomTabsService} using the given package name and
     59      * {@link ServiceConnection}.
     60      * @param context     {@link Context} to use while calling
     61      *                    {@link Context#bindService(Intent, ServiceConnection, int)}
     62      * @param packageName Package name to set on the {@link Intent} for binding.
     63      * @param connection  {@link CustomTabsServiceConnection} to use when binding. This will
     64      *                    return a {@link CustomTabsClient} on
     65      *                    {@link CustomTabsServiceConnection
     66      *                    #onCustomTabsServiceConnected(ComponentName, CustomTabsClient)}
     67      * @return Whether the binding was successful.
     68      */
     69     public static boolean bindCustomTabsService(Context context,
     70             String packageName, CustomTabsServiceConnection connection) {
     71         Intent intent = new Intent(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
     72         if (!TextUtils.isEmpty(packageName)) intent.setPackage(packageName);
     73         return context.bindService(intent, connection,
     74                 Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY);
     75     }
     76 
     77     /**
     78      * Returns the preferred package to use for Custom Tabs, preferring the default VIEW handler.
     79      *
     80      * @see #getPackageName(Context, List<String>, boolean)
     81      */
     82     public static String getPackageName(Context context, @Nullable List<String> packages) {
     83         return getPackageName(context, packages, false);
     84     }
     85 
     86     /**
     87      * Returns the preferred package to use for Custom Tabs.
     88      *
     89      * The preferred package name is the default VIEW intent handler as long as it supports Custom
     90      * Tabs. To modify this preferred behavior, set <code>ignoreDefault</code> to true and give a
     91      * non empty list of package names in <code>packages</code>.
     92      *
     93      * @param context       {@link Context} to use for querying the packages.
     94      * @param packages      Ordered list of packages to test for Custom Tabs support, in
     95      *                      decreasing order of priority.
     96      * @param ignoreDefault If set, the default VIEW handler won't get priority over other browsers.
     97      * @return The preferred package name for handling Custom Tabs, or <code>null</code>.
     98      */
     99     public static String getPackageName(
    100         Context context, @Nullable List<String> packages, boolean ignoreDefault) {
    101         PackageManager pm = context.getPackageManager();
    102 
    103         List<String> packageNames = packages == null ? new ArrayList<String>() : packages;
    104         Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://"));
    105 
    106         if (!ignoreDefault) {
    107             ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
    108             if (defaultViewHandlerInfo != null) {
    109                 String packageName = defaultViewHandlerInfo.activityInfo.packageName;
    110                 packageNames = new ArrayList<String>(packageNames.size() + 1);
    111                 packageNames.add(packageName);
    112                 if (packages != null) packageNames.addAll(packages);
    113             }
    114         }
    115 
    116         Intent serviceIntent = new Intent(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
    117         for (String packageName : packageNames) {
    118             serviceIntent.setPackage(packageName);
    119             if (pm.resolveService(serviceIntent, 0) != null) return packageName;
    120         }
    121         return null;
    122     }
    123 
    124     /**
    125      * Connects to the Custom Tabs warmup service, and initializes the browser.
    126      *
    127      * This convenience method connects to the service, and immediately warms up the Custom Tabs
    128      * implementation. Since service connection is asynchronous, the return code is not the return
    129      * code of warmup.
    130      * This call is optional, and clients are encouraged to connect to the service, call
    131      * <code>warmup()</code> and create a session. In this case, calling this method is not
    132      * necessary.
    133      *
    134      * @param context     {@link Context} to use to connect to the remote service.
    135      * @param packageName Package name of the target implementation.
    136      * @return Whether the binding was successful.
    137      */
    138     public static boolean connectAndInitialize(Context context, String packageName) {
    139         if (packageName == null) return false;
    140         final Context applicationContext = context.getApplicationContext();
    141         CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {
    142             @Override
    143             public final void onCustomTabsServiceConnected(
    144                     ComponentName name, CustomTabsClient client) {
    145                 client.warmup(0);
    146                 // Unbinding immediately makes the target process "Empty", provided that it is
    147                 // not used by anyone else, and doesn't contain any Activity. This makes it
    148                 // likely to get killed, but is preferable to keeping the connection around.
    149                 applicationContext.unbindService(this);
    150             }
    151 
    152            @Override
    153            public final void onServiceDisconnected(ComponentName componentName) { }
    154         };
    155         try {
    156             return bindCustomTabsService(applicationContext, packageName, connection);
    157         } catch (SecurityException e) {
    158             return false;
    159         }
    160     }
    161 
    162     /**
    163      * Warm up the browser process.
    164      *
    165      * Allows the browser application to pre-initialize itself in the background. Significantly
    166      * speeds up URL opening in the browser. This is asynchronous and can be called several times.
    167      *
    168      * @param flags Reserved for future use.
    169      * @return      Whether the warmup was successful.
    170      */
    171     public boolean warmup(long flags) {
    172         try {
    173             return mService.warmup(flags);
    174         } catch (RemoteException e) {
    175             return false;
    176         }
    177     }
    178 
    179     /**
    180      * Creates a new session through an ICustomTabsService with the optional callback. This session
    181      * can be used to associate any related communication through the service with an intent and
    182      * then later with a Custom Tab. The client can then send later service calls or intents to
    183      * through same session-intent-Custom Tab association.
    184      * @param callback The callback through which the client will receive updates about the created
    185      *                 session. Can be null. All the callbacks will be received on the UI thread.
    186      * @return The session object that was created as a result of the transaction. The client can
    187      *         use this to relay session specific calls.
    188      *         Null on error.
    189      */
    190     public CustomTabsSession newSession(final CustomTabsCallback callback) {
    191         ICustomTabsCallback.Stub wrapper = new ICustomTabsCallback.Stub() {
    192             private Handler mHandler = new Handler(Looper.getMainLooper());
    193 
    194             @Override
    195             public void onNavigationEvent(final int navigationEvent, final Bundle extras) {
    196                 if (callback == null) return;
    197                 mHandler.post(new Runnable() {
    198                     @Override
    199                     public void run() {
    200                         callback.onNavigationEvent(navigationEvent, extras);
    201                     }
    202                 });
    203             }
    204 
    205             @Override
    206             public void extraCallback(final String callbackName, final Bundle args)
    207                     throws RemoteException {
    208                 if (callback == null) return;
    209                 mHandler.post(new Runnable() {
    210                     @Override
    211                     public void run() {
    212                         callback.extraCallback(callbackName, args);
    213                     }
    214                 });
    215             }
    216 
    217             @Override
    218             public void onMessageChannelReady(final Bundle extras)
    219                     throws RemoteException {
    220                 if (callback == null) return;
    221                 mHandler.post(new Runnable() {
    222                     @Override
    223                     public void run() {
    224                         callback.onMessageChannelReady(extras);
    225                     }
    226                 });
    227             }
    228 
    229             @Override
    230             public void onPostMessage(final String message, final Bundle extras)
    231                     throws RemoteException {
    232                 if (callback == null) return;
    233                 mHandler.post(new Runnable() {
    234                     @Override
    235                     public void run() {
    236                         callback.onPostMessage(message, extras);
    237                     }
    238                 });
    239             }
    240 
    241             @Override
    242             public void onRelationshipValidationResult(
    243                     final @CustomTabsService.Relation int relation, final Uri requestedOrigin, final boolean result,
    244                     final @Nullable Bundle extras) throws RemoteException {
    245                 if (callback == null) return;
    246                 mHandler.post(new Runnable() {
    247                     @Override
    248                     public void run() {
    249                         callback.onRelationshipValidationResult(
    250                                 relation, requestedOrigin, result, extras);
    251                     }
    252                 });
    253             }
    254         };
    255 
    256         try {
    257             if (!mService.newSession(wrapper)) return null;
    258         } catch (RemoteException e) {
    259             return null;
    260         }
    261         return new CustomTabsSession(mService, wrapper, mServiceComponentName);
    262     }
    263 
    264     public Bundle extraCommand(String commandName, Bundle args) {
    265         try {
    266             return mService.extraCommand(commandName, args);
    267         } catch (RemoteException e) {
    268             return null;
    269         }
    270     }
    271 }
    272