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