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