Skip to content

Commit 4cc2eda

Browse files
authored
Merge pull request #3496 from aws-amplify/fix/auth/android-hosted-flakiness
fix(auth): Android Hosted UI flakiness
2 parents 0cc7687 + c4813b5 commit 4cc2eda

File tree

1 file changed

+56
-23
lines changed

1 file changed

+56
-23
lines changed

packages/auth/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AmplifyAuthCognitoPlugin.kt

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
2020
import io.flutter.embedding.engine.plugins.activity.ActivityAware
2121
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
2222
import io.flutter.plugin.common.PluginRegistry
23-
import kotlinx.coroutines.CoroutineName
24-
import kotlinx.coroutines.CoroutineScope
25-
import kotlinx.coroutines.Dispatchers
26-
import kotlinx.coroutines.delay
27-
import kotlinx.coroutines.launch
28-
import kotlinx.coroutines.plus
2923
import java.util.Locale
3024

3125
open class AmplifyAuthCognitoPlugin :
@@ -43,9 +37,9 @@ open class AmplifyAuthCognitoPlugin :
4337
const val CUSTOM_TAB_REQUEST_CODE = 8888
4438

4539
/**
46-
* The scope in which to spawn tasks which should not be awaited from the main thread.
40+
* An intent extra used to signal the cancellation of a Hosted UI sign-in/sign-out.
4741
*/
48-
val scope = CoroutineScope(Dispatchers.IO) + CoroutineName("amplify_flutter.AuthCognito")
42+
const val CUSTOM_TAB_CANCEL_EXTRA = "com.amazonaws.amplify.auth.hosted_ui.cancel"
4943
}
5044

