-
-
Notifications
You must be signed in to change notification settings - Fork 48
CCCT-2439 Launch Connect Apps Without Showing The Login Screen #3753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 19 commits
f34655d
2d3bd52
ad7fdc2
20f959f
9e3aa2c
7cafc14
6a9a96f
5393194
bc7ddc6
2e373a6
5af435e
4f3657a
b847f7f
10082ee
ff83fa1
1568ebf
18c9126
aa9285c
502c326
b331085
8153e70
c663ef9
126daaf
72aa6df
cc723d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,6 @@ | |
|
|
||
| import org.commcare.dalvik.R; | ||
| import org.commcare.login.AppSeater; | ||
| import org.commcare.login.SeatResult; | ||
| import org.javarosa.core.services.locale.Localization; | ||
|
|
||
| /** | ||
|
|
@@ -35,7 +34,7 @@ protected void onCreate(Bundle savedInstanceState) { | |
| ); | ||
| } | ||
|
|
||
| private void finishWithResult(SeatResult result) { | ||
| private void finishWithResult() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: This function no longer takes a result so the name is misleading. Maybe rename it to setResultAndFinish instead?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good eye!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| setResult(RESULT_OK, new Intent(getIntent())); | ||
| finish(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| package org.commcare.connect | ||
|
|
||
| import android.content.Context | ||
| import androidx.lifecycle.LifecycleOwner | ||
| import androidx.lifecycle.lifecycleScope | ||
| import kotlinx.coroutines.Job | ||
| import kotlinx.coroutines.launch | ||
| import org.commcare.CommCareApplication | ||
| import org.commcare.activities.DataPullController.DataPullMode | ||
| import org.commcare.activities.LoginMode | ||
| import org.commcare.android.database.app.models.UserKeyRecord | ||
| import org.commcare.connect.database.ConnectUserDatabaseUtil | ||
| import org.commcare.google.services.analytics.FirebaseAnalyticsUtil | ||
| import org.commcare.login.AppSeater | ||
| import org.commcare.login.AuthSource | ||
| import org.commcare.login.LoginController | ||
| import org.commcare.login.LoginError | ||
| import org.commcare.login.LoginProgressListener | ||
| import org.commcare.login.LoginRequest | ||
| import org.commcare.login.LoginResult | ||
| import org.commcare.login.SeatResult | ||
| import java.util.Locale | ||
|
|
||
| /** | ||
| * Terminal result of a Connect launch. All non-token, non-seat errors fold into [Retryable]; | ||
| * since this path only does PersonalID password logins, there is no SyncFailed or Demo outcome. | ||
| */ | ||
| sealed class LaunchOutcome { | ||
| object Launched : LaunchOutcome() | ||
|
|
||
| object TokenDenied : LaunchOutcome() | ||
|
|
||
| object AppSeatFailed : LaunchOutcome() | ||
|
|
||
| object CredentialResolutionFailed : LaunchOutcome() | ||
|
|
||
| data class Retryable( | ||
| val error: LoginError, | ||
| ) : LaunchOutcome() | ||
| } | ||
|
|
||
| /** | ||
| * Seats and signs into a Connect app with the worker's PersonalID credentials without showing | ||
| * [org.commcare.activities.LoginActivity]. | ||
| */ | ||
| class ConnectAppLauncher internal constructor( | ||
| private val seatApp: suspend (String, LoginProgressListener) -> SeatResult, | ||
| private val performLogin: suspend (Context, LoginRequest, LoginProgressListener) -> LoginResult, | ||
| private val connectUsername: (Context) -> String?, | ||
| ) { | ||
| constructor() : this( | ||
| seatApp = { appId, listener -> AppSeater().seatIfNeeded(appId, listener) }, | ||
| performLogin = { context, request, listener -> LoginController(context).performLogin(request, listener) }, | ||
| connectUsername = { context -> ConnectUserDatabaseUtil.getUser(context)?.userId }, | ||
| ) | ||
|
|
||
| fun interface OutcomeCallback { | ||
| fun onOutcome(outcome: LaunchOutcome) | ||
| } | ||
|
|
||
| /** Fire-and-forget [awaitOutcome], scoped to [lifecycleOwner] so it cancels with the view. */ | ||
| fun start( | ||
| lifecycleOwner: LifecycleOwner, | ||
| context: Context, | ||
| appId: String, | ||
| isLearning: Boolean, | ||
| listener: LoginProgressListener, | ||
| callback: OutcomeCallback, | ||
| ): Job = | ||
| lifecycleOwner.lifecycleScope.launch { | ||
| callback.onOutcome(awaitOutcome(context, appId, isLearning, listener)) | ||
| } | ||
|
|
||
| suspend fun awaitOutcome( | ||
| context: Context, | ||
| appId: String, | ||
| isLearning: Boolean, | ||
| listener: LoginProgressListener, | ||
| ): LaunchOutcome { | ||
| CommCareApplication.instance().closeUserSession() | ||
| FirebaseAnalyticsUtil.reportCccAppLaunch( | ||
| if (isLearning) "Learn" else "Deliver", | ||
| appId, | ||
| ) | ||
|
|
||
| if (seatApp(appId, listener) is SeatResult.Failed) { | ||
| return LaunchOutcome.AppSeatFailed | ||
| } | ||
|
|
||
| val username = | ||
| connectUsername(context)?.trim()?.lowercase(Locale.ROOT) | ||
| if (username.isNullOrEmpty()) { | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return LaunchOutcome.CredentialResolutionFailed | ||
| } | ||
|
|
||
| val request = | ||
| LoginRequest( | ||
| appId = appId, | ||
| username = username, | ||
| passwordOrPin = "", | ||
| credentialType = LoginMode.PASSWORD, | ||
| authSource = AuthSource.PersonalId, | ||
| restoreSession = false, | ||
| triggerMultipleUsersWarning = hasMultipleMatchingUsers(username), | ||
| blockRemoteKeyManagement = false, | ||
| dataPullMode = DataPullMode.NORMAL, | ||
| ) | ||
|
|
||
| return when (val result = performLogin(context, request, listener)) { | ||
| is LoginResult.Success -> LaunchOutcome.Launched | ||
| is LoginResult.Failed -> | ||
| when (result.error) { | ||
| is LoginError.TokenDenied -> LaunchOutcome.TokenDenied | ||
| else -> LaunchOutcome.Retryable(result.error) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun hasMultipleMatchingUsers(username: String): Boolean { | ||
| var count = 0 | ||
| for (record in CommCareApplication.instance().getAppStorage(UserKeyRecord::class.java)) { | ||
| if (record.username == username && ++count > 1) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package org.commcare.connect | ||
|
|
||
| /** Actions a caller runs for a [LaunchOutcome]; a seam so the mapping is testable without a fragment. */ | ||
| interface LaunchActions { | ||
| fun dismissProgress() | ||
|
|
||
| fun launchHome() | ||
|
|
||
| fun handleTokenDenied() | ||
|
|
||
| fun recoverFromSeatFailure() | ||
|
|
||
| fun fallBackToLegacyLaunch() | ||
|
|
||
| fun reportFailure(reason: String) | ||
| } | ||
|
|
||
| /** Maps each [LaunchOutcome] to its [LaunchActions] calls, always dismissing progress first. */ | ||
| object LaunchOutcomeRouter { | ||
| fun dispatch( | ||
| outcome: LaunchOutcome, | ||
| actions: LaunchActions, | ||
| ) { | ||
| actions.dismissProgress() | ||
| when (outcome) { | ||
| LaunchOutcome.Launched -> actions.launchHome() | ||
| LaunchOutcome.TokenDenied -> actions.handleTokenDenied() | ||
| LaunchOutcome.AppSeatFailed -> actions.recoverFromSeatFailure() | ||
| LaunchOutcome.CredentialResolutionFailed -> { | ||
| actions.reportFailure("CredentialResolutionFailed") | ||
| actions.fallBackToLegacyLaunch() | ||
| } | ||
| is LaunchOutcome.Retryable -> { | ||
| actions.reportFailure(outcome.error::class.java.simpleName) | ||
| actions.fallBackToLegacyLaunch() | ||
| } | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.