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