From 79c5f6f0328706da2ed9339a868f8bbca0094bc0 Mon Sep 17 00:00:00 2001 From: marina Date: Tue, 22 Sep 2020 12:20:37 +0200 Subject: [PATCH] add logout feature --- .../net/openid/appauthdemo/TokenActivity.java | 82 ++++++++-- app/res/raw/auth_config.json | 15 +- app/res/values/idp_configs.xml | 26 +++ app/res/values/strings.xml | 1 + library/AndroidManifest.xml | 16 +- .../AuthorizationServiceDiscovery.java | 2 + .../net/openid/appauth/BrowserHandler.java | 123 ++++++++++++++ .../openid/appauth/BrowserPackageHelper.java | 151 ++++++++++++++++++ .../net/openid/appauth/LogoutRequest.java | 30 ++++ .../net/openid/appauth/LogoutService.java | 90 +++++++++++ .../appauth/LogoutUriReceiverActivity.java | 60 +++++++ .../appauth/PendingLogoutIntentStore.java | 42 +++++ 12 files changed, 613 insertions(+), 25 deletions(-) create mode 100644 app/res/values/idp_configs.xml create mode 100644 library/java/net/openid/appauth/BrowserHandler.java create mode 100644 library/java/net/openid/appauth/BrowserPackageHelper.java create mode 100644 library/java/net/openid/appauth/LogoutRequest.java create mode 100644 library/java/net/openid/appauth/LogoutService.java create mode 100644 library/java/net/openid/appauth/LogoutUriReceiverActivity.java create mode 100644 library/java/net/openid/appauth/PendingLogoutIntentStore.java diff --git a/app/java/net/openid/appauthdemo/TokenActivity.java b/app/java/net/openid/appauthdemo/TokenActivity.java index 292c9388..7df3bbc9 100644 --- a/app/java/net/openid/appauthdemo/TokenActivity.java +++ b/app/java/net/openid/appauthdemo/TokenActivity.java @@ -15,6 +15,7 @@ package net.openid.appauthdemo; import android.app.Activity; +import android.app.PendingIntent; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -38,6 +39,8 @@ import net.openid.appauth.AuthorizationServiceDiscovery; import net.openid.appauth.ClientAuthentication; import net.openid.appauth.EndSessionRequest; +import net.openid.appauth.LogoutRequest; +import net.openid.appauth.LogoutService; import net.openid.appauth.TokenRequest; import net.openid.appauth.TokenResponse; import okio.Okio; @@ -68,6 +71,7 @@ public class TokenActivity extends AppCompatActivity { private static final String KEY_USER_INFO = "userInfo"; private static final int END_SESSION_REQUEST_CODE = 911; + private static final String EXTRA_AUTH_SERVICE_DISCOVERY = "authServiceDiscovery"; private AuthorizationService mAuthService; private AuthStateManager mStateManager; @@ -325,11 +329,7 @@ private void handleCodeExchangeResponse( } } - /** - * Demonstrates the use of {@link AuthState#performActionWithFreshTokens} to retrieve - * user info from the IDP's user info endpoint. This callback will negotiate a new access - * token / id token for use in a follow-up action, or provide an error if this fails. - */ + @MainThread private void fetchUserInfo() { displayLoading("Fetching user info"); @@ -420,21 +420,69 @@ private void endSession() { startActivityForResult(endSessionEnten, END_SESSION_REQUEST_CODE); } + + static AuthorizationServiceDiscovery getDiscoveryDocFromIntent(Intent intent) { + if (!intent.hasExtra(EXTRA_AUTH_SERVICE_DISCOVERY)) { + return null; + } + String discoveryJson = intent.getStringExtra(EXTRA_AUTH_SERVICE_DISCOVERY); + try { + return new AuthorizationServiceDiscovery(new JSONObject(discoveryJson)); + } catch (JSONException | AuthorizationServiceDiscovery.MissingArgumentException ex) { + throw new IllegalStateException("Malformed JSON in discovery doc"); + } + } + @MainThread private void signOut() { - // discard the authorization and token state, but retain the configuration and - // dynamic client registration (if applicable), to save from retrieving them again. - AuthState currentState = mStateManager.getCurrent(); - AuthState clearedState = - new AuthState(currentState.getAuthorizationServiceConfiguration()); - if (currentState.getLastRegistrationResponse() != null) { - clearedState.update(currentState.getLastRegistrationResponse()); +// // discard the authorization and token state, but retain the configuration and +// // dynamic client registration (if applicable), to save from retrieving them again. +// AuthState currentState = mStateManager.getCurrent(); +// AuthState clearedState = +// new AuthState(currentState.getAuthorizationServiceConfiguration()); +// if (currentState.getLastRegistrationResponse() != null) { +// clearedState.update(currentState.getLastRegistrationResponse()); +// } +// mStateManager.replace(clearedState); +// +// Intent mainIntent = new Intent(this, LoginActivity.class); +// mainIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); +// startActivity(mainIntent); +// finish(); + + AuthState mAuthState = mStateManager.getCurrent(); + + if (mAuthState.getAuthorizationServiceConfiguration() == null) { + Log.e(TAG, "Cannot make userInfo request without service configuration"); } - mStateManager.replace(clearedState); - Intent mainIntent = new Intent(this, LoginActivity.class); - mainIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(mainIntent); - finish(); + mAuthState.performActionWithFreshTokens(mAuthService, new AuthState.AuthStateAction() { + @Override + public void execute(String accessToken, String idToken, AuthorizationException ex) { + if (ex != null) { + Log.e(TAG, "Token refresh failed when fetching user info"); + return; + } + + AuthorizationServiceDiscovery discoveryDoc = getDiscoveryDocFromIntent(getIntent()); + if (discoveryDoc == null) { + throw new IllegalStateException("no available discovery doc"); + } + + Uri endSessionEndpoint = Uri.parse(discoveryDoc.getEndSessionEndpoint().toString()); + + String logoutUri = getResources().getString(R.string.keycloak_auth_logout_uri); + LogoutRequest logoutRequest = new LogoutRequest(endSessionEndpoint, + Uri.parse(logoutUri)); + + LogoutService logoutService = new LogoutService(TokenActivity.this); + logoutService.performLogoutRequest( + logoutRequest, + PendingIntent.getActivity( + TokenActivity.this, logoutRequest.hashCode(), + new Intent(TokenActivity.this, LoginActivity.class), 0) + ); + } + }); } } diff --git a/app/res/raw/auth_config.json b/app/res/raw/auth_config.json index 1796efab..12cb9d6f 100644 --- a/app/res/raw/auth_config.json +++ b/app/res/raw/auth_config.json @@ -1,12 +1,13 @@ { - "client_id": "", - "redirect_uri": "net.openid.appauthdemo:/oauth2redirect", - "end_session_uri":"", + "client_id": "truMobile_Android", + "prompt": "login", + "redirect_uri": "net.openid.appauthdemo:/oauthredirect", + "end_session_uri":"https://secure.trufla.dev/auth/realms/sharp/protocol/openid-connect/logout", "authorization_scope": "openid email profile", "discovery_uri": "", - "authorization_endpoint_uri": "", - "token_endpoint_uri": "", - "registration_endpoint_uri": "", - "user_info_endpoint_uri": "", + "authorization_endpoint_uri": "https://secure.trufla.dev/auth/realms/Sharp/protocol/openid-connect/auth", + "token_endpoint_uri": "https://secure.trufla.dev/auth/realms/Sharp/protocol/openid-connect/token", + "registration_endpoint_uri": "https://secure.trufla.dev/auth/realms/Sharp/clients-registrations/openid-connect", + "user_info_endpoint_uri": "https://secure.trufla.dev/auth/realms/Sharp/protocol/openid-connect/userinfo", "https_required": true } diff --git a/app/res/values/idp_configs.xml b/app/res/values/idp_configs.xml new file mode 100644 index 00000000..54e2ae3d --- /dev/null +++ b/app/res/values/idp_configs.xml @@ -0,0 +1,26 @@ + + + + + + false + YOUR_ID.apps.googleusercontent.com + true + + demo-test + + com.googleusercontent.apps.YOUR_ID + com.googleusercontent.apps.YOUR_ID:/oauth2redirect + com.test.demo + com.test.logout + com.test.demo:/oauth2Callback + "com.test.logout:/oauth2Callback" + diff --git a/app/res/values/strings.xml b/app/res/values/strings.xml index 253b9b92..e5a54b7f 100644 --- a/app/res/values/strings.xml +++ b/app/res/values/strings.xml @@ -28,4 +28,5 @@ Access token has expired Authorization settings in use: Use PendingIntent\'s for completion + logout diff --git a/library/AndroidManifest.xml b/library/AndroidManifest.xml index ef6c6050..15991d9b 100644 --- a/library/AndroidManifest.xml +++ b/library/AndroidManifest.xml @@ -28,7 +28,21 @@ - + + + + + + + + + + + + diff --git a/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java b/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java index c304eee0..a9cd7974 100644 --- a/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java +++ b/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java @@ -617,4 +617,6 @@ private static StringListField strList(String key, List defaults) { private static BooleanField bool(String key, boolean defaultValue) { return new BooleanField(key, defaultValue); } + + } diff --git a/library/java/net/openid/appauth/BrowserHandler.java b/library/java/net/openid/appauth/BrowserHandler.java new file mode 100644 index 00000000..85b0e820 --- /dev/null +++ b/library/java/net/openid/appauth/BrowserHandler.java @@ -0,0 +1,123 @@ +package net.openid.appauth; + +import android.content.ComponentName; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabsClient; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.customtabs.CustomTabsServiceConnection; +import androidx.browser.customtabs.CustomTabsSession; + +import net.openid.appauth.internal.Logger; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Created by Marina Wageed on 22,September,2020 + * Trufla Technology, + * Cairo, Egypt. + */ +class BrowserHandler +{ + + /** + * Wait for at most this amount of time for the browser connection to be established. + */ + private static final long CLIENT_WAIT_TIME = 1L; + + @NonNull + private final Context mContext; + + @NonNull + private final String mBrowserPackage; + + @Nullable + private final CustomTabsServiceConnection mConnection; + + @NonNull + private final AtomicReference 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(); + } +}