Skip to content
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

[Widp] Implement login with two factor #228

Merged
merged 9 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ android {
val mapboxAccessToken = System.getenv("MAPBOX_ACCESS_TOKEN") ?: defMapboxToken
val bitriseSentryDSN = System.getenv("SENTRY_DSN") ?: ""

buildConfigField("String", "SDK_VERSION", "\"" + "1.10.1-eyeseetea-fork-1" + "\"")
buildConfigField("String", "SDK_VERSION", "\"" + "1.10.1-eyeseetea-fork-2" + "\"")
buildConfigField("String", "MAPBOX_ACCESS_TOKEN", "\"" + mapboxAccessToken + "\"")
buildConfigField("String", "MATOMO_URL", "\"https://usage.analytics.dhis2.org/matomo.php\"")
buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}")
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/org/dhis2/data/server/OpenIdSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class OpenIdSession(
enum class LogOutReason {
OPEN_ID,
DISABLED_ACCOUNT,
UNAUTHORIZED,
}

fun setSessionCallback(
Expand All @@ -44,6 +45,8 @@ class OpenIdSession(
d2.userModule().accountManager().accountDeletionObservable()
.filter { it == AccountDeletionReason.ACCOUNT_DISABLED }
.map { LogOutReason.DISABLED_ACCOUNT },
d2.userModule().accountManager().logOutObservable()
.map { LogOutReason.UNAUTHORIZED },
).defaultSubscribe(
schedulerProvider,
{ sessionCallback(it) },
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/org/dhis2/data/server/UserManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
public interface UserManager {

@NonNull
Observable<User> logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl);
Observable<User> logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl, String twoFactorCode);

@NonNull
Observable<IntentWithRequestCode> logIn(@NonNull OpenIDConnectConfig config);
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/org/dhis2/data/server/UserManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public UserManagerImpl(@NonNull D2 d2, ServerSettingsRepository repository) {

@NonNull
@Override
public Observable<User> logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl) {
return Observable.defer(() -> d2.userModule().logIn(username, password, serverUrl).toObservable());
public Observable<User> logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl, String twoFactorCode ) {
return Observable.defer(() -> d2.userModule().logIn(username, password, serverUrl, twoFactorCode).toObservable());
}

@NonNull
Expand Down
15 changes: 13 additions & 2 deletions app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.dhis2.App
import org.dhis2.bindings.buildInfo
import org.dhis2.R
import org.dhis2.bindings.app
import org.dhis2.bindings.buildInfo
import org.dhis2.bindings.onRightDrawableClicked
import org.dhis2.commons.Constants.ACCOUNT_RECOVERY
import org.dhis2.commons.Constants.EXTRA_DATA
Expand Down Expand Up @@ -146,7 +146,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View {
EXTRA_ACCOUNT_DISABLED,
true,
)

OpenIdSession.LogOutReason.UNAUTHORIZED -> putBoolean(EXTRA_SESSION_EXPIRED, true)
null -> {
// Nothing to do in this case
}
Expand Down Expand Up @@ -212,6 +212,8 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View {

presenter.isDataComplete.observe(this) { this.setLoginVisibility(it) }

presenter.twoFactorCodeVisible.observe(this) { this.setTwoFactorCodeVisibility(it) }

presenter.isTestingEnvironment.observe(
this,
) { testingEnvironment ->
Expand Down Expand Up @@ -250,6 +252,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View {
binding.clearPassButton.setOnClickListener { binding.userPassEdit.text = null }
binding.clearUserNameButton.setOnClickListener { binding.userNameEdit.text = null }
binding.clearUrl.setOnClickListener { binding.serverUrlEdit.text = null }
binding.clearTwoFactoButton.setOnClickListener { binding.userTwoFactorCodeEdit.text = null }

presenter.loginProgressVisible.observe(this) { show ->
showLoginProgress(show, getString(R.string.authenticating))
Expand Down Expand Up @@ -351,6 +354,14 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View {
binding.login.isEnabled = isVisible
}

fun setTwoFactorCodeVisibility(isVisible: Boolean) {
if (isVisible){
binding.twoFactoContainer.visibility = View.VISIBLE
} else {
binding.twoFactoContainer.visibility = View.GONE
}
}

private fun showLoginProgress(showLogin: Boolean, message: String? = null) {
if (showLogin) {
window.setFlags(
Expand Down
40 changes: 28 additions & 12 deletions app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class LoginViewModel(
val serverUrl = MutableLiveData<String>()
val userName = MutableLiveData<String>()
val password = MutableLiveData<String>()
val twoFactorCode = MutableLiveData<String>()
val isDataComplete = MutableLiveData<Boolean>()
val isTestingEnvironment = MutableLiveData<Trio<String, String, String>>()
var testingCredentials: MutableMap<String, TestingCredential>? = null
Expand All @@ -85,6 +86,9 @@ class LoginViewModel(
private val _displayMoreActions = MutableLiveData<Boolean>(true)
val displayMoreActions: LiveData<Boolean> = _displayMoreActions

private val _twoFactorCodeVisible = MutableLiveData(false)
val twoFactorCodeVisible: LiveData<Boolean> = _twoFactorCodeVisible

init {
this.userManager?.let {
disposable.add(
Expand Down Expand Up @@ -117,8 +121,8 @@ class LoginViewModel(
DEFAULT_URL.ifEmpty { view.getDefaultServerProtocol() }
}

if (DEFAULT_URL.isNotEmpty()){
onServerChanged(defaultUrl(),0,0,0)
if (DEFAULT_URL.isNotEmpty()) {
onServerChanged(defaultUrl(), 0, 0, 0)
}

view.setUrl(defaultUrl())
Expand Down Expand Up @@ -227,6 +231,7 @@ class LoginViewModel(
userName.value!!.trim { it <= ' ' },
password.value!!,
serverUrl.value!!,
twoFactorCode.value,
)
.map {
run {
Expand Down Expand Up @@ -367,7 +372,11 @@ class LoginViewModel(
userManager?.d2?.userModule()?.blockingLogOut()
logIn()
} else {
view.renderError(throwable)
if (throwable is D2Error && throwable.errorCode() == D2ErrorCode.INCORRECT_TWO_FACTOR_CODE && _twoFactorCodeVisible.value == false) {
_twoFactorCodeVisible.postValue(true)
} else {
view.renderError(throwable)
}
}
}

Expand All @@ -383,13 +392,13 @@ class LoginViewModel(

fun areSameCredentials(): Boolean {
return (
preferenceProvider.areCredentialsSet() &&
preferenceProvider.areSameCredentials(
serverUrl.value!!,
userName.value!!,
password.value!!,
)
).also { areSameCredentials -> if (!areSameCredentials) saveUserCredentials() }
preferenceProvider.areCredentialsSet() &&
preferenceProvider.areSameCredentials(
serverUrl.value!!,
userName.value!!,
password.value!!,
)
).also { areSameCredentials -> if (!areSameCredentials) saveUserCredentials() }
}

private fun saveUserCredentials() {
Expand Down Expand Up @@ -544,10 +553,17 @@ class LoginViewModel(
}
}

fun onTwoFactorCodeChanged(twoFactorCode: CharSequence, start: Int, before: Int, count: Int) {
if (password.toString() != this.password.value) {
this.twoFactorCode.value = twoFactorCode.toString()
checkData()
}
}

private fun checkData() {
val newValue = !serverUrl.value.isNullOrEmpty() &&
!userName.value.isNullOrEmpty() &&
!password.value.isNullOrEmpty()
!userName.value.isNullOrEmpty() &&
!password.value.isNullOrEmpty()
if (isDataComplete.value == null || isDataComplete.value != newValue) {
isDataComplete.value = newValue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ class ProgramEventDetailActivity :
}
})
.onNoConnectionListener {
val contextView = findViewById<View>(R.id.rootView)
val contextView = findViewById<View>(R.id.toolbar)
Snackbar.make(
contextView,
R.string.sync_offline_check_connection,
Expand Down
57 changes: 56 additions & 1 deletion app/src/main/res/layout/activity_login.xml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,61 @@
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/twoFactoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/passContainer"
android:visibility="gone">


<ImageView
android:id="@+id/twoFactorCodeIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/user_two_factor"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/user_two_factor"
app:srcCompat="@drawable/ic_i_block" />

<ImageButton
android:id="@+id/clearTwoFactoButton"
style="@style/ActionIconNoPadding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="@id/user_two_factor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/user_two_factor"
app:srcCompat="@drawable/ic_close" />

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/user_two_factor"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:hint="@string/two_factor_code_hint"
android:textColorHint="@color/text_black_808"
android:theme="@style/loginInputText"
app:layout_constraintEnd_toStartOf="@id/clearTwoFactoButton"
app:layout_constraintStart_toEndOf="@id/twoFactorCodeIcon"
app:layout_constraintTop_toTopOf="parent">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/user_two_factor_code_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="text"
android:onTextChanged="@{presenter::onTwoFactorCodeChanged}"
android:textColor="@color/text_black_333"
android:textSize="17sp" />

</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>


<ImageButton
android:id="@+id/biometricButton"
Expand All @@ -224,7 +279,7 @@
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/passContainer"
app:layout_constraintTop_toBottomOf="@id/twoFactoContainer"
app:layout_constraintVertical_chainStyle="spread"
app:srcCompat="@drawable/ic_fingerprint"
app:tint="?colorPrimary"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<string name="url_hint">Server url</string>
<string name="user_hint">Username</string>
<string name="password_hint">Password</string>
<string name="two_factor_code_hint">Two factor code</string>
<string name="log_in_button">Log In</string>
<string name="unlock_button">Unlock session</string>
<string name="syncing_configuration">Syncing your Configuration</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,15 @@ class EventProgramFilterSearchHelperTest {
@Test
fun `Should return query by program`() {
eventFilterSearchHelper.getFilteredEventRepository(
Program.builder().uid("programUid").build(), null
)
Program.builder().uid("programUid").build())
verify(filterRepository).eventsByProgram("programUid")
}

@Ignore("Null pointer exception in bitrise")
@Test
fun `Should not apply any filters if not set`() {
eventFilterSearchHelper.getFilteredEventRepository(
Program.builder().uid("programUid").build(), null
)
Program.builder().uid("programUid").build())
verify(filterRepository, times(0)).applyOrgUnitFilter(
any<EventQueryCollectionRepository>(),
any(),
Expand Down Expand Up @@ -114,8 +112,7 @@ class EventProgramFilterSearchHelperTest {
) doReturn mock()

eventFilterSearchHelper.getFilteredEventRepository(
Program.builder().uid("programUid").build(), null
)
Program.builder().uid("programUid").build())

verify(filterRepository, times(1))
.applyOrgUnitFilter(any<EventQueryCollectionRepository>(), any())
Expand All @@ -134,8 +131,7 @@ class EventProgramFilterSearchHelperTest {
fun `Should apply sorting for supported sorting type`() {
filterManager.sortingItem = SortingItem(Filters.PERIOD, SortingStatus.ASC)
eventFilterSearchHelper.getFilteredEventRepository(
Program.builder().uid("programUid").build(), null
)
Program.builder().uid("programUid").build())
verify(filterRepository, times(1)).sortByEventDate(any(), any())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.atMost
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.given
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
Expand Down Expand Up @@ -196,7 +197,7 @@ class LoginViewModelTest {
fun `Should log in successfully and show fabric dialog when user has not been asked before`() {
val mockedUser: User = mock()
whenever(view.initLogin()) doReturn userManager
whenever(userManager.logIn(any(), any(), any())) doReturn Observable.just(mockedUser)
whenever(userManager.logIn(any(), any(), any(), eq(null))) doReturn Observable.just(mockedUser)
instantiateLoginViewModelWithNullUserManager()
loginViewModel.onServerChanged(serverUrl = "serverUrl", 0, 0, 0)
loginViewModel.onUserChanged(userName = "username", 0, 0, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class ProgramRepositoryImplTest {
d2.programModule().programs().uid(any()).blockingGet(),
)doReturnConsecutively mockedPrograms()
whenever(
filterPresenter.filteredEventProgram(any(), any())
filterPresenter.filteredEventProgram(any())
) doReturn mock()
whenever(
filterPresenter.filteredEventProgram(any()).blockingGet(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class D2ErrorUtils(

D2ErrorCode.DATABASE_IMPORT_FAILED -> "Database import failed"
D2ErrorCode.DATABASE_IMPORT_INVALID_FILE -> "Invalid file"
D2ErrorCode.INCORRECT_TWO_FACTOR_CODE -> context.getString(R.string.incorrect_two_factor_code)
}
}

Expand Down
1 change: 1 addition & 0 deletions commons/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -294,5 +294,6 @@
<string name="period_span_default_label" translatable="false">%s - %s</string>
<string name="week_period_span_default_label">Week %d %s to %s</string>
<string name="biweek_period_span_default_label" translatable="false">%d %s - %d %s</string>
<string name="incorrect_two_factor_code">Incorrect two factor code</string>

</resources>
2 changes: 1 addition & 1 deletion dhis2-android-sdk
Submodule dhis2-android-sdk updated 67 files
+6 −6 core/src/androidTest/java/org/hisp/dhis/android/core/LogInCallRealIntegrationShould.java
+7 −7 core/src/androidTest/java/org/hisp/dhis/android/core/MetadataCallRealIntegrationShould.kt
+6 −6 core/src/androidTest/java/org/hisp/dhis/android/core/MultiUserMockIntegrationShould.kt
+6 −6 core/src/androidTest/java/org/hisp/dhis/android/core/MultiUserRealIntegrationShould.java
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/TeisCallRealIntegrationShould.kt
+10 −10 ...sp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt
+1 −1 ...oidTest/java/org/hisp/dhis/android/core/category/internal/CategoryComboEndpointCallRealIntegrationShould.kt
+1 −1 .../androidTest/java/org/hisp/dhis/android/core/category/internal/CategoryEndpointCallRealIntegrationShould.kt
+1 −1 ...idTest/java/org/hisp/dhis/android/core/dataelement/internal/DataElementEndpointCallRealIntegrationShould.kt
+1 −1 .../java/org/hisp/dhis/android/core/dataset/internal/DataSetCompleteRegistrationCallRealIntegrationShould.java
+1 −1 ...ava/org/hisp/dhis/android/core/dataset/internal/DataSetCompleteRegistrationPostCallMockIntegrationShould.kt
+4 −4 ...a/org/hisp/dhis/android/core/dataset/internal/DataSetCompleteRegistrationPostCallRealIntegrationShould.java
+1 −1 ...rc/androidTest/java/org/hisp/dhis/android/core/dataset/internal/DataSetEndpointCallRealIntegrationShould.kt
+1 −1 ...ndroidTest/java/org/hisp/dhis/android/core/datavalue/internal/DataValueEndpointCallRealIntegrationShould.kt
+1 −1 .../androidTest/java/org/hisp/dhis/android/core/datavalue/internal/DataValuePostCallRealIntegrationShould.java
+2 −2 ...droidTest/java/org/hisp/dhis/android/core/domain/aggregated/data/AggregatedDataCallRealIntegrationShould.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/event/internal/EventAPIRealShould.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/event/internal/EventDownloadRealIntegrationShould.java
+2 −2 core/src/androidTest/java/org/hisp/dhis/android/core/event/internal/EventEndpointCallRealIntegrationShould.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/event/internal/EventPostCallRealIntegrationShould.kt
+2 −2 .../androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceCallRealIntegrationShould.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/note/NotePostCallRealIntegrationShould.java
+1 −1 ...rc/androidTest/java/org/hisp/dhis/android/core/program/internal/ProgramEndpointCallRealIntegrationShould.kt
+2 −2 core/src/androidTest/java/org/hisp/dhis/android/core/systeminfo/DHISVersionsManagerRealIntegrationShould.kt
+1 −1 ...org/hisp/dhis/android/core/trackedentity/TrackedEntityAttributeReservedValueManagerRealIntegrationShould.kt
+1 −1 ...droidTest/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityInstanceCallRealIntegrationShould.java
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/api/BreakTheGlassAPIShould.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/api/TrackedEntityInstanceAPIShould.kt
+1 −1 ...droid/core/trackedentity/internal/TrackedEntityAttributeReservedValueEndpointCallRealIntegrationShould.java
+1 −1 ...a/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstancePostCallRealIntegrationShould.java
+1 −1 ...g/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryAndDownloadRealIntegrationShould.kt
+1 −1 ...java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallRealIntegrationShould.kt
+2 −2 core/src/androidTest/java/org/hisp/dhis/android/core/user/LoginErrorHandlingRealIntegrationShould.java
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/LogInCallMockIntegrationShould.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/LogInOfflineCallMockIntegrationShould.kt
+5 −5 core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/LogoutCallRealIntegrationShould.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt
+1 −1 core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/VisualizationEndpointCallShould.kt
+4 −4 core/src/androidTest/java/org/hisp/dhis/android/core/wipe/WipeDBCallRealIntegrationShould.kt
+1 −0 core/src/androidTest/java/org/hisp/dhis/android/localanalytics/tests/BaseLocalAnalyticsTest.kt
+16 −4 .../java/org/hisp/dhis/android/realservertests/apischema/tests/ApiSchemaUpdatesCheckerRealIntegrationShould.kt
+8 −8 core/src/androidTest/java/org/hisp/dhis/android/testapp/user/AccountManagerMockIntegrationShould.kt
+0 −6 core/src/main/assets/migrations/76.sql
+4 −1 core/src/main/assets/migrations/85.sql
+0 −3 core/src/main/assets/migrations/86.sql
+1 −1 core/src/main/assets/migrations/94.sql
+4 −4 core/src/main/assets/migrations/95.sql
+1 −1 core/src/main/assets/snapshots/snapshot.sql
+6 −3 core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/CookieAuthenticatorHelper.kt
+1 −1 core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/ParentAuthenticator.kt
+20 −4 ...src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/PasswordAndCookieAuthenticator.kt
+4 −1 core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/UserIdAuthenticatorHelper.kt
+2 −1 core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java
+7 −1 core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java
+2 −0 core/src/main/java/org/hisp/dhis/android/core/user/AccountManager.kt
+2 −2 core/src/main/java/org/hisp/dhis/android/core/user/UserModule.kt
+4 −1 core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt
+53 −0 core/src/main/java/org/hisp/dhis/android/core/user/internal/ConnectLogoutHandler.kt
+73 −11 core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt
+7 −0 core/src/main/java/org/hisp/dhis/android/core/user/internal/LoginPayload.kt
+3 −0 core/src/main/java/org/hisp/dhis/android/core/user/internal/LoginResponse.kt
+4 −4 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleImpl.kt
+7 −0 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserService.kt
+4 −0 core/src/sharedTest/resources/user/auth_login_success.json
+65 −12 core/src/test/java/org/hisp/dhis/android/core/arch/api/authentication/internal/ParentAuthenticatorShould.java
+6 −5 ...c/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt
+74 −23 core/src/test/java/org/hisp/dhis/android/core/user/internal/LogInCallUnitShould.kt