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)
CustomTabsClient(ICustomTabsService service, ComponentName componentName)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      */
bindCustomTabsService(Context context, String packageName, CustomTabsServiceConnection connection)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      */
getPackageName(Context context, @Nullable List<String> packages)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      */
getPackageName( Context context, @Nullable List<String> packages, boolean ignoreDefault)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      */
connectAndInitialize(Context context, String packageName)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      */
warmup(long flags)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      */
newSession(final CustomTabsCallback callback)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 
extraCommand(String commandName, Bundle args)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