mClient;
+
+ @NonNull
+ private final CountDownLatch mClientLatch;
+
+ BrowserHandler(@NonNull Context context) {
+ mContext = context;
+ mBrowserPackage = BrowserPackageHelper.getInstance().getPackageNameToUse(context);
+ mClient = new AtomicReference<>();
+ mClientLatch = new CountDownLatch(1);
+ mConnection = bindCustomTabsService();
+ }
+
+ private CustomTabsServiceConnection bindCustomTabsService() {
+ CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ Logger.debug("CustomTabsService is disconnected");
+ setClient(null);
+ }
+
+ @Override
+ public void onCustomTabsServiceConnected(ComponentName componentName,
+ CustomTabsClient customTabsClient) {
+ Logger.debug("CustomTabsService is connected");
+ customTabsClient.warmup(0);
+ setClient(customTabsClient);
+ }
+
+ private void setClient(@Nullable CustomTabsClient client) {
+ mClient.set(client);
+ mClientLatch.countDown();
+ }
+ };
+
+ if (!CustomTabsClient.bindCustomTabsService(
+ mContext,
+ mBrowserPackage,
+ connection)) {
+ // this is expected if the browser does not support custom tabs
+ Logger.info("Unable to bind custom tabs service");
+ mClientLatch.countDown();
+ return null;
+ }
+
+ return connection;
+ }
+
+ public CustomTabsIntent.Builder createCustomTabsIntentBuilder() {
+ return new CustomTabsIntent.Builder(createSession());
+ }
+
+ public String getBrowserPackage() {
+ return mBrowserPackage;
+ }
+
+ public void unbind() {
+ if (mConnection == null) {
+ return;
+ }
+
+ mContext.unbindService(mConnection);
+ mClient.set(null);
+ Logger.debug("CustomTabsService is disconnected");
+ }
+
+ private CustomTabsSession createSession() {
+ try {
+ mClientLatch.await(CLIENT_WAIT_TIME, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Logger.info("Interrupted while waiting for browser connection");
+ mClientLatch.countDown();
+ }
+
+ CustomTabsClient client = mClient.get();
+ if (client != null) {
+ return client.newSession(null);
+ }
+
+ return null;
+ }
+}
diff --git a/library/java/net/openid/appauth/BrowserPackageHelper.java b/library/java/net/openid/appauth/BrowserPackageHelper.java
new file mode 100644
index 00000000..ab63b755
--- /dev/null
+++ b/library/java/net/openid/appauth/BrowserPackageHelper.java
@@ -0,0 +1,151 @@
+package net.openid.appauth;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Created by Marina Wageed on 22,September,2020
+ * Trufla Technology,
+ * Cairo, Egypt.
+ */
+class BrowserPackageHelper
+{
+ private static final String SCHEME_HTTP = "http";
+ private static final String SCHEME_HTTPS = "https";
+
+ /**
+ * The service we expect to find on a web browser that indicates it supports custom tabs.
+ */
+ @VisibleForTesting
+ static final String ACTION_CUSTOM_TABS_CONNECTION =
+ "android.support.customtabs.action.CustomTabsService";
+
+ /**
+ * An arbitrary (but unregistrable, per
+ * IANA rules) web intent used to query
+ * for installed web browsers on the system.
+ */
+ @VisibleForTesting
+ static final Intent BROWSER_INTENT = new Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("http://www.example.com"));
+
+ private static BrowserPackageHelper sInstance;
+
+ public static synchronized BrowserPackageHelper getInstance() {
+ if (sInstance == null) {
+ sInstance = new BrowserPackageHelper();
+ }
+ return sInstance;
+ }
+
+ @VisibleForTesting
+ static synchronized void clearInstance() {
+ sInstance = null;
+ }
+
+ private String mPackageNameToUse;
+
+ private BrowserPackageHelper() {}
+
+ /**
+ * Searches through all apps that handle VIEW intents and have a warmup service. Picks
+ * the one chosen by the user if this choice has been made, otherwise any browser with a warmup
+ * service is returned. If no browser has a warmup service, the default browser will be
+ * returned. If no default browser has been chosen, an arbitrary browser package is returned.
+ *
+ * This is not threadsafe.
+ *
+ * @param context {@link Context} to use for accessing {@link PackageManager}.
+ * @return The package name recommended to use for connecting to custom tabs related components.
+ */
+ public String getPackageNameToUse(Context context) {
+ if (mPackageNameToUse != null) {
+ return mPackageNameToUse;
+ }
+
+ PackageManager pm = context.getPackageManager();
+
+ // retrieve a list of all the matching handlers for the browser intent.
+ // queryIntentActivities will ensure that these are priority ordered, with the default
+ // (if set) as the first entry. Ignoring any matches which are not "full" browsers,
+ // pick the first that supports custom tabs, or the first full browser otherwise.
+ ResolveInfo firstMatch = null;
+ List resolvedActivityList =
+ pm.queryIntentActivities(BROWSER_INTENT, PackageManager.GET_RESOLVED_FILTER);
+
+ for (ResolveInfo info : resolvedActivityList) {
+ // ignore handlers which are not browers
+ if (!isFullBrowser(info)) {
+ continue;
+ }
+
+ // we hold the first non-default browser as the default browser to use, if we do
+ // not find any that support a warmup service.
+ if (firstMatch == null) {
+ firstMatch = info;
+ }
+
+ if (hasWarmupService(pm, info.activityInfo.packageName)) {
+ // we have found a browser with a warmup service, return it
+ mPackageNameToUse = info.activityInfo.packageName;
+ return mPackageNameToUse;
+ }
+ }
+
+ // No handlers have a warmup service, so we return the first match (typically the
+ // default browser), or null if there are no identifiable browsers.
+ if (firstMatch != null) {
+ mPackageNameToUse = firstMatch.activityInfo.packageName;
+ } else {
+ mPackageNameToUse = null;
+ }
+ return mPackageNameToUse;
+ }
+
+ private boolean hasWarmupService(PackageManager pm, String packageName) {
+ Intent serviceIntent = new Intent();
+ serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
+ serviceIntent.setPackage(packageName);
+ return (pm.resolveService(serviceIntent, 0) != null);
+ }
+
+ public boolean isFullBrowser(ResolveInfo resolveInfo) {
+ // The filter must match ACTION_VIEW, CATEGORY_BROWSEABLE, and at least one scheme,
+ if (!resolveInfo.filter.hasAction(Intent.ACTION_VIEW)
+ || !resolveInfo.filter.hasCategory(Intent.CATEGORY_BROWSABLE)
+ || resolveInfo.filter.schemesIterator() == null) {
+ return false;
+ }
+
+ // The filter must not be restricted to any particular set of authorities
+ if (resolveInfo.filter.authoritiesIterator() != null) {
+ return false;
+ }
+
+ // The filter must support both HTTP and HTTPS.
+ boolean supportsHttp = false;
+ boolean supportsHttps = false;
+ Iterator schemeIter = resolveInfo.filter.schemesIterator();
+ while (schemeIter.hasNext()) {
+ String scheme = schemeIter.next();
+ supportsHttp |= SCHEME_HTTP.equals(scheme);
+ supportsHttps |= SCHEME_HTTPS.equals(scheme);
+
+ if (supportsHttp && supportsHttps) {
+ return true;
+ }
+ }
+
+ // at least one of HTTP or HTTPS is not supported
+ return false;
+ }
+}
diff --git a/library/java/net/openid/appauth/LogoutRequest.java b/library/java/net/openid/appauth/LogoutRequest.java
new file mode 100644
index 00000000..6f964d81
--- /dev/null
+++ b/library/java/net/openid/appauth/LogoutRequest.java
@@ -0,0 +1,30 @@
+package net.openid.appauth;
+
+import android.net.Uri;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Created by Marina Wageed on 22,September,2020
+ * Trufla Technology,
+ * Cairo, Egypt.
+ */
+
+public class LogoutRequest
+{
+ @VisibleForTesting
+ static final String PARAM_REDIRECT_URI = "redirect_uri";
+
+ private Uri logoutEndpoint;
+ private Uri redirectUri;
+
+ public LogoutRequest(Uri logoutEndpoint, Uri redirectUri) {
+ this.logoutEndpoint = logoutEndpoint;
+ this.redirectUri = redirectUri;
+ }
+
+ public Uri toUri() {
+ Uri.Builder uriBuilder = logoutEndpoint.buildUpon()
+ .appendQueryParameter(PARAM_REDIRECT_URI, redirectUri.toString());
+ return uriBuilder.build();
+ }
+}
diff --git a/library/java/net/openid/appauth/LogoutService.java b/library/java/net/openid/appauth/LogoutService.java
new file mode 100644
index 00000000..c4b6b798
--- /dev/null
+++ b/library/java/net/openid/appauth/LogoutService.java
@@ -0,0 +1,90 @@
+package net.openid.appauth;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.browser.customtabs.CustomTabsIntent;
+
+import net.openid.appauth.internal.Logger;
+
+import static net.openid.appauth.Preconditions.checkNotNull;
+
+/**
+ * Created by Marina Wageed on 22,September,2020
+ * Trufla Technology,
+ * Cairo, Egypt.
+ */
+
+public class LogoutService
+{
+ @VisibleForTesting
+ Context mContext;
+
+ @NonNull
+ private final BrowserHandler mBrowserHandler;
+
+ private boolean mDisposed = false;
+
+ public LogoutService(@NonNull Context context) {
+ this(context, new BrowserHandler(context));
+ }
+
+ @VisibleForTesting
+ LogoutService(@NonNull Context context,
+ @NonNull BrowserHandler browserHandler) {
+ mContext = checkNotNull(context);
+ mBrowserHandler = checkNotNull(browserHandler);
+ }
+
+ public CustomTabsIntent.Builder createCustomTabsIntentBuilder() {
+ checkNotDisposed();
+ return mBrowserHandler.createCustomTabsIntentBuilder();
+ }
+
+ public void performLogoutRequest(
+ @NonNull LogoutRequest request,
+ @NonNull PendingIntent resultHandlerIntent) {
+ performLogoutRequest(request,
+ resultHandlerIntent,
+ createCustomTabsIntentBuilder().build());
+ }
+
+ public void performLogoutRequest(
+ @NonNull LogoutRequest request,
+ @NonNull PendingIntent resultHandlerIntent,
+ @NonNull CustomTabsIntent customTabsIntent) {
+ checkNotDisposed();
+ Uri requestUri = request.toUri();
+ PendingLogoutIntentStore.getInstance().addPendingIntent(request, resultHandlerIntent);
+ Intent intent = customTabsIntent.intent;
+ intent.setData(requestUri);
+ if (TextUtils.isEmpty(intent.getPackage())) {
+ intent.setPackage(mBrowserHandler.getBrowserPackage());
+ }
+
+ Logger.debug("Using %s as browser for auth", intent.getPackage());
+ intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.NO_TITLE);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+
+ mContext.startActivity(intent);
+ }
+
+ public void dispose() {
+ if (mDisposed) {
+ return;
+ }
+ mBrowserHandler.unbind();
+ mDisposed = true;
+ }
+
+ private void checkNotDisposed() {
+ if (mDisposed) {
+ throw new IllegalStateException("Service has been disposed and rendered inoperable");
+ }
+ }
+}
diff --git a/library/java/net/openid/appauth/LogoutUriReceiverActivity.java b/library/java/net/openid/appauth/LogoutUriReceiverActivity.java
new file mode 100644
index 00000000..23e23536
--- /dev/null
+++ b/library/java/net/openid/appauth/LogoutUriReceiverActivity.java
@@ -0,0 +1,60 @@
+package net.openid.appauth;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+
+import net.openid.appauth.internal.Logger;
+
+/**
+ * Created by Marina Wageed on 22,September,2020
+ * Trufla Technology,
+ * Cairo, Egypt.
+ */
+
+
+class LogoutUriReceiverActivity extends Activity
+{
+ private static final String KEY_STATE = "state";
+ private Clock mClock = SystemClock.INSTANCE;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ // Uri data = intent.getData();
+ // String state = data.getQueryParameter(KEY_STATE);
+
+ LogoutRequest request = PendingLogoutIntentStore.getInstance().getOriginalRequest();
+ PendingIntent target = PendingLogoutIntentStore.getInstance().getPendingIntent();
+
+ /*
+ Intent responseData;
+ if (data.getQueryParameterNames().contains(AuthorizationException.PARAM_ERROR)) {
+ String error = data.getQueryParameter(AuthorizationException.PARAM_ERROR);
+ AuthorizationException ex = AuthorizationException.fromOAuthTemplate(
+ AuthorizationException.AuthorizationRequestErrors.byString(error),
+ error,
+ data.getQueryParameter(AuthorizationException.PARAM_ERROR_DESCRIPTION),
+ UriUtil.parseUriIfAvailable(
+ data.getQueryParameter(AuthorizationException.PARAM_ERROR_URI)));
+ responseData = ex.toIntent();
+ } else {
+ AuthorizationResponse response = new AuthorizationResponse.Builder(request)
+ .fromUri(data, mClock)
+ .build();
+ responseData = response.toIntent();
+ }
+ */
+
+ Logger.debug("Forwarding redirect");
+ try {
+ target.send(this, 0, null);
+ } catch (PendingIntent.CanceledException e) {
+ Logger.errorWithStack(e, "Unable to send pending intent");
+ }
+
+ finish();
+ }
+}
diff --git a/library/java/net/openid/appauth/PendingLogoutIntentStore.java b/library/java/net/openid/appauth/PendingLogoutIntentStore.java
new file mode 100644
index 00000000..c8b3e490
--- /dev/null
+++ b/library/java/net/openid/appauth/PendingLogoutIntentStore.java
@@ -0,0 +1,42 @@
+package net.openid.appauth;
+
+import android.app.PendingIntent;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Created by Marina Wageed on 22,September,2020
+ * Trufla Technology,
+ * Cairo, Egypt.
+ */
+class PendingLogoutIntentStore
+{
+ private Queue mRequests = new LinkedList<>();
+ private Queue mPendingIntents = new LinkedList<>();
+
+ private static PendingLogoutIntentStore sInstance;
+
+ private PendingLogoutIntentStore() {
+ }
+
+ public static synchronized PendingLogoutIntentStore getInstance() {
+ if (sInstance == null) {
+ sInstance = new PendingLogoutIntentStore();
+ }
+ return sInstance;
+ }
+
+ public void addPendingIntent(LogoutRequest request, PendingIntent intent) {
+ mRequests.add(request);
+ mPendingIntents.add(intent);
+ }
+
+ public LogoutRequest getOriginalRequest() {
+ return mRequests.poll();
+ }
+
+ public PendingIntent getPendingIntent() {
+ return mPendingIntents.poll();
+ }
+}