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}