5145
/**
@@ -194,6 +188,7 @@ open class AmplifyAuthCognitoPlugin :
194188
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
195189
packageManager.getPackageInfo(packageName, PackageInfoFlags.of(0)).versionName
196190
} else {
191+
@Suppress("DEPRECATION")
197192
packageManager.getPackageInfo(packageName, 0).versionName
198193
}
199194
} catch (e: PackageManager.NameNotFoundException) {
@@ -310,6 +305,8 @@ open class AmplifyAuthCognitoPlugin :
310305
/**
311306
* The application ID of the installed package which can handle custom tabs,
312307
* typically a browser like Chrome.
308+
*
309+
* Adapted from: https://github.com/GoogleChrome/custom-tabs-client/blob/f55501961a211a92eacbe3c2f15d7c58c19c8ef9/shared/src/main/java/org/chromium/customtabsclient/shared/CustomTabsHelper.java#L64
313310
*/
314311
private val browserPackageName: String? by lazy {
315312
val packageManager = mainActivity!!.packageManager
@@ -319,10 +316,18 @@ open class AmplifyAuthCognitoPlugin :
319316
addCategory(Intent.CATEGORY_BROWSABLE)
320317
data = Uri.fromParts("https", "", null)
321318
}
322-
val defaultViewHandlerInfo = packageManager.resolveActivity(
323-
activityIntent,
324-
MATCH_DEFAULT_ONLY
325-
)
319+
val defaultViewHandlerInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
320+
packageManager.resolveActivity(
321+
activityIntent,
322+
PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY.toLong())
323+
)
324+
} else {
325+
@Suppress("DEPRECATION")
326+
packageManager.resolveActivity(
327+
activityIntent,
328+
MATCH_DEFAULT_ONLY
329+
)
330+
}
326331
Log.d(TAG, "[browserPackageName] Resolved activity info: $defaultViewHandlerInfo")
327332
var defaultViewHandlerPackageName: String? = null
328333
if (defaultViewHandlerInfo != null) {
@@ -331,14 +336,28 @@ open class AmplifyAuthCognitoPlugin :
331336
Log.d(TAG, "[browserPackageName] Resolved default package: $defaultViewHandlerPackageName")
332337

333338
// Get all apps that can handle VIEW intents.
334-
val resolvedActivityList = packageManager.queryIntentActivities(activityIntent, MATCH_ALL)
339+
val resolvedActivityList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
340+
packageManager.queryIntentActivities(
341+
activityIntent,
342+
PackageManager.ResolveInfoFlags.of(MATCH_ALL.toLong())
343+
)
344+
} else {
345+
@Suppress("DEPRECATION")
346+
packageManager.queryIntentActivities(activityIntent, MATCH_ALL)
347+
}
335348
val packagesSupportingCustomTabs = mutableListOf<String>()
336349
for (info in resolvedActivityList) {
337350
val serviceIntent = Intent()
338351
serviceIntent.action = ACTION_CUSTOM_TABS_CONNECTION
339352
serviceIntent.`package` = info.activityInfo.packageName
340353
// Check if the package also resolves the Custom Tabs service.
341-
if (packageManager.resolveService(serviceIntent, 0) != null) {
354+
val resolvedService = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
355+
packageManager.resolveService(serviceIntent, PackageManager.ResolveInfoFlags.of(0))
356+
} else {
357+
@Suppress("DEPRECATION")
358+
packageManager.resolveService(serviceIntent, 0)
359+
}
360+
if (resolvedService != null) {
342361
packagesSupportingCustomTabs.add(info.activityInfo.packageName)
343362
}
344363
}
@@ -461,6 +480,12 @@ open class AmplifyAuthCognitoPlugin :
461480
}
462481
true
463482
}
483+
} else if (intent.hasExtra(CUSTOM_TAB_CANCEL_EXTRA)) {
484+
// Intents originating with this extra are meant to close the loop on sign-in/out events.
485+
// See the notes on [onActivityResult] for more information.
486+
Log.d(TAG, "[onNewIntent] Cancelling current operation")
487+
cancelCurrentOperation()
488+
return true
464489
}
465490
Log.d(TAG, "[onNewIntent] Not handling intent")
466491
return false
@@ -469,20 +494,28 @@ open class AmplifyAuthCognitoPlugin :
469494
/**
470495
* Called when the custom tab activity completes, either by cancellation or success.
471496
*
472-
* There is no way to distinguish whether it is a success or cancellation, but since this is
473-
* called just before or after [onNewIntent], we wait a second for both to be called.
474-
* Afterwards, we are guaranteed that if [signInResult]/[signOutResult] is not `null`, then it
475-
* is a cancellation since otherwise, [handleSignInResult] would have set it to `null`.
497+
* There is no way to distinguish whether it is a success or cancellation, and our logic for
498+
* handling the success case is centered in [onNewIntent] which is called just before or after
499+
* this method in the success case and not at all in the error case.
500+
*
501+
* In the cancellation case, [onNewIntent] is not called, only this method, which means that
502+
* we either need to wait for a couple seconds to see if [onNewIntent] is called or trigger an
503+
* intent of our own which will be intercepted by [onNewIntent]. The former is flaky and found to
504+
* be unreliable, but the latter is robust.
505+
*
506+
* By the time we reach this method, a new intent will have been initiated in the success case.
507+
* By creating a new one, we are guaranteed it will be handled after the success intent, in which
508+
* case it's a no-op. In the error case, the intent we create will trigger the cancellation of
509+
* the pending sign-in or sign-out request. Either way, the request is completed.
476510
*/
477511
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
478512
Log.d(TAG, "[onActivityResult] Got result: requestCode=$requestCode, resultCode=$resultCode, intent=$intent")
479513
if (requestCode == CUSTOM_TAB_REQUEST_CODE) {
480-
scope.launch {
481-
delay(1000L)
482-
launch(Dispatchers.Main) {
483-
cancelCurrentOperation()
484-
}
514+
val cancelIntent = Intent(applicationContext!!, mainActivity!!.javaClass).apply {
515+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
516+
putExtra(CUSTOM_TAB_CANCEL_EXTRA, true)
485517
}
518+
applicationContext!!.startActivity(cancelIntent)
486519
return true
487520
}
488521
return false

0 commit comments

Comments
 (0)