diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04bede97bc..ed5c562b93 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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}") diff --git a/app/src/main/java/org/dhis2/data/server/OpenIdSession.kt b/app/src/main/java/org/dhis2/data/server/OpenIdSession.kt index 8815b95e51..25170ecb16 100644 --- a/app/src/main/java/org/dhis2/data/server/OpenIdSession.kt +++ b/app/src/main/java/org/dhis2/data/server/OpenIdSession.kt @@ -25,6 +25,7 @@ class OpenIdSession( enum class LogOutReason { OPEN_ID, DISABLED_ACCOUNT, + UNAUTHORIZED, } fun setSessionCallback( @@ -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) }, diff --git a/app/src/main/java/org/dhis2/data/server/UserManager.java b/app/src/main/java/org/dhis2/data/server/UserManager.java index a166cfc832..009f10789a 100644 --- a/app/src/main/java/org/dhis2/data/server/UserManager.java +++ b/app/src/main/java/org/dhis2/data/server/UserManager.java @@ -18,7 +18,7 @@ public interface UserManager { @NonNull - Observable logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl); + Observable logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl, String twoFactorCode); @NonNull Observable logIn(@NonNull OpenIDConnectConfig config); diff --git a/app/src/main/java/org/dhis2/data/server/UserManagerImpl.java b/app/src/main/java/org/dhis2/data/server/UserManagerImpl.java index 1bc8fa5851..3bb03b9adb 100644 --- a/app/src/main/java/org/dhis2/data/server/UserManagerImpl.java +++ b/app/src/main/java/org/dhis2/data/server/UserManagerImpl.java @@ -27,8 +27,8 @@ public UserManagerImpl(@NonNull D2 d2, ServerSettingsRepository repository) { @NonNull @Override - public Observable logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl) { - return Observable.defer(() -> d2.userModule().logIn(username, password, serverUrl).toObservable()); + public Observable logIn(@NonNull String username, @NonNull String password, @NonNull String serverUrl, String twoFactorCode ) { + return Observable.defer(() -> d2.userModule().logIn(username, password, serverUrl, twoFactorCode).toObservable()); } @NonNull diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt index 94a3d2d02f..784e26b0c2 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -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 @@ -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 } @@ -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 -> @@ -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)) @@ -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( diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt index eb8564beec..716093b75e 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -73,6 +73,7 @@ class LoginViewModel( val serverUrl = MutableLiveData() val userName = MutableLiveData() val password = MutableLiveData() + val twoFactorCode = MutableLiveData() val isDataComplete = MutableLiveData() val isTestingEnvironment = MutableLiveData>() var testingCredentials: MutableMap? = null @@ -85,6 +86,9 @@ class LoginViewModel( private val _displayMoreActions = MutableLiveData(true) val displayMoreActions: LiveData = _displayMoreActions + private val _twoFactorCodeVisible = MutableLiveData(false) + val twoFactorCodeVisible: LiveData = _twoFactorCodeVisible + init { this.userManager?.let { disposable.add( @@ -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()) @@ -227,6 +231,7 @@ class LoginViewModel( userName.value!!.trim { it <= ' ' }, password.value!!, serverUrl.value!!, + twoFactorCode.value, ) .map { run { @@ -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) + } } } @@ -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() { @@ -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 } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt index b8c292896e..9f6120a1cc 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt @@ -452,7 +452,7 @@ class ProgramEventDetailActivity : } }) .onNoConnectionListener { - val contextView = findViewById(R.id.rootView) + val contextView = findViewById(R.id.toolbar) Snackbar.make( contextView, R.string.sync_offline_check_connection, diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index d40c34c775..680e23dd39 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -211,6 +211,61 @@ + + + + + + + + + + + + + + Server url Username Password + Two factor code Log In Unlock session Syncing your Configuration diff --git a/app/src/test/java/org/dhis2/data/filter/EventProgramFilterSearchHelperTest.kt b/app/src/test/java/org/dhis2/data/filter/EventProgramFilterSearchHelperTest.kt index f0c4b8ed32..c19f6b7930 100644 --- a/app/src/test/java/org/dhis2/data/filter/EventProgramFilterSearchHelperTest.kt +++ b/app/src/test/java/org/dhis2/data/filter/EventProgramFilterSearchHelperTest.kt @@ -62,8 +62,7 @@ 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") } @@ -71,8 +70,7 @@ class EventProgramFilterSearchHelperTest { @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(), any(), @@ -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(), any()) @@ -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()) } } diff --git a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt index 0bf3f0fbf6..27329d1550 100644 --- a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt @@ -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 @@ -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) diff --git a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt index 57f5508a71..9c17c6fa56 100644 --- a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt @@ -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(), diff --git a/commons/src/main/java/org/dhis2/commons/resources/D2ErrorUtils.kt b/commons/src/main/java/org/dhis2/commons/resources/D2ErrorUtils.kt index 1666d3d79d..b43eb7f61b 100644 --- a/commons/src/main/java/org/dhis2/commons/resources/D2ErrorUtils.kt +++ b/commons/src/main/java/org/dhis2/commons/resources/D2ErrorUtils.kt @@ -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) } } diff --git a/commons/src/main/res/values/strings.xml b/commons/src/main/res/values/strings.xml index cc98608cf1..fbd4244de2 100644 --- a/commons/src/main/res/values/strings.xml +++ b/commons/src/main/res/values/strings.xml @@ -294,5 +294,6 @@ %s - %s Week %d %s to %s %d %s - %d %s + Incorrect two factor code diff --git a/dhis2-android-sdk b/dhis2-android-sdk index 30fb10f3ec..1acf258ff9 160000 --- a/dhis2-android-sdk +++ b/dhis2-android-sdk @@ -1 +1 @@ -Subproject commit 30fb10f3ec4103351a3134e17baab9815a6350d0 +Subproject commit 1acf258ff95e4f8cbfd64067df254abe9f603542