From 5a1caff9690048e6a01ceab01c324fc84d36b9b8 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 8 Jun 2017 18:22:49 -0300 Subject: [PATCH 1/9] Handle web authentication internally without the user need to resume it --- auth0/build.gradle | 5 +- auth0/src/main/AndroidManifest.xml | 33 +- .../provider/AuthenticationActivity.java | 128 ++++++++ .../provider/CustomTabsController.java | 189 +++++++++++ .../auth0/android/provider/OAuthManager.java | 26 +- .../android/provider/RedirectActivity.java | 19 ++ .../android/provider/WebAuthProvider.java | 5 +- .../provider/AuthenticationActivityMock.java | 32 ++ .../provider/AuthenticationActivityTest.java | 236 ++++++++++++++ .../provider/CustomTabsControllerTest.java | 293 ++++++++++++++++++ .../android/provider/OAuthManagerTest.java | 1 - .../android/provider/WebAuthProviderTest.java | 113 ++++--- 12 files changed, 1012 insertions(+), 68 deletions(-) create mode 100644 auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java create mode 100644 auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java create mode 100644 auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java create mode 100644 auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java create mode 100644 auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java create mode 100644 auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java diff --git a/auth0/build.gradle b/auth0/build.gradle index 152195589..b868ea4ea 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -63,10 +63,11 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.0.1' + compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:customtabs:25.3.1' compile 'com.squareup.okhttp:okhttp:2.7.5' compile 'com.squareup.okhttp:logging-interceptor:2.7.5' - compile 'com.google.code.gson:gson:2.6.2' + compile 'com.google.code.gson:gson:2.7' compile 'com.auth0.android:jwtdecode:1.1.0' testCompile 'junit:junit:4.12' diff --git a/auth0/src/main/AndroidManifest.xml b/auth0/src/main/AndroidManifest.xml index ccc5384fb..6f2354e4b 100644 --- a/auth0/src/main/AndroidManifest.xml +++ b/auth0/src/main/AndroidManifest.xml @@ -22,4 +22,35 @@ ~ THE SOFTWARE. --> - + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java new file mode 100644 index 000000000..113e619e4 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java @@ -0,0 +1,128 @@ +package com.auth0.android.provider; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +class AuthenticationActivity extends Activity { + + private static final String TAG = AuthenticationActivity.class.getSimpleName(); + + static final String EXTRA_USE_BROWSER = "com.auth0.android.EXTRA_USE_BROWSER"; + static final String EXTRA_USE_FULL_SCREEN = "com.auth0.android.EXTRA_USE_FULL_SCREEN"; + static final String EXTRA_CONNECTION_NAME = "com.auth0.android.EXTRA_CONNECTION_NAME"; + private static final String EXTRA_INTENT_LAUNCHED = "com.auth0.android.EXTRA_INTENT_LAUNCHED"; + private boolean intentLaunched; + private CustomTabsController customTabsController; + + public static void authenticateUsingBrowser(Context context, Uri authorizeUri) { + Intent intent = new Intent(context, AuthenticationActivity.class); + intent.setData(authorizeUri); + intent.putExtra(AuthenticationActivity.EXTRA_USE_BROWSER, true); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(intent); + } + + public static void authenticateUsingWebView(Activity activity, Uri authorizeUri, int requestCode, String connection, boolean useFullScreen) { + Intent intent = new Intent(activity, AuthenticationActivity.class); + intent.setData(authorizeUri); + intent.putExtra(AuthenticationActivity.EXTRA_USE_BROWSER, false); + intent.putExtra(AuthenticationActivity.EXTRA_USE_FULL_SCREEN, useFullScreen); + intent.putExtra(AuthenticationActivity.EXTRA_CONNECTION_NAME, connection); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + activity.startActivityForResult(intent, requestCode); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.e(TAG, "Activity result"); + if (resultCode == RESULT_OK) { + deliverSuccessfulAuthenticationResult(data); + } + finish(); + } + + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(EXTRA_INTENT_LAUNCHED, intentLaunched); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + intentLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false); + } + } + + @Override + protected void onResume() { + super.onResume(); + Log.e(TAG, "onResume: intentLaunched = " + intentLaunched); + if (!intentLaunched) { + Log.e(TAG, "OnResume: Launching authentication intent"); + intentLaunched = true; + launchAuthenticationIntent(); + return; + } + + if (getIntent().getData() != null) { + Log.e(TAG, "OnResume: Passing result to the WebAuthProvider"); + deliverSuccessfulAuthenticationResult(getIntent()); + } else { + Log.e(TAG, "OnResume: The authentication was canceled"); + } + finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (customTabsController != null) { + customTabsController.unbindService(); + customTabsController = null; + } + } + + private void launchAuthenticationIntent() { + Bundle extras = getIntent().getExtras(); + final Uri authorizeUri = getIntent().getData(); + if (!extras.getBoolean(EXTRA_USE_BROWSER, true)) { + Log.e(TAG, "OnCreate: Launching WebAuthActivity intent for result"); + Intent intent = new Intent(this, WebAuthActivity.class); + intent.setData(authorizeUri); + intent.putExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, extras.getString(EXTRA_CONNECTION_NAME)); + intent.putExtra(WebAuthActivity.FULLSCREEN_EXTRA, extras.getBoolean(EXTRA_USE_FULL_SCREEN)); + startActivityForResult(intent, -1); + return; + } + + Log.e(TAG, "OnCreate: Launching Intent.VIEW intent"); + customTabsController = createCustomTabsController(); + customTabsController.bindServiceAndLaunchUri(authorizeUri); + } + + @VisibleForTesting + protected CustomTabsController createCustomTabsController() { + return new CustomTabsController(this); + } + + @VisibleForTesting + protected void deliverSuccessfulAuthenticationResult(Intent result) { + WebAuthProvider.resume(result); + } + +} diff --git a/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java new file mode 100644 index 000000000..5e1080387 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java @@ -0,0 +1,189 @@ +package com.auth0.android.provider; + +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.customtabs.CustomTabsSession; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +class CustomTabsController extends CustomTabsServiceConnection { + + private static final String TAG = CustomTabsController.class.getSimpleName(); + private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"; + //Known Browsers + private static final String CHROME_STABLE = "com.android.chrome"; + private static final String CHROME_SYSTEM = "com.google.android.apps.chrome"; + private static final String CHROME_BETA = "com.android.chrome.beta"; + private static final String CHROME_DEV = "com.android.chrome.dev"; + + private final WeakReference context; + private final String preferredPackage; + private CustomTabsSession session; + private Uri nextUri; + private boolean isBound; + + @VisibleForTesting + CustomTabsController(@NonNull Context context, @NonNull String browserPackage) { + this.context = new WeakReference<>(context); + this.preferredPackage = browserPackage; + } + + public CustomTabsController(@NonNull Context context) { + this(context, getBestBrowserPackage(context)); + } + + @VisibleForTesting + void clearContext() { + this.context.clear(); + } + + @Override + public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient customTabsClient) { + Log.d(TAG, "CustomTabs Service connected"); + isBound = true; + if (customTabsClient != null) { + customTabsClient.warmup(0L); + session = customTabsClient.newSession(null); + } + if (nextUri != null) { + launchUri(nextUri); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + Log.d(TAG, "CustomTabs Service disconnected"); + session = null; + } + + /** + * Attempts to bind the Custom Tabs Service to the Context. + * + * @return true if the request to bind the service was successful, false if the service was already bound or it couldn't be bound. + */ + public boolean bindService() { + Log.v(TAG, "Trying to bind the service"); + Context context = this.context.get(); + boolean success = false; + if (!isBound && context != null) { + success = CustomTabsClient.bindCustomTabsService(context, preferredPackage, this); + } + Log.d(TAG, "Bound: " + success); + return success; + } + + /** + * Attempts to unbind the Custom Tabs Service from the Context. + */ + public void unbindService() { + Log.v(TAG, "Trying to unbind the service"); + Context context = this.context.get(); + if (isBound && context != null) { + context.unbindService(this); + } + this.isBound = false; + this.nextUri = null; + } + + /** + * Attempst to bind the Custom Tabs Service to the Context and opens a Uri as soon as possible. + * + * @param uri the uri to open in a Custom Tab or Browser. + * @return true if the request to bind the service was successful, false if the service was already bound or it couldn't be bound. + */ + public boolean bindServiceAndLaunchUri(@NonNull Uri uri) { + this.nextUri = uri; + boolean boundRequestSuccess = bindService(); + if (isBound || !boundRequestSuccess) { + launchUri(uri); + } + return boundRequestSuccess; + } + + /** + * Opens a Uri in a Custom Tab or Browser + * + * @param uri the uri to open in a Custoam Tab or Browser. + */ + public void launchUri(@NonNull Uri uri) { + Context context = this.context.get(); + if (context == null) { + Log.v(TAG, "Custom Tab Context was no longer valid."); + return; + } + + Log.d(TAG, "Launching uri.."); + final Intent intent = new CustomTabsIntent.Builder(session) + .build() + .intent; + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.setData(uri); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException ignored) { + Intent fallbackIntent = new Intent(Intent.ACTION_VIEW, uri); + fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + context.startActivity(fallbackIntent); + } + } + + /** + * Query the OS for a Custom Tab compatible Browser application. + * It will pick the default browser first if is Custom Tab compatible, then any Chrome browser or the first Custom Tab compatible browser. + * + * @param context a valid Context + * @return the recommended Browser application package name, compatible with Custom Tabs if possible. + */ + @VisibleForTesting + static String getBestBrowserPackage(@NonNull Context context) { + PackageManager pm = context.getPackageManager(); + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo webHandler = pm.resolveActivity(browserIntent, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL : PackageManager.MATCH_DEFAULT_ONLY); + String defaultBrowser = null; + if (webHandler != null) { + defaultBrowser = webHandler.activityInfo.packageName; + } + + List resolvedActivityList = pm.queryIntentActivities(browserIntent, 0); + List customTabsBrowsers = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + customTabsBrowsers.add(info.activityInfo.packageName); + } + } + if (customTabsBrowsers.contains(defaultBrowser)) { + return defaultBrowser; + } else if (customTabsBrowsers.contains(CHROME_STABLE)) { + return CHROME_STABLE; + } else if (customTabsBrowsers.contains(CHROME_SYSTEM)) { + return CHROME_SYSTEM; + } else if (customTabsBrowsers.contains(CHROME_BETA)) { + return CHROME_BETA; + } else if (customTabsBrowsers.contains(CHROME_DEV)) { + return CHROME_DEV; + } else if (!customTabsBrowsers.isEmpty()) { + return customTabsBrowsers.get(0); + } else { + return defaultBrowser; + } + } + +} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java index d7e34e494..2b55cf05f 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java @@ -2,7 +2,6 @@ import android.app.Activity; import android.app.Dialog; -import android.content.Intent; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -63,7 +62,7 @@ class OAuthManager { private PKCE pkce; private Long currentTimeInMillis; - OAuthManager(Auth0 account, AuthCallback callback, Map parameters) { + OAuthManager(@NonNull Auth0 account, @NonNull AuthCallback callback, @NonNull Map parameters) { this.account = account; this.callback = callback; this.parameters = new HashMap<>(parameters); @@ -87,21 +86,12 @@ void startAuthorization(Activity activity, String redirectUri, int requestCode) addClientParameters(parameters, redirectUri); addValidationParameters(parameters); Uri uri = buildAuthorizeUri(); - this.requestCode = requestCode; - final Intent intent; - if (this.useBrowser) { - Log.d(TAG, "About to start the authorization using the Browser"); - intent = new Intent(Intent.ACTION_VIEW, uri); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - activity.startActivity(intent); + + if (useBrowser) { + AuthenticationActivity.authenticateUsingBrowser(activity, uri); } else { - Log.d(TAG, "About to start the authorization using the WebView"); - intent = new Intent(activity, WebAuthActivity.class); - intent.setData(uri); - intent.putExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, parameters.get(KEY_CONNECTION)); - intent.putExtra(WebAuthActivity.FULLSCREEN_EXTRA, useFullScreen); - activity.startActivityForResult(intent, requestCode); + AuthenticationActivity.authenticateUsingWebView(activity, uri, requestCode, parameters.get(KEY_CONNECTION), useFullScreen); } } @@ -121,7 +111,7 @@ boolean resumeAuthorization(AuthorizeResult data) { try { assertNoError(values.get(KEY_ERROR), values.get(KEY_ERROR_DESCRIPTION)); assertValidState(parameters.get(KEY_STATE), values.get(KEY_STATE)); - if (parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { + if (parameters.containsKey(KEY_RESPONSE_TYPE) && parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { assertValidNonce(parameters.get(KEY_NONCE), values.get(KEY_ID_TOKEN)); } @@ -234,7 +224,7 @@ private void addValidationParameters(Map parameters) { String state = getRandomString(parameters.get(KEY_STATE)); parameters.put(KEY_STATE, state); - if (parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { + if (parameters.containsKey(KEY_RESPONSE_TYPE) && parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { String nonce = getRandomString(parameters.get(KEY_NONCE)); parameters.put(KEY_NONCE, nonce); } @@ -255,7 +245,7 @@ private void createPKCE(String redirectUri) { } private boolean shouldUsePKCE() { - return parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_CODE) && PKCE.isAvailable(); + return parameters.containsKey(KEY_RESPONSE_TYPE) && parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_CODE) && PKCE.isAvailable(); } @VisibleForTesting diff --git a/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java b/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java new file mode 100644 index 000000000..1b2bf2e29 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java @@ -0,0 +1,19 @@ +package com.auth0.android.provider; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +public class RedirectActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceBundle) { + super.onCreate(savedInstanceBundle); + Intent intent = new Intent(this, AuthenticationActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.setData(getIntent().getData()); + startActivity(intent); + finish(); + } + +} diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java index d24f97561..aa9aca2eb 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java @@ -266,8 +266,8 @@ Builder withPKCE(PKCE pkce) { */ @Deprecated public void start(@NonNull Activity activity, @NonNull AuthCallback callback, int requestCode) { + managerInstance = null; if (account.getAuthorizeUrl() == null) { - managerInstance = null; final AuthenticationException ex = new AuthenticationException("a0.invalid_authorize_url", "Auth0 authorize URL not properly set. This can be related to an invalid domain."); callback.onFailure(ex); return; @@ -309,7 +309,6 @@ public static Builder init(@NonNull Auth0 account) { return new Builder(account); } - /** * Initialize the WebAuthProvider instance with an Android Context. Additional settings can be configured * in the Builder, like setting the connection name or authentication parameters. @@ -318,7 +317,7 @@ public static Builder init(@NonNull Auth0 account) { * @return a new Builder instance to customize. */ public static Builder init(@NonNull Context context) { - return new Builder(new Auth0(context)); + return init(new Auth0(context)); } /** diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java new file mode 100644 index 000000000..bc50dd76d --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java @@ -0,0 +1,32 @@ +package com.auth0.android.provider; + +import android.content.Intent; + +/** + * Created by lbalmaceda on 6/12/17. + */ + +public class AuthenticationActivityMock extends AuthenticationActivity { + + private CustomTabsController customTabsController; + private Intent deliveredIntent; + + @Override + protected CustomTabsController createCustomTabsController() { + return customTabsController; + } + + @Override + protected void deliverSuccessfulAuthenticationResult(Intent result) { + this.deliveredIntent = result; + super.deliverSuccessfulAuthenticationResult(result); + } + + public void setCustomTabsController(CustomTabsController customTabsController) { + this.customTabsController = customTabsController; + } + + public Intent getDeliveredIntent() { + return deliveredIntent; + } +} diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java new file mode 100644 index 000000000..4a5af498c --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java @@ -0,0 +1,236 @@ +package com.auth0.android.provider; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; +import org.robolectric.util.ActivityController; + +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlag; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.robolectric.Shadows.shadowOf; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = com.auth0.android.auth0.BuildConfig.class, sdk = 18, manifest = Config.NONE) +public class AuthenticationActivityTest { + + @Mock + private Uri uri; + @Mock + private Uri resultUri; + @Mock + private CustomTabsController customTabsController; + @Captor + private ArgumentCaptor intentCaptor; + @Captor + private ArgumentCaptor uriCaptor; + + private Activity callerActivity; + private AuthenticationActivityMock activity; + private ShadowActivity activityShadow; + private ActivityController activityController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + callerActivity = spy(Robolectric.buildActivity(Activity.class).get()); + } + + private void createActivity(Intent configurationIntent) { + activityController = Robolectric.buildActivity(AuthenticationActivityMock.class, configurationIntent); + activity = activityController.get(); + activity.setCustomTabsController(customTabsController); + activityShadow = shadowOf(activity); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateUsingBrowser() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + + verify(customTabsController).bindServiceAndLaunchUri(uriCaptor.capture()); + assertThat(uriCaptor.getValue(), is(notNullValue())); + assertThat(uriCaptor.getValue(), is(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause(); + //Browser is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + activityController.newIntent(authenticationResultIntent); + activityController.resume(); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + verify(customTabsController).unbindService(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldCancelAuthenticationUsingBrowser() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + + verify(customTabsController).bindServiceAndLaunchUri(uriCaptor.capture()); + assertThat(uriCaptor.getValue(), is(notNullValue())); + assertThat(uriCaptor.getValue(), is(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause(); + //Browser is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(null); + activityController.newIntent(authenticationResultIntent); + activityController.resume(); + + assertThat(activity.getDeliveredIntent(), is(nullValue())); + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + verify(customTabsController).unbindService(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateUsingWebView() throws Exception { + verifyNoMoreInteractions(customTabsController); + + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + final ShadowActivity.IntentForResult webViewIntent = activityShadow.getNextStartedActivityForResult(); + + Bundle extras = webViewIntent.intent.getExtras(); + assertThat(extras.containsKey(WebAuthActivity.CONNECTION_NAME_EXTRA), is(true)); + assertThat(extras.getString(WebAuthActivity.CONNECTION_NAME_EXTRA), is("facebook")); + assertThat(extras.containsKey(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + assertThat(extras.getBoolean(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + + assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); + assertThat(webViewIntent.intent, hasData(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause(); + //WebViewActivity is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + activityShadow.receiveResult(webViewIntent.intent, Activity.RESULT_OK, authenticationResultIntent); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldCancelAuthenticationUsingWebView() throws Exception { + verifyNoMoreInteractions(customTabsController); + + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + final ShadowActivity.IntentForResult webViewIntent = activityShadow.getNextStartedActivityForResult(); + + Bundle extras = webViewIntent.intent.getExtras(); + assertThat(extras.containsKey(WebAuthActivity.CONNECTION_NAME_EXTRA), is(true)); + assertThat(extras.getString(WebAuthActivity.CONNECTION_NAME_EXTRA), is("facebook")); + assertThat(extras.containsKey(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + assertThat(extras.getBoolean(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + + assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); + assertThat(webViewIntent.intent, hasData(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause(); + //WebViewActivity is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + activityShadow.receiveResult(webViewIntent.intent, Activity.RESULT_CANCELED, authenticationResultIntent); + + assertThat(activity.getDeliveredIntent(), is(nullValue())); + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchForBrowserAuthentication() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + Intent intent = intentCaptor.getValue(); + Assert.assertThat(intent, is(notNullValue())); + Assert.assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + Assert.assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + Assert.assertThat(intent, hasData(uri)); + + Bundle extras = intent.getExtras(); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(false)); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(false)); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + Assert.assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchForWebViewAuthentication() throws Exception { + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + Intent intent = intentCaptor.getValue(); + Assert.assertThat(intent, is(notNullValue())); + Assert.assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + Assert.assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + Assert.assertThat(intent, hasData(uri)); + + Bundle extras = intentCaptor.getValue().getExtras(); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(true)); + Assert.assertThat(extras.getString(AuthenticationActivity.EXTRA_CONNECTION_NAME), is("facebook")); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + Assert.assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + Assert.assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(false)); + } + +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java new file mode 100644 index 000000000..67896565f --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java @@ -0,0 +1,293 @@ +package com.auth0.android.provider; + +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.customtabs.CustomTabsCallback; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = com.auth0.android.auth0.BuildConfig.class, sdk = 21, manifest = Config.NONE) +public class CustomTabsControllerTest { + + private static final String DEFAULT_BROWSER_PACKAGE = "com.auth0.browser"; + private static final String CHROME_STABLE_PACKAGE = "com.android.chrome"; + private static final String CHROME_SYSTEM_PACKAGE = "com.google.android.apps.chrome"; + private static final String CHROME_BETA_PACKAGE = "com.android.chrome.beta"; + private static final String CHROME_DEV_PACKAGE = "com.android.chrome.dev"; + private static final String CUSTOM_TABS_BROWSER_1 = "com.browser.customtabs1"; + private static final String CUSTOM_TABS_BROWSER_2 = "com.browser.customtabs2"; + + @Mock + private Context context; + @Mock + private Uri uri; + @Mock + private CustomTabsClient customTabsClient; + @Captor + private ArgumentCaptor launchIntentCaptor; + @Captor + private ArgumentCaptor serviceIntentCaptor; + @Captor + private ArgumentCaptor serviceConnectionCaptor; + @Rule + public ExpectedException exception = ExpectedException.none(); + + private CustomTabsController controller; + + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + controller = new CustomTabsController(context, DEFAULT_BROWSER_PACKAGE); + } + + @Test + public void shouldChooseNullBrowserIfNoBrowserAvailable() throws Exception { + preparePackageManagerForCustomTabs(null); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(nullValue())); + } + + @Test + public void shouldChooseDefaultBrowserIfIsCustomTabsCapable() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, DEFAULT_BROWSER_PACKAGE); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(DEFAULT_BROWSER_PACKAGE)); + } + + @Test + public void shouldChooseDefaultBrowserIfNoOtherBrowserIsCustomTabsCapable() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(DEFAULT_BROWSER_PACKAGE)); + } + + @Test + public void shouldChooseChromeStableOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_STABLE_PACKAGE, CHROME_SYSTEM_PACKAGE, CHROME_BETA_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_STABLE_PACKAGE)); + } + + @Test + public void shouldChooseChromeSystemOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_SYSTEM_PACKAGE, CHROME_BETA_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_SYSTEM_PACKAGE)); + } + + @Test + public void shouldChooseChromeBetaOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_BETA_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_BETA_PACKAGE)); + } + + @Test + public void shouldChooseChromeDevOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_DEV_PACKAGE)); + } + + @Test + public void shouldChooseCustomTabsCapableBrowserIfAvailable() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CUSTOM_TABS_BROWSER_1)); + } + + @Test + public void shouldNotUnbindIfNeverBound() throws Exception { + controller.unbindService(); + verify(context, never()).unbindService(serviceConnectionCaptor.capture()); + } + + @Test + public void shouldUnbind() throws Exception { + bindService(true, false); + connectBoundService(); + + controller.unbindService(); + verify(context).unbindService(serviceConnectionCaptor.capture()); + final CustomTabsServiceConnection connection = serviceConnectionCaptor.getValue(); + CustomTabsServiceConnection controllerConnection = controller; + assertThat(connection, is(equalTo(controllerConnection))); + } + + @SuppressWarnings("WrongConstant") + @Test + public void shouldBind() throws Exception { + boolean success = bindService(true, false); + assertThat(success, is(true)); + verify(context, never()).startActivity(any(Intent.class)); + } + + @SuppressWarnings("WrongConstant") + @Test + public void shouldNotBindIfAlreadyBound() throws Exception { + bindService(true, false); + connectBoundService(); + + boolean success = bindService(false, false); + assertThat(success, is(false)); + verify(context, never()).startActivity(any(Intent.class)); + } + + @Test + public void shouldFailToBind() throws Exception { + boolean success = bindService(false, false); + assertThat(success, is(false)); + verify(context, never()).startActivity(any(Intent.class)); + } + + @Test + public void shouldBindAndLaunchUri() throws Exception { + boolean success = bindService(true, true); + assertThat(success, is(true)); + connectBoundService(); + + verify(context).startActivity(launchIntentCaptor.capture()); + Intent intent = launchIntentCaptor.getValue(); + assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(intent.getData(), is(uri)); + assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + } + + @Test + public void shouldFailToBindButLaunchUri() throws Exception { + boolean success = bindService(false, true); + assertThat(success, is(false)); + + verify(context).startActivity(launchIntentCaptor.capture()); + Intent intent = launchIntentCaptor.getValue(); + assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(intent.getData(), is(uri)); + assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + } + + @Test + public void shouldLaunchUri() throws Exception { + controller.launchUri(uri); + verify(context).startActivity(launchIntentCaptor.capture()); + Intent intent = launchIntentCaptor.getValue(); + assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(true)); + assertThat(intent.getData(), is(uri)); + assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + } + + @Test + public void shouldNotLaunchUriIfContextNoLongerValid() throws Exception { + controller.clearContext(); + controller.launchUri(uri); + verify(context, never()).startActivity(any(Intent.class)); + } + + @Test + public void shouldLaunchUriWithFallbackIfCustomTabIntentFails() throws Exception { + doThrow(ActivityNotFoundException.class) + .doNothing() + .when(context).startActivity(any(Intent.class)); + controller.launchUri(uri); + verify(context, times(2)).startActivity(launchIntentCaptor.capture()); + List intents = launchIntentCaptor.getAllValues(); + + Intent customTabIntent = intents.get(0); + assertThat(customTabIntent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(customTabIntent.getData(), is(uri)); + assertThat(customTabIntent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + assertThat(customTabIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(true)); + + Intent fallbackIntent = intents.get(1); + assertThat(fallbackIntent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(fallbackIntent.getData(), is(uri)); + assertThat(fallbackIntent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + assertThat(fallbackIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(false)); + } + + //Helper Methods + + @SuppressWarnings("WrongConstant") + private boolean bindService(boolean willSucceed, boolean alsoLaunchUri) { + Mockito.doReturn(willSucceed).when(context).bindService( + serviceIntentCaptor.capture(), + serviceConnectionCaptor.capture(), + Mockito.anyInt()); + boolean success = alsoLaunchUri ? controller.bindServiceAndLaunchUri(uri) : controller.bindService(); + Intent intent = serviceIntentCaptor.getValue(); + assertThat(intent.getPackage(), is(DEFAULT_BROWSER_PACKAGE)); + return success; + } + + private void connectBoundService() { + CustomTabsServiceConnection conn = serviceConnectionCaptor.getValue(); + conn.onCustomTabsServiceConnected(new ComponentName(DEFAULT_BROWSER_PACKAGE, DEFAULT_BROWSER_PACKAGE + ".CustomTabsService"), customTabsClient); + verify(customTabsClient).newSession(Matchers.eq(null)); + verify(customTabsClient).warmup(eq(0L)); + } + + @SuppressWarnings("WrongConstant") + private void preparePackageManagerForCustomTabs(String defaultBrowserPackage, String... customTabEnabledPackages) { + PackageManager pm = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(pm); + ResolveInfo defaultPackage = resolveInfoForPackageName(defaultBrowserPackage); + when(pm.resolveActivity(any(Intent.class), anyInt())).thenReturn(defaultPackage); + when(pm.resolveService(any(Intent.class), eq(0))).thenReturn(defaultPackage); + + List customTabsCapable = new ArrayList<>(); + for (String customTabEnabledPackage : customTabEnabledPackages) { + customTabsCapable.add(resolveInfoForPackageName(customTabEnabledPackage)); + } + when(pm.queryIntentActivities(any(Intent.class), eq(0))).thenReturn(customTabsCapable); + } + + private ResolveInfo resolveInfoForPackageName(String packageName) { + if (packageName == null) { + return null; + } + ResolveInfo resInfo = mock(ResolveInfo.class); + resInfo.activityInfo = new ActivityInfo(); + resInfo.activityInfo.packageName = packageName; + return resInfo; + } +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java index 35a39dfb4..133bf8b96 100644 --- a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java @@ -17,7 +17,6 @@ import java.util.Date; import java.util.HashMap; -import static org.cyberneko.html.HTMLElements.HEAD; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java index 752ca32aa..4a9e7103c 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java @@ -2,11 +2,12 @@ import android.app.Activity; import android.app.Dialog; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.content.res.Resources; import android.net.Uri; +import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Base64; @@ -36,9 +37,7 @@ import java.util.Map; import java.util.Set; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlag; import static android.support.test.espresso.intent.matcher.UriMatchers.hasHost; import static android.support.test.espresso.intent.matcher.UriMatchers.hasParamWithName; @@ -56,9 +55,10 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -91,6 +91,10 @@ public void setUp() throws Exception { MockitoAnnotations.initMocks(this); activity = spy(Robolectric.buildActivity(Activity.class).get()); account = new Auth0("clientId", "domain"); + + //Next line is needed to avoid CustomTabService from being bound to Test environment + //noinspection WrongConstant + doReturn(false).when(activity).bindService(any(Intent.class), any(ServiceConnection.class), anyInt()); } @SuppressWarnings("deprecation") @@ -946,56 +950,55 @@ public void shouldBuildAuthorizeURIWithResponseTypeCode() throws Exception { @SuppressWarnings("deprecation") @Test public void shouldStartWithBrowser() throws Exception { - Activity activity = mock(Activity.class); - Context appContext = mock(Context.class); - when(activity.getApplicationContext()).thenReturn(appContext); - when(activity.getPackageName()).thenReturn("package"); - when(appContext.getPackageName()).thenReturn("package"); WebAuthProvider.init(account) .useBrowser(true) .useCodeGrant(false) .start(activity, callback); - ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); verify(activity).startActivity(intentCaptor.capture()); - assertThat(intentCaptor.getValue(), is(notNullValue())); - assertThat(intentCaptor.getValue(), hasAction(Intent.ACTION_VIEW)); - assertThat(intentCaptor.getValue(), hasFlag(Intent.FLAG_ACTIVITY_NO_HISTORY)); + Intent intent = intentCaptor.getValue(); + assertThat(intent, is(notNullValue())); + assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + assertThat(intent.getData(), is(notNullValue())); + + Bundle extras = intentCaptor.getValue().getExtras(); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(false)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(false)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); } @SuppressWarnings("deprecation") @Test public void shouldStartWithWebViewAndDefaultConnection() throws Exception { - Activity activity = mock(Activity.class); - Context appContext = mock(Context.class); - when(activity.getApplicationContext()).thenReturn(appContext); - when(activity.getPackageName()).thenReturn("package"); - when(appContext.getPackageName()).thenReturn("package"); WebAuthProvider.init(account) .useBrowser(false) .useCodeGrant(false) .useFullscreen(false) .start(activity, callback, REQUEST_CODE); - ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); verify(activity).startActivityForResult(intentCaptor.capture(), any(Integer.class)); - ComponentName expComponent = new ComponentName("package", WebAuthActivity.class.getName()); - assertThat(intentCaptor.getValue(), is(notNullValue())); - assertThat(intentCaptor.getValue(), hasComponent(expComponent)); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, null)); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.FULLSCREEN_EXTRA, false)); + Intent intent = intentCaptor.getValue(); + assertThat(intent, is(notNullValue())); + assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + assertThat(intent.getData(), is(notNullValue())); + + Bundle extras = intentCaptor.getValue().getExtras(); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(true)); + assertThat(extras.getString(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(nullValue())); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(false)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(false)); } @SuppressWarnings("deprecation") @Test public void shouldStartWithWebViewAndCustomConnection() throws Exception { - Activity activity = mock(Activity.class); - Context appContext = mock(Context.class); - when(activity.getApplicationContext()).thenReturn(appContext); - when(activity.getPackageName()).thenReturn("package"); - when(appContext.getPackageName()).thenReturn("package"); WebAuthProvider.init(account) .useBrowser(false) .withConnection("my-connection") @@ -1003,14 +1006,21 @@ public void shouldStartWithWebViewAndCustomConnection() throws Exception { .useFullscreen(true) .start(activity, callback); - ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); verify(activity).startActivityForResult(intentCaptor.capture(), any(Integer.class)); - ComponentName expComponent = new ComponentName("package", WebAuthActivity.class.getName()); - assertThat(intentCaptor.getValue(), is(notNullValue())); - assertThat(intentCaptor.getValue(), hasComponent(expComponent)); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, "my-connection")); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.FULLSCREEN_EXTRA, true)); + Intent intent = intentCaptor.getValue(); + assertThat(intent, is(notNullValue())); + assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + assertThat(intent.getData(), is(notNullValue())); + + Bundle extras = intent.getExtras(); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(true)); + assertThat(extras.getString(AuthenticationActivity.EXTRA_CONNECTION_NAME), is("my-connection")); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(false)); } @SuppressWarnings({"deprecation", "ThrowableResultOfMethodCallIgnored"}) @@ -1070,6 +1080,7 @@ public void shouldResumeWithIntentWithResponseTypeIdToken() throws Exception { Intent intent = createAuthIntent(createHash(customNonceJWT(sentNonce), null, null, null, null, sentState, null, null)); assertTrue(WebAuthProvider.resume(intent)); + verify(callback).onSuccess(any(Credentials.class)); } @@ -1119,7 +1130,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(intent)); ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); @@ -1158,7 +1169,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(REQUEST_CODE, Activity.RESULT_OK, intent)); ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); @@ -1186,10 +1197,18 @@ public void shouldResumeWithIntentWithImplicitGrant() throws Exception { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(intent)); - verify(callback).onSuccess(any(Credentials.class)); + ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); + verify(callback).onSuccess(credentialsCaptor.capture()); + + assertThat(credentialsCaptor.getValue(), is(notNullValue())); + assertThat(credentialsCaptor.getValue().getIdToken(), is("urlId")); + assertThat(credentialsCaptor.getValue().getAccessToken(), is("urlAccess")); + assertThat(credentialsCaptor.getValue().getRefreshToken(), is("urlRefresh")); + assertThat(credentialsCaptor.getValue().getType(), is("urlType")); + assertThat(credentialsCaptor.getValue().getExpiresIn(), is(1111L)); } @SuppressWarnings("deprecation") @@ -1205,10 +1224,18 @@ public void shouldResumeWithRequestCodeWithImplicitGrant() throws Exception { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(REQUEST_CODE, Activity.RESULT_OK, intent)); - verify(callback).onSuccess(any(Credentials.class)); + ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); + verify(callback).onSuccess(credentialsCaptor.capture()); + + assertThat(credentialsCaptor.getValue(), is(notNullValue())); + assertThat(credentialsCaptor.getValue().getIdToken(), is("urlId")); + assertThat(credentialsCaptor.getValue().getAccessToken(), is("urlAccess")); + assertThat(credentialsCaptor.getValue().getRefreshToken(), is("urlRefresh")); + assertThat(credentialsCaptor.getValue().getType(), is("urlType")); + assertThat(credentialsCaptor.getValue().getExpiresIn(), is(1111L)); } @Test @@ -1253,7 +1280,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { .useCodeGrant(true) .withPKCE(pkce) .start(activity, callback); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, "1234567890", null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, "1234567890", null, null)); assertTrue(WebAuthProvider.resume(intent)); verify(callback).onFailure(dialog); @@ -1276,7 +1303,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { .useCodeGrant(true) .withPKCE(pkce) .start(activity, callback); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, "1234567890", null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, "1234567890", null, null)); assertTrue(WebAuthProvider.resume(intent)); verify(callback).onFailure(exception); From 5a06bd05727bdc38b06f08703f347bcb09d30ece Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 13 Jun 2017 12:26:59 -0300 Subject: [PATCH 2/9] add missing tests --- .../provider/AuthenticationActivity.java | 7 +- .../android/provider/RedirectActivity.java | 4 +- .../provider/AuthenticationActivityMock.java | 4 +- .../provider/AuthenticationActivityTest.java | 101 +++++++++++++++++- .../provider/RedirectActivityTest.java | 86 +++++++++++++++ 5 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 auth0/src/test/java/com/auth0/android/provider/RedirectActivityTest.java diff --git a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java index 113e619e4..6eae7b515 100644 --- a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java +++ b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; @@ -111,13 +112,13 @@ private void launchAuthenticationIntent() { } Log.e(TAG, "OnCreate: Launching Intent.VIEW intent"); - customTabsController = createCustomTabsController(); + customTabsController = createCustomTabsController(this); customTabsController.bindServiceAndLaunchUri(authorizeUri); } @VisibleForTesting - protected CustomTabsController createCustomTabsController() { - return new CustomTabsController(this); + protected CustomTabsController createCustomTabsController(@NonNull Context context) { + return new CustomTabsController(context); } @VisibleForTesting diff --git a/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java b/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java index 1b2bf2e29..75eea1f09 100644 --- a/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java +++ b/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java @@ -11,7 +11,9 @@ public void onCreate(Bundle savedInstanceBundle) { super.onCreate(savedInstanceBundle); Intent intent = new Intent(this, AuthenticationActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.setData(getIntent().getData()); + if (getIntent() != null) { + intent.setData(getIntent().getData()); + } startActivity(intent); finish(); } diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java index bc50dd76d..f249cb913 100644 --- a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java @@ -1,6 +1,8 @@ package com.auth0.android.provider; +import android.content.Context; import android.content.Intent; +import android.support.annotation.NonNull; /** * Created by lbalmaceda on 6/12/17. @@ -12,7 +14,7 @@ public class AuthenticationActivityMock extends AuthenticationActivity { private Intent deliveredIntent; @Override - protected CustomTabsController createCustomTabsController() { + protected CustomTabsController createCustomTabsController(@NonNull Context context) { return customTabsController; } diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java index 4a5af498c..bc1daeddc 100644 --- a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java @@ -15,6 +15,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowActivity; import org.robolectric.util.ActivityController; @@ -78,13 +79,41 @@ public void shouldAuthenticateUsingBrowser() throws Exception { assertThat(uriCaptor.getValue(), is(notNullValue())); assertThat(uriCaptor.getValue(), is(uri)); assertThat(activity.getDeliveredIntent(), is(nullValue())); - activityController.pause(); + activityController.pause().stop(); //Browser is shown Intent authenticationResultIntent = new Intent(); authenticationResultIntent.setData(resultUri); activityController.newIntent(authenticationResultIntent); - activityController.resume(); + activityController.start().resume(); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + verify(customTabsController).unbindService(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateAfterRecreatedUsingBrowser() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + + verify(customTabsController).bindServiceAndLaunchUri(uriCaptor.capture()); + assertThat(uriCaptor.getValue(), is(notNullValue())); + assertThat(uriCaptor.getValue(), is(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + //Browser is shown + //Memory needed. Let's kill the activity + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + recreateAndCallNewIntent(authenticationResultIntent); assertThat(activity.getDeliveredIntent(), is(notNullValue())); assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); @@ -108,13 +137,13 @@ public void shouldCancelAuthenticationUsingBrowser() throws Exception { assertThat(uriCaptor.getValue(), is(notNullValue())); assertThat(uriCaptor.getValue(), is(uri)); assertThat(activity.getDeliveredIntent(), is(nullValue())); - activityController.pause(); + activityController.pause().stop(); //Browser is shown Intent authenticationResultIntent = new Intent(); authenticationResultIntent.setData(null); activityController.newIntent(authenticationResultIntent); - activityController.resume(); + activityController.start().resume(); assertThat(activity.getDeliveredIntent(), is(nullValue())); assertThat(activity.isFinishing(), is(true)); @@ -159,6 +188,41 @@ public void shouldAuthenticateUsingWebView() throws Exception { activityController.destroy(); } + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateAfterRecreatedUsingWebView() throws Exception { + verifyNoMoreInteractions(customTabsController); + + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + final ShadowActivity.IntentForResult webViewIntent = activityShadow.getNextStartedActivityForResult(); + + Bundle extras = webViewIntent.intent.getExtras(); + assertThat(extras.containsKey(WebAuthActivity.CONNECTION_NAME_EXTRA), is(true)); + assertThat(extras.getString(WebAuthActivity.CONNECTION_NAME_EXTRA), is("facebook")); + assertThat(extras.containsKey(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + assertThat(extras.getBoolean(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + + assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); + assertThat(webViewIntent.intent, hasData(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + //WebViewActivity is shown + //Memory needed. Let's kill the activity + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + recreateAndCallActivityResult(123, authenticationResultIntent); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + } + @SuppressWarnings("deprecation") @Test public void shouldCancelAuthenticationUsingWebView() throws Exception { @@ -180,7 +244,7 @@ public void shouldCancelAuthenticationUsingWebView() throws Exception { assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); assertThat(webViewIntent.intent, hasData(uri)); assertThat(activity.getDeliveredIntent(), is(nullValue())); - activityController.pause(); + activityController.pause().stop(); //WebViewActivity is shown Intent authenticationResultIntent = new Intent(); @@ -233,4 +297,31 @@ public void shouldLaunchForWebViewAuthentication() throws Exception { Assert.assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(false)); } + @Test + public void shouldCreateCustomTabsController() throws Exception { + final AuthenticationActivity authenticationActivity = new AuthenticationActivity(); + final CustomTabsController controller = authenticationActivity.createCustomTabsController(RuntimeEnvironment.application); + + assertThat(controller, is(notNullValue())); + } + + private void recreateAndCallNewIntent(Intent data) { + Bundle outState = new Bundle(); + activityController.saveInstanceState(outState); + activityController.pause().stop().destroy(); + createActivity(null); + activityController.create(outState).start().restoreInstanceState(outState); + activityController.newIntent(data); + activityController.resume(); + } + + private void recreateAndCallActivityResult(int reqCode, Intent data) { + Bundle outState = new Bundle(); + activityController.saveInstanceState(outState); + activityController.pause().stop().destroy(); + createActivity(null); + activityController.create(outState).start().restoreInstanceState(outState); + activity.onActivityResult(reqCode, Activity.RESULT_OK, data); + activityController.resume(); + } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/RedirectActivityTest.java b/auth0/src/test/java/com/auth0/android/provider/RedirectActivityTest.java new file mode 100644 index 000000000..655d567ec --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/RedirectActivityTest.java @@ -0,0 +1,86 @@ +package com.auth0.android.provider; + +import android.content.Intent; +import android.net.Uri; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; +import org.robolectric.util.ActivityController; + +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlags; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.robolectric.Shadows.shadowOf; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = com.auth0.android.auth0.BuildConfig.class, sdk = 18, manifest = Config.NONE) +public class RedirectActivityTest { + + + @Mock + private Uri uri; + + private RedirectActivity activity; + private ShadowActivity activityShadow; + private ActivityController activityController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + private void createActivity(Intent launchIntent) { + activityController = Robolectric.buildActivity(RedirectActivity.class, launchIntent); + activity = activityController.get(); + activityShadow = shadowOf(activity); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchAuthenticationActivityWithDataOnSuccess() throws Exception { + Intent resultIntent = new Intent(); + resultIntent.setData(uri); + + createActivity(resultIntent); + activityController.create().start().resume(); + + Intent authenticationIntent = activityShadow.getNextStartedActivity(); + assertThat(authenticationIntent, is(notNullValue())); + assertThat(authenticationIntent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(authenticationIntent, hasData(uri)); + assertThat(authenticationIntent, hasFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)); + + assertThat(activity.isFinishing(), is(true)); + activityController.destroy(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchAuthenticationActivityWithoutDataOnCancel() throws Exception { + Intent resultIntent = new Intent(); + resultIntent.setData(null); + + createActivity(resultIntent); + activityController.create().start().resume(); + + Intent authenticationIntent = activityShadow.getNextStartedActivity(); + assertThat(authenticationIntent, is(notNullValue())); + assertThat(authenticationIntent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(authenticationIntent.getData(), is(nullValue())); + assertThat(authenticationIntent, hasFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)); + + assertThat(activity.isFinishing(), is(true)); + activityController.destroy(); + } +} \ No newline at end of file From b59bc8a10e195b15f841595dcc6dddd525cc9a81 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 13 Jun 2017 14:36:16 -0300 Subject: [PATCH 3/9] update README with new WebAuth usage. Allow to change the scheme by using a placeholder. --- README.md | 62 +++++++++++-------- auth0/src/main/AndroidManifest.xml | 2 +- .../provider/AuthenticationActivity.java | 10 +-- .../android/provider/WebAuthProvider.java | 11 ++-- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9d77e61d2..e57cf71bd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Android API version 15 or newer ## Installation -###Gradle +### Gradle Auth0.android is available through [Gradle](https://gradle.org/). To install it, simply add the following line to your `build.gradle` file: @@ -269,13 +269,33 @@ First go to [Auth0 Dashboard](https://manage.auth0.com/#/applications) and go to https://{YOUR_AUTH0_DOMAIN}/android/{YOUR_APP_PACKAGE_NAME}/callback ``` -Open your app's `AndroidManifest.xml` file and add the following permission. +Remember to replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name. -```xml - + +Next, define placeholders for the Auth0 Domain and Scheme which are going to be used internally by the library to register an **intent-filter**. Go to your application's `build.gradle` file and add the `manifestPlaceholders` line as shown below: + +```groovy +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + defaultConfig { + applicationId "com.auth0.samples" + minSdkVersion 15 + targetSdkVersion 25 + //... + + //---> Add the next line + manifestPlaceholders = [auth0Domain: "@string/auth0_domain", auth0Scheme: "https"] + //<--- + } + //... +} ``` -Also register the intent filters inside your activity's tag, so you can receive the call in your activity. Note that you will have to specify the callback url inside the `data` tag. +It's a good practice to define reusable resources like `@string/auth0_domain` but you can also hard code the value. In case you're using a [custom scheme](#a-note-about-app-deep-linking) you must update the `auth0Scheme` property. + +Alternatively you can define your own **intent-filter** for the `RedirectActivity` in the `AndroidManifest.xml` file replacing the one defined by the library. If you choose to do this, the `manifestPlaceholders` don't need to be set. In your manifest inside your application's tag add the `RedirectActivity` declaration: ```xml @@ -283,22 +303,18 @@ Also register the intent filters inside your activity's tag, so you can receive - - + android:name="com.auth0.android.provider.RedirectActivity"> + - @@ -306,23 +322,17 @@ Also register the intent filters inside your activity's tag, so you can receive ``` -Make sure the Activity's **launchMode** is declared as "singleTask" or the result won't come back after the authentication. +Remember to replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name. -When you launch the WebAuthProvider you'll expect a result back. To capture the response override the `onNewIntent` method and call `WebAuthProvider.resume()` with the received parameters: +Finally, don't forget to add the internet permission. -```java -public class MyActivity extends Activity { +```xml + +``` - @Override - protected void onNewIntent(Intent intent) { - if (WebAuthProvider.resume(intent)) { - return; - } - super.onNewIntent(intent); - } -} -``` +> In versions 1.8.0 and before you had to define the **intent-filter** in your own activity to capture the result in the `onNewIntent` method and call `WebAuthProvider.resume()` with the received intent. This call is no longer required for versions greater than 1.8.0 as it's done for you by the library. + ##### A note about App Deep Linking: @@ -451,7 +461,7 @@ android { ref: https://github.com/square/okio/issues/58#issuecomment-72672263 -##Proguard +## Proguard The rules should be applied automatically if your application is using `minifyEnabled = true`. If you want to include them manually check the [proguard directory](proguard). By default you should at least use the following files: * `proguard-okio.pro` diff --git a/auth0/src/main/AndroidManifest.xml b/auth0/src/main/AndroidManifest.xml index 6f2354e4b..c69aea785 100644 --- a/auth0/src/main/AndroidManifest.xml +++ b/auth0/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ + android:scheme="${auth0Scheme}" /> diff --git a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java index 6eae7b515..ea8290007 100644 --- a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java +++ b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java @@ -10,7 +10,7 @@ import android.support.annotation.VisibleForTesting; import android.util.Log; -class AuthenticationActivity extends Activity { +public class AuthenticationActivity extends Activity { private static final String TAG = AuthenticationActivity.class.getSimpleName(); @@ -21,7 +21,7 @@ class AuthenticationActivity extends Activity { private boolean intentLaunched; private CustomTabsController customTabsController; - public static void authenticateUsingBrowser(Context context, Uri authorizeUri) { + static void authenticateUsingBrowser(Context context, Uri authorizeUri) { Intent intent = new Intent(context, AuthenticationActivity.class); intent.setData(authorizeUri); intent.putExtra(AuthenticationActivity.EXTRA_USE_BROWSER, true); @@ -29,7 +29,7 @@ public static void authenticateUsingBrowser(Context context, Uri authorizeUri) { context.startActivity(intent); } - public static void authenticateUsingWebView(Activity activity, Uri authorizeUri, int requestCode, String connection, boolean useFullScreen) { + static void authenticateUsingWebView(Activity activity, Uri authorizeUri, int requestCode, String connection, boolean useFullScreen) { Intent intent = new Intent(activity, AuthenticationActivity.class); intent.setData(authorizeUri); intent.putExtra(AuthenticationActivity.EXTRA_USE_BROWSER, false); @@ -117,12 +117,12 @@ private void launchAuthenticationIntent() { } @VisibleForTesting - protected CustomTabsController createCustomTabsController(@NonNull Context context) { + CustomTabsController createCustomTabsController(@NonNull Context context) { return new CustomTabsController(context); } @VisibleForTesting - protected void deliverSuccessfulAuthenticationResult(Intent result) { + void deliverSuccessfulAuthenticationResult(Intent result) { WebAuthProvider.resume(result); } diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java index aa9aca2eb..813a4ceae 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java @@ -255,9 +255,7 @@ Builder withPKCE(PKCE pkce) { } /** - * Begins the authentication flow. - * Make sure to override your activity's onNewIntent() and onActivityResult() methods, - * and call this provider's resume() method with the received parameters. + * Request user Authentication. The result will be received in the callback. * * @param activity context to run the authentication * @param callback to receive the parsed results @@ -285,8 +283,7 @@ public void start(@NonNull Activity activity, @NonNull AuthCallback callback, in } /** - * Begins the authentication flow. - * Make sure to override your activity's onNewIntent() method and call this provider's resume() method with the received parameters. + * Request user Authentication. The result will be received in the callback. * * @param activity context to run the authentication * @param callback to receive the parsed results @@ -323,6 +320,8 @@ public static Builder init(@NonNull Context context) { /** * Finishes the authentication flow by passing the data received in the activity's onActivityResult() callback. * The final authentication result will be delivered to the callback specified when calling start(). + *

+ * This is no longer required to be called, the authentication is handled internally as long as you've correctly setup the intent-filter. * * @param requestCode the request code received on the onActivityResult() call * @param resultCode the result code received on the onActivityResult() call @@ -347,6 +346,8 @@ public static boolean resume(int requestCode, int resultCode, @Nullable Intent i /** * Finishes the authentication flow by passing the data received in the activity's onNewIntent() callback. * The final authentication result will be delivered to the callback specified when calling start(). + *

+ * This is no longer required to be called, the authentication is handled internally as long as you've correctly setup the intent-filter. * * @param intent the data received on the onNewIntent() call * @return true if a result was expected and has a valid format, or false if not. From 2dd43680f256556f0ca0db39343f3e1e2cbf1000 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 13 Jun 2017 16:14:00 -0300 Subject: [PATCH 4/9] fix WebView result parsing --- .../android/provider/AuthenticationActivity.java | 16 +++------------- .../android/provider/CustomTabsController.java | 11 ++++++----- .../com/auth0/android/provider/OAuthManager.java | 1 + .../provider/AuthenticationActivityTest.java | 4 ++++ .../provider/CustomTabsControllerTest.java | 6 ------ 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java index ea8290007..818fbc1aa 100644 --- a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java +++ b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java @@ -8,16 +8,14 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; -import android.util.Log; public class AuthenticationActivity extends Activity { - private static final String TAG = AuthenticationActivity.class.getSimpleName(); - static final String EXTRA_USE_BROWSER = "com.auth0.android.EXTRA_USE_BROWSER"; static final String EXTRA_USE_FULL_SCREEN = "com.auth0.android.EXTRA_USE_FULL_SCREEN"; static final String EXTRA_CONNECTION_NAME = "com.auth0.android.EXTRA_CONNECTION_NAME"; private static final String EXTRA_INTENT_LAUNCHED = "com.auth0.android.EXTRA_INTENT_LAUNCHED"; + private boolean intentLaunched; private CustomTabsController customTabsController; @@ -47,14 +45,12 @@ protected void onNewIntent(Intent intent) { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - Log.e(TAG, "Activity result"); if (resultCode == RESULT_OK) { deliverSuccessfulAuthenticationResult(data); } finish(); } - @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -72,19 +68,14 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { @Override protected void onResume() { super.onResume(); - Log.e(TAG, "onResume: intentLaunched = " + intentLaunched); if (!intentLaunched) { - Log.e(TAG, "OnResume: Launching authentication intent"); intentLaunched = true; launchAuthenticationIntent(); return; } if (getIntent().getData() != null) { - Log.e(TAG, "OnResume: Passing result to the WebAuthProvider"); deliverSuccessfulAuthenticationResult(getIntent()); - } else { - Log.e(TAG, "OnResume: The authentication was canceled"); } finish(); } @@ -102,16 +93,15 @@ private void launchAuthenticationIntent() { Bundle extras = getIntent().getExtras(); final Uri authorizeUri = getIntent().getData(); if (!extras.getBoolean(EXTRA_USE_BROWSER, true)) { - Log.e(TAG, "OnCreate: Launching WebAuthActivity intent for result"); Intent intent = new Intent(this, WebAuthActivity.class); intent.setData(authorizeUri); intent.putExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, extras.getString(EXTRA_CONNECTION_NAME)); intent.putExtra(WebAuthActivity.FULLSCREEN_EXTRA, extras.getBoolean(EXTRA_USE_FULL_SCREEN)); - startActivityForResult(intent, -1); + //The request code value can be ignored + startActivityForResult(intent, 33); return; } - Log.e(TAG, "OnCreate: Launching Intent.VIEW intent"); customTabsController = createCustomTabsController(this); customTabsController.bindServiceAndLaunchUri(authorizeUri); } diff --git a/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java index 5e1080387..163fd52fd 100644 --- a/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java +++ b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +@SuppressWarnings("WeakerAccess") class CustomTabsController extends CustomTabsServiceConnection { private static final String TAG = CustomTabsController.class.getSimpleName(); @@ -68,6 +69,7 @@ public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabs public void onServiceDisconnected(ComponentName componentName) { Log.d(TAG, "CustomTabs Service disconnected"); session = null; + isBound = false; } /** @@ -82,7 +84,7 @@ public boolean bindService() { if (!isBound && context != null) { success = CustomTabsClient.bindCustomTabsService(context, preferredPackage, this); } - Log.d(TAG, "Bound: " + success); + Log.v(TAG, "Bind request result: " + success); return success; } @@ -92,11 +94,10 @@ public boolean bindService() { public void unbindService() { Log.v(TAG, "Trying to unbind the service"); Context context = this.context.get(); - if (isBound && context != null) { + if (context != null) { context.unbindService(this); } - this.isBound = false; - this.nextUri = null; + nextUri = null; } /** @@ -106,7 +107,7 @@ public void unbindService() { * @return true if the request to bind the service was successful, false if the service was already bound or it couldn't be bound. */ public boolean bindServiceAndLaunchUri(@NonNull Uri uri) { - this.nextUri = uri; + nextUri = uri; boolean boundRequestSuccess = bindService(); if (isBound || !boundRequestSuccess) { launchUri(uri); diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java index 2b55cf05f..385b2b553 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; +@SuppressWarnings("WeakerAccess") class OAuthManager { private static final String TAG = OAuthManager.class.getSimpleName(); diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java index bc1daeddc..936ffa1f2 100644 --- a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java @@ -24,6 +24,7 @@ import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlag; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; @@ -172,6 +173,7 @@ public void shouldAuthenticateUsingWebView() throws Exception { assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); assertThat(webViewIntent.intent, hasData(uri)); + assertThat(webViewIntent.requestCode, is(greaterThan(0))); assertThat(activity.getDeliveredIntent(), is(nullValue())); activityController.pause(); //WebViewActivity is shown @@ -208,6 +210,7 @@ public void shouldAuthenticateAfterRecreatedUsingWebView() throws Exception { assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); assertThat(webViewIntent.intent, hasData(uri)); + assertThat(webViewIntent.requestCode, is(greaterThan(0))); assertThat(activity.getDeliveredIntent(), is(nullValue())); //WebViewActivity is shown //Memory needed. Let's kill the activity @@ -243,6 +246,7 @@ public void shouldCancelAuthenticationUsingWebView() throws Exception { assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); assertThat(webViewIntent.intent, hasData(uri)); + assertThat(webViewIntent.requestCode, is(greaterThan(0))); assertThat(activity.getDeliveredIntent(), is(nullValue())); activityController.pause().stop(); //WebViewActivity is shown diff --git a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java index 67896565f..263a24086 100644 --- a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java @@ -136,12 +136,6 @@ public void shouldChooseCustomTabsCapableBrowserIfAvailable() throws Exception { assertThat(bestPackage, is(CUSTOM_TABS_BROWSER_1)); } - @Test - public void shouldNotUnbindIfNeverBound() throws Exception { - controller.unbindService(); - verify(context, never()).unbindService(serviceConnectionCaptor.capture()); - } - @Test public void shouldUnbind() throws Exception { bindService(true, false); From 3c24fd8df5610596921b72157844586cc3eabc44 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 15 Jun 2017 14:35:26 -0300 Subject: [PATCH 5/9] remove scheme placeholder, override intent-filter if need a different scheme --- README.md | 14 ++++++++------ auth0/src/main/AndroidManifest.xml | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e57cf71bd..6f5f9b1d4 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ https://{YOUR_AUTH0_DOMAIN}/android/{YOUR_APP_PACKAGE_NAME}/callback Remember to replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name. -Next, define placeholders for the Auth0 Domain and Scheme which are going to be used internally by the library to register an **intent-filter**. Go to your application's `build.gradle` file and add the `manifestPlaceholders` line as shown below: +Next, define a placeholder for the Auth0 Domain which is going to be used internally by the library to register an **intent-filter**. Go to your application's `build.gradle` file and add the `manifestPlaceholders` line as shown below: ```groovy apply plugin: 'com.android.application' @@ -286,16 +286,18 @@ android { //... //---> Add the next line - manifestPlaceholders = [auth0Domain: "@string/auth0_domain", auth0Scheme: "https"] + manifestPlaceholders = [auth0Domain: "@string/auth0_domain"] //<--- } //... } ``` -It's a good practice to define reusable resources like `@string/auth0_domain` but you can also hard code the value. In case you're using a [custom scheme](#a-note-about-app-deep-linking) you must update the `auth0Scheme` property. +It's a good practice to define reusable resources like `@string/auth0_domain` but you can also hard code the value in the file. -Alternatively you can define your own **intent-filter** for the `RedirectActivity` in the `AndroidManifest.xml` file replacing the one defined by the library. If you choose to do this, the `manifestPlaceholders` don't need to be set. In your manifest inside your application's tag add the `RedirectActivity` declaration: +Alternatively, you can declare the `RedirectActivity` in the `AndroidManifest.xml` file with your own **intent-filter** so it overrides the library's default. If you do this then the `manifestPlaceholders` don't need to be set. If you choose to use a [custom scheme](#a-note-about-app-deep-linking) you must define your own intent-filter as explained below. + +In your manifest inside your application's tag add the `RedirectActivity` declaration: ```xml @@ -322,7 +324,7 @@ Alternatively you can define your own **intent-filter** for the `RedirectActivit ``` -Remember to replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name. +If you request a different scheme you must replace the `android:scheme` property. Remember to also replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name. Finally, don't forget to add the internet permission. @@ -331,7 +333,7 @@ Finally, don't forget to add the internet permission. ``` -> In versions 1.8.0 and before you had to define the **intent-filter** in your own activity to capture the result in the `onNewIntent` method and call `WebAuthProvider.resume()` with the received intent. This call is no longer required for versions greater than 1.8.0 as it's done for you by the library. +> In versions 1.8.0 and before you had to define the **intent-filter** inside your activity to capture the result in the `onNewIntent` method and call `WebAuthProvider.resume()` with the received intent. This call is no longer required for versions greater than 1.8.0 as it's now done for you by the library. ##### A note about App Deep Linking: diff --git a/auth0/src/main/AndroidManifest.xml b/auth0/src/main/AndroidManifest.xml index c69aea785..6f2354e4b 100644 --- a/auth0/src/main/AndroidManifest.xml +++ b/auth0/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ + android:scheme="https" /> From 1c04b6083927d31bbe36f9f240ad207da36a15f7 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 19 Jun 2017 15:54:28 -0300 Subject: [PATCH 6/9] wait up to 1 sec to bind custom tab service and default to browser --- .../provider/AuthenticationActivity.java | 3 +- .../provider/CustomTabsController.java | 97 +++++++++---------- .../provider/AuthenticationActivityTest.java | 9 +- .../provider/CustomTabsControllerTest.java | 66 ++++--------- 4 files changed, 72 insertions(+), 103 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java index 818fbc1aa..8450fec02 100644 --- a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java +++ b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java @@ -103,7 +103,8 @@ private void launchAuthenticationIntent() { } customTabsController = createCustomTabsController(this); - customTabsController.bindServiceAndLaunchUri(authorizeUri); + customTabsController.bindService(); + customTabsController.launchUri(authorizeUri); } @VisibleForTesting diff --git a/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java index 163fd52fd..b912c2966 100644 --- a/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java +++ b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java @@ -19,11 +19,15 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; @SuppressWarnings("WeakerAccess") class CustomTabsController extends CustomTabsServiceConnection { private static final String TAG = CustomTabsController.class.getSimpleName(); + private static final long MAX_WAIT_TIME_SECONDS = 1; private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"; //Known Browsers private static final String CHROME_STABLE = "com.android.chrome"; @@ -32,14 +36,16 @@ class CustomTabsController extends CustomTabsServiceConnection { private static final String CHROME_DEV = "com.android.chrome.dev"; private final WeakReference context; + private final AtomicReference session; + private final CountDownLatch sessionLatch; private final String preferredPackage; - private CustomTabsSession session; - private Uri nextUri; - private boolean isBound; + @VisibleForTesting CustomTabsController(@NonNull Context context, @NonNull String browserPackage) { this.context = new WeakReference<>(context); + this.session = new AtomicReference<>(); + this.sessionLatch = new CountDownLatch(1); this.preferredPackage = browserPackage; } @@ -54,38 +60,32 @@ void clearContext() { @Override public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient customTabsClient) { - Log.d(TAG, "CustomTabs Service connected"); - isBound = true; - if (customTabsClient != null) { - customTabsClient.warmup(0L); - session = customTabsClient.newSession(null); - } - if (nextUri != null) { - launchUri(nextUri); + if (customTabsClient == null) { + return; } + Log.d(TAG, "CustomTabs Service connected"); + customTabsClient.warmup(0L); + session.set(customTabsClient.newSession(null)); + sessionLatch.countDown(); } @Override public void onServiceDisconnected(ComponentName componentName) { Log.d(TAG, "CustomTabs Service disconnected"); - session = null; - isBound = false; + session.set(null); } /** * Attempts to bind the Custom Tabs Service to the Context. - * - * @return true if the request to bind the service was successful, false if the service was already bound or it couldn't be bound. */ - public boolean bindService() { + public void bindService() { Log.v(TAG, "Trying to bind the service"); Context context = this.context.get(); boolean success = false; - if (!isBound && context != null) { + if (context != null) { success = CustomTabsClient.bindCustomTabsService(context, preferredPackage, this); } Log.v(TAG, "Bind request result: " + success); - return success; } /** @@ -97,49 +97,46 @@ public void unbindService() { if (context != null) { context.unbindService(this); } - nextUri = null; } /** - * Attempst to bind the Custom Tabs Service to the Context and opens a Uri as soon as possible. + * Opens a Uri in a Custom Tab or Browser. + * The Custom Tab service will be given up to {@link CustomTabsController#MAX_WAIT_TIME_SECONDS} to be connected. + * If it fails to connect the Uri will be opened on a Browser. * * @param uri the uri to open in a Custom Tab or Browser. - * @return true if the request to bind the service was successful, false if the service was already bound or it couldn't be bound. - */ - public boolean bindServiceAndLaunchUri(@NonNull Uri uri) { - nextUri = uri; - boolean boundRequestSuccess = bindService(); - if (isBound || !boundRequestSuccess) { - launchUri(uri); - } - return boundRequestSuccess; - } - - /** - * Opens a Uri in a Custom Tab or Browser - * - * @param uri the uri to open in a Custoam Tab or Browser. */ - public void launchUri(@NonNull Uri uri) { - Context context = this.context.get(); + public void launchUri(@NonNull final Uri uri) { + final Context context = this.context.get(); if (context == null) { Log.v(TAG, "Custom Tab Context was no longer valid."); return; } - Log.d(TAG, "Launching uri.."); - final Intent intent = new CustomTabsIntent.Builder(session) - .build() - .intent; - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.setData(uri); - try { - context.startActivity(intent); - } catch (ActivityNotFoundException ignored) { - Intent fallbackIntent = new Intent(Intent.ACTION_VIEW, uri); - fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - context.startActivity(fallbackIntent); - } + new Thread(new Runnable() { + @Override + public void run() { + boolean available = false; + try { + available = sessionLatch.await(MAX_WAIT_TIME_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + Log.d(TAG, "Launching URI. Custom Tabs available: " + available); + + final Intent intent = new CustomTabsIntent.Builder(session.get()) + .build() + .intent; + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.setData(uri); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException ignored) { + Intent fallbackIntent = new Intent(Intent.ACTION_VIEW, uri); + fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + context.startActivity(fallbackIntent); + } + } + }).start(); } /** diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java index 936ffa1f2..d76a99971 100644 --- a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java @@ -76,7 +76,8 @@ public void shouldAuthenticateUsingBrowser() throws Exception { createActivity(intentCaptor.getValue()); activityController.create().start().resume(); - verify(customTabsController).bindServiceAndLaunchUri(uriCaptor.capture()); + verify(customTabsController).bindService(); + verify(customTabsController).launchUri(uriCaptor.capture()); assertThat(uriCaptor.getValue(), is(notNullValue())); assertThat(uriCaptor.getValue(), is(uri)); assertThat(activity.getDeliveredIntent(), is(nullValue())); @@ -106,7 +107,8 @@ public void shouldAuthenticateAfterRecreatedUsingBrowser() throws Exception { createActivity(intentCaptor.getValue()); activityController.create().start().resume(); - verify(customTabsController).bindServiceAndLaunchUri(uriCaptor.capture()); + verify(customTabsController).bindService(); + verify(customTabsController).launchUri(uriCaptor.capture()); assertThat(uriCaptor.getValue(), is(notNullValue())); assertThat(uriCaptor.getValue(), is(uri)); assertThat(activity.getDeliveredIntent(), is(nullValue())); @@ -134,7 +136,8 @@ public void shouldCancelAuthenticationUsingBrowser() throws Exception { createActivity(intentCaptor.getValue()); activityController.create().start().resume(); - verify(customTabsController).bindServiceAndLaunchUri(uriCaptor.capture()); + verify(customTabsController).bindService(); + verify(customTabsController).launchUri(uriCaptor.capture()); assertThat(uriCaptor.getValue(), is(notNullValue())); assertThat(uriCaptor.getValue(), is(uri)); assertThat(activity.getDeliveredIntent(), is(nullValue())); diff --git a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java index 263a24086..ba83f7d13 100644 --- a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java @@ -24,6 +24,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.internal.verification.VerificationModeFactory; +import org.mockito.verification.Timeout; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -40,7 +42,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -55,6 +57,7 @@ public class CustomTabsControllerTest { private static final String CHROME_DEV_PACKAGE = "com.android.chrome.dev"; private static final String CUSTOM_TABS_BROWSER_1 = "com.browser.customtabs1"; private static final String CUSTOM_TABS_BROWSER_2 = "com.browser.customtabs2"; + private static final long MAX_TEST_WAIT_TIME_MS = 2000; @Mock private Context context; @@ -138,7 +141,7 @@ public void shouldChooseCustomTabsCapableBrowserIfAvailable() throws Exception { @Test public void shouldUnbind() throws Exception { - bindService(true, false); + bindService(true); connectBoundService(); controller.unbindService(); @@ -148,70 +151,35 @@ public void shouldUnbind() throws Exception { assertThat(connection, is(equalTo(controllerConnection))); } - @SuppressWarnings("WrongConstant") - @Test - public void shouldBind() throws Exception { - boolean success = bindService(true, false); - assertThat(success, is(true)); - verify(context, never()).startActivity(any(Intent.class)); - } - - @SuppressWarnings("WrongConstant") - @Test - public void shouldNotBindIfAlreadyBound() throws Exception { - bindService(true, false); - connectBoundService(); - - boolean success = bindService(false, false); - assertThat(success, is(false)); - verify(context, never()).startActivity(any(Intent.class)); - } - - @Test - public void shouldFailToBind() throws Exception { - boolean success = bindService(false, false); - assertThat(success, is(false)); - verify(context, never()).startActivity(any(Intent.class)); - } - @Test public void shouldBindAndLaunchUri() throws Exception { - boolean success = bindService(true, true); - assertThat(success, is(true)); + bindService(true); + controller.launchUri(uri); connectBoundService(); - verify(context).startActivity(launchIntentCaptor.capture()); + verify(context, timeout(MAX_TEST_WAIT_TIME_MS)).startActivity(launchIntentCaptor.capture()); Intent intent = launchIntentCaptor.getValue(); assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(true)); assertThat(intent.getData(), is(uri)); assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); } @Test public void shouldFailToBindButLaunchUri() throws Exception { - boolean success = bindService(false, true); - assertThat(success, is(false)); - - verify(context).startActivity(launchIntentCaptor.capture()); - Intent intent = launchIntentCaptor.getValue(); - assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); - assertThat(intent.getData(), is(uri)); - assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); - } - - @Test - public void shouldLaunchUri() throws Exception { + bindService(false); controller.launchUri(uri); - verify(context).startActivity(launchIntentCaptor.capture()); + + verify(context, timeout(MAX_TEST_WAIT_TIME_MS)).startActivity(launchIntentCaptor.capture()); Intent intent = launchIntentCaptor.getValue(); assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); - assertThat(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(true)); assertThat(intent.getData(), is(uri)); assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); } @Test public void shouldNotLaunchUriIfContextNoLongerValid() throws Exception { + bindService(true); controller.clearContext(); controller.launchUri(uri); verify(context, never()).startActivity(any(Intent.class)); @@ -223,7 +191,8 @@ public void shouldLaunchUriWithFallbackIfCustomTabIntentFails() throws Exception .doNothing() .when(context).startActivity(any(Intent.class)); controller.launchUri(uri); - verify(context, times(2)).startActivity(launchIntentCaptor.capture()); + + verify(context, new Timeout(MAX_TEST_WAIT_TIME_MS, VerificationModeFactory.times(2))).startActivity(launchIntentCaptor.capture()); List intents = launchIntentCaptor.getAllValues(); Intent customTabIntent = intents.get(0); @@ -242,15 +211,14 @@ public void shouldLaunchUriWithFallbackIfCustomTabIntentFails() throws Exception //Helper Methods @SuppressWarnings("WrongConstant") - private boolean bindService(boolean willSucceed, boolean alsoLaunchUri) { + private void bindService(boolean willSucceed) { Mockito.doReturn(willSucceed).when(context).bindService( serviceIntentCaptor.capture(), serviceConnectionCaptor.capture(), Mockito.anyInt()); - boolean success = alsoLaunchUri ? controller.bindServiceAndLaunchUri(uri) : controller.bindService(); + controller.bindService(); Intent intent = serviceIntentCaptor.getValue(); assertThat(intent.getPackage(), is(DEFAULT_BROWSER_PACKAGE)); - return success; } private void connectBoundService() { From 22f7d59dcc00f8ec50fe97ed8297f88241419fdd Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 19 Jun 2017 17:36:16 -0300 Subject: [PATCH 7/9] add dummy placeholder for local tests to pass --- auth0/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/auth0/build.gradle b/auth0/build.gradle index b868ea4ea..6ee655754 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -59,6 +59,12 @@ android { lintOptions { warning 'InvalidPackage' } + buildTypes { + debug { + //Helps tests. buildTypes values are not included in the merged manifest + manifestPlaceholders = [auth0Domain: "auth0.test.domain"] + } + } } dependencies { From a4f529ee011ec0ecf079b171db718532d56add98 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 29 Jun 2017 14:57:11 -0300 Subject: [PATCH 8/9] use applicationId placeholder in the README snippet --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6f5f9b1d4..6d75e1c83 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ First go to [Auth0 Dashboard](https://manage.auth0.com/#/applications) and go to https://{YOUR_AUTH0_DOMAIN}/android/{YOUR_APP_PACKAGE_NAME}/callback ``` -Remember to replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name. +Remember to replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name, available in your `app/build.gradle` file as the `applicationId` value. Next, define a placeholder for the Auth0 Domain which is going to be used internally by the library to register an **intent-filter**. Go to your application's `build.gradle` file and add the `manifestPlaceholders` line as shown below: @@ -314,7 +314,7 @@ In your manifest inside your application's tag add the `RedirectActivity` declar @@ -324,9 +324,7 @@ In your manifest inside your application's tag add the `RedirectActivity` declar ``` -If you request a different scheme you must replace the `android:scheme` property. Remember to also replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name. - -Finally, don't forget to add the internet permission. +If you request a different scheme you must replace the `android:scheme` property. Finally, don't forget to add the internet permission. ```xml From 9ad1e0daf70576b15a3c320b579b0e92a0fccfc4 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 3 Jul 2017 13:11:33 -0300 Subject: [PATCH 9/9] add manifest merger notes --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6d75e1c83..0c0bb3ff0 100644 --- a/README.md +++ b/README.md @@ -295,17 +295,21 @@ android { It's a good practice to define reusable resources like `@string/auth0_domain` but you can also hard code the value in the file. -Alternatively, you can declare the `RedirectActivity` in the `AndroidManifest.xml` file with your own **intent-filter** so it overrides the library's default. If you do this then the `manifestPlaceholders` don't need to be set. If you choose to use a [custom scheme](#a-note-about-app-deep-linking) you must define your own intent-filter as explained below. +Alternatively, you can declare the `RedirectActivity` in the `AndroidManifest.xml` file with your own **intent-filter** so it overrides the library's default. If you do this then the `manifestPlaceholders` don't need to be set as long as the activity contains the `tools:node="replace"` like in the snippet below. If you choose to use a [custom scheme](#a-note-about-app-deep-linking) you must define your own intent-filter as explained below. In your manifest inside your application's tag add the `RedirectActivity` declaration: ```xml + + android:name="com.auth0.android.provider.RedirectActivity" + tools:node="replace"> @@ -322,9 +326,10 @@ In your manifest inside your application's tag add the `RedirectActivity` declar + ``` -If you request a different scheme you must replace the `android:scheme` property. Finally, don't forget to add the internet permission. +If you request a different scheme you must replace the `android:scheme` property value. Finally, don't forget to add the internet permission. ```xml