001/*
002 * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the
010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
011 * express or implied. See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package net.openid.appauth.browser;
016
017import android.content.ComponentName;
018import android.content.Context;
019import android.net.Uri;
020import android.os.Bundle;
021import androidx.annotation.NonNull;
022import androidx.annotation.Nullable;
023import androidx.annotation.WorkerThread;
024import androidx.browser.customtabs.CustomTabsCallback;
025import androidx.browser.customtabs.CustomTabsClient;
026import androidx.browser.customtabs.CustomTabsIntent;
027import androidx.browser.customtabs.CustomTabsServiceConnection;
028import androidx.browser.customtabs.CustomTabsSession;
029
030import net.openid.appauth.internal.Logger;
031import net.openid.appauth.internal.UriUtil;
032
033import java.lang.ref.WeakReference;
034import java.util.List;
035import java.util.concurrent.CountDownLatch;
036import java.util.concurrent.TimeUnit;
037import java.util.concurrent.atomic.AtomicReference;
038
039/**
040 * Hides the details of establishing connections and sessions with custom tabs, to make testing
041 * easier.
042 */
043public class CustomTabManager {
044
045    /**
046     * Wait for at most this amount of time for the browser connection to be established.
047     */
048    private static final long CLIENT_WAIT_TIME = 1L;
049
050    @NonNull
051    private final WeakReference<Context> mContextRef;
052
053    @NonNull
054    private final AtomicReference<CustomTabsClient> mClient;
055
056    @NonNull
057    private final CountDownLatch mClientLatch;
058
059    @Nullable
060    private CustomTabsServiceConnection mConnection;
061
062    public CustomTabManager(@NonNull Context context) {
063        mContextRef = new WeakReference<>(context);
064        mClient = new AtomicReference<>();
065        mClientLatch = new CountDownLatch(1);
066    }
067
068    public synchronized void bind(@NonNull String browserPackage) {
069        if (mConnection != null) {
070            return;
071        }
072
073        mConnection = new CustomTabsServiceConnection() {
074            @Override
075            public void onServiceDisconnected(ComponentName componentName) {
076                Logger.debug("CustomTabsService is disconnected");
077                setClient(null);
078            }
079
080            @Override
081            public void onCustomTabsServiceConnected(ComponentName componentName,
082                                                     CustomTabsClient customTabsClient) {
083                Logger.debug("CustomTabsService is connected");
084                customTabsClient.warmup(0);
085                setClient(customTabsClient);
086            }
087
088            private void setClient(@Nullable CustomTabsClient client) {
089                mClient.set(client);
090                mClientLatch.countDown();
091            }
092        };
093
094        Context context = mContextRef.get();
095        if (context == null || !CustomTabsClient.bindCustomTabsService(
096                context,
097                browserPackage,
098                mConnection)) {
099            // this is expected if the browser does not support custom tabs
100            Logger.info("Unable to bind custom tabs service");
101            mClientLatch.countDown();
102        }
103    }
104
105    /**
106     * Creates a {@link androidx.browser.customtabs.CustomTabsIntent.Builder custom tab builder},
107     * with an optional list of optional URIs that may be requested. The URI list
108     * should be ordered such that the most likely URI to be requested is first. If the selected
109     * browser does not support custom tabs, then the URI list has no effect.
110     */
111    @WorkerThread
112    @NonNull
113    public CustomTabsIntent.Builder createTabBuilder(@Nullable Uri... possibleUris) {
114        return new CustomTabsIntent.Builder(createSession(null, possibleUris));
115    }
116
117    public synchronized void dispose() {
118        if (mConnection == null) {
119            return;
120        }
121
122        Context context = mContextRef.get();
123        if (context != null) {
124            context.unbindService(mConnection);
125        }
126
127        mClient.set(null);
128        Logger.debug("CustomTabsService is disconnected");
129    }
130
131    /**
132     * Creates a {@link androidx.browser.customtabs.CustomTabsSession custom tab session} for
133     * use with a custom tab intent, with optional callbacks and optional list of URIs that may
134     * be requested. The URI list should be ordered such that the most likely URI to be requested
135     * is first. If no custom tab supporting browser is available, this will return {@code null}.
136     */
137    @WorkerThread
138    @Nullable
139    public CustomTabsSession createSession(
140            @Nullable CustomTabsCallback callbacks,
141            @Nullable Uri... possibleUris) {
142        CustomTabsClient client = getClient();
143        if (client == null) {
144            return null;
145        }
146
147        CustomTabsSession session = client.newSession(callbacks);
148        if (session == null) {
149            Logger.warn("Failed to create custom tabs session through custom tabs client");
150            return null;
151        }
152
153        if (possibleUris != null && possibleUris.length > 0) {
154            List<Bundle> additionalUris = UriUtil.toCustomTabUriBundle(possibleUris, 1);
155            session.mayLaunchUrl(possibleUris[0], null, additionalUris);
156        }
157
158        return session;
159    }
160
161    /**
162     * Retrieve the custom tab client used to communicate with the custom tab supporting browser,
163     * if available.
164     */
165    @WorkerThread
166    public CustomTabsClient getClient() {
167        try {
168            mClientLatch.await(CLIENT_WAIT_TIME, TimeUnit.SECONDS);
169        } catch (InterruptedException e) {
170            Logger.info("Interrupted while waiting for browser connection");
171            mClientLatch.countDown();
172        }
173
174        return mClient.get();
175    }
176}