From c58a9b8f8743b5efee4d1d5d49aea7a7b58add53 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Mon, 20 Sep 2021 09:56:38 +0200 Subject: [PATCH 01/27] Changed pattern classes to presentation.ui.security package --- .../security/OCSettingsPatternLockTest.kt | 12 ++++---- .../security/SettingsSecurityFragmentTest.kt | 28 +++++++++---------- owncloudApp/src/main/AndroidManifest.xml | 2 +- .../main/java/com/owncloud/android/MainApp.kt | 10 +++---- .../authentication/BiometricManager.java | 1 + .../ui/security/PatternActivity.java} | 4 +-- .../ui/security}/PatternManager.java | 13 ++++----- .../fragments/SettingsSecurityFragment.kt | 10 +++---- .../settings/SettingsSecurityViewModel.kt | 14 +++++----- .../providers/DocumentsStorageProvider.kt | 4 +-- .../ui/activity/BiometricActivity.java | 2 +- .../ui/activity/ManageSpaceActivity.java | 9 +++--- .../settings/SettingsSecurityViewModelTest.kt | 20 ++++++------- 13 files changed, 65 insertions(+), 64 deletions(-) rename owncloudApp/src/main/java/com/owncloud/android/{ui/activity/PatternLockActivity.java => presentation/ui/security/PatternActivity.java} (99%) rename owncloudApp/src/main/java/com/owncloud/android/{authentication => presentation/ui/security}/PatternManager.java (90%) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/OCSettingsPatternLockTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/OCSettingsPatternLockTest.kt index 46f20f6f264..e497c7439bc 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/OCSettingsPatternLockTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/OCSettingsPatternLockTest.kt @@ -29,7 +29,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule import com.owncloud.android.R -import com.owncloud.android.ui.activity.PatternLockActivity +import com.owncloud.android.presentation.ui.security.PatternActivity import org.junit.After import org.junit.Rule import org.junit.Test @@ -38,7 +38,7 @@ class OCSettingsPatternLockTest { @Rule @JvmField - val activityRule = ActivityTestRule(PatternLockActivity::class.java, true, false) + val activityRule = ActivityTestRule(PatternActivity::class.java, true, false) private val intent = Intent() private val context = InstrumentationRegistry.getInstrumentation().targetContext @@ -53,7 +53,7 @@ class OCSettingsPatternLockTest { @Test fun patternLockView() { //Open Activity in pattern creation mode - openPatternActivity(PatternLockActivity.ACTION_REQUEST_WITH_RESULT) + openPatternActivity(PatternActivity.ACTION_REQUEST_WITH_RESULT) onView(withText(R.string.pattern_configure_pattern)).check(matches(isDisplayed())) onView(withText(R.string.pattern_configure_your_pattern_explanation)).check(matches(isDisplayed())) @@ -67,7 +67,7 @@ class OCSettingsPatternLockTest { storePattern() //Open Activity in pattern deletion mode - openPatternActivity(PatternLockActivity.ACTION_CHECK_WITH_RESULT) + openPatternActivity(PatternActivity.ACTION_CHECK_WITH_RESULT) onView(withText(R.string.pattern_remove_pattern)).check(matches(isDisplayed())) onView(withText(R.string.pattern_no_longer_required)).check(matches(isDisplayed())) @@ -75,8 +75,8 @@ class OCSettingsPatternLockTest { private fun storePattern() { val appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit() - appPrefs.putString(PatternLockActivity.KEY_PATTERN, patternToSave) - appPrefs.putBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, true) + appPrefs.putString(PatternActivity.KEY_PATTERN, patternToSave) + appPrefs.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) appPrefs.apply() } diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt index 36092c68653..c2cff829d93 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt @@ -47,7 +47,7 @@ import com.owncloud.android.presentation.ui.security.PassCodeActivity import com.owncloud.android.presentation.viewmodels.security.PassCodeViewModel import com.owncloud.android.testutil.security.OC_PASSCODE_4_DIGITS import com.owncloud.android.testutil.security.OC_PATTERN -import com.owncloud.android.ui.activity.PatternLockActivity +import com.owncloud.android.presentation.ui.security.PatternActivity import com.owncloud.android.utils.matchers.verifyPreference import com.owncloud.android.utils.mockIntent import io.mockk.every @@ -126,7 +126,7 @@ class SettingsSecurityFragmentTest { fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud) fragmentScenario.onFragment { fragment -> prefPasscode = fragment.findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE)!! - prefPattern = fragment.findPreference(PatternLockActivity.PREFERENCE_SET_PATTERN)!! + prefPattern = fragment.findPreference(PatternActivity.PREFERENCE_SET_PATTERN)!! prefBiometric = fragment.findPreference(BiometricActivity.PREFERENCE_SET_BIOMETRIC) prefLockApplication = fragment.findPreference(PREFERENCE_LOCK_TIMEOUT)!! prefTouchesWithOtherVisibleWindows = @@ -144,7 +144,7 @@ class SettingsSecurityFragmentTest { assertFalse(prefPasscode.isChecked) prefPattern.verifyPreference( - keyPref = PatternLockActivity.PREFERENCE_SET_PATTERN, + keyPref = PatternActivity.PREFERENCE_SET_PATTERN, titlePref = context.getString(R.string.prefs_pattern), visible = true, enabled = true @@ -213,7 +213,7 @@ class SettingsSecurityFragmentTest { launchTest() onView(withText(R.string.prefs_pattern)).perform(click()) - intended(hasComponent(PatternLockActivity::class.java.name)) + intended(hasComponent(PatternActivity::class.java.name)) } @Test @@ -237,8 +237,8 @@ class SettingsSecurityFragmentTest { launchTest() mockIntent( - extras = Pair(PatternLockActivity.KEY_PATTERN, OC_PATTERN), - action = PatternLockActivity.ACTION_REQUEST_WITH_RESULT + extras = Pair(PatternActivity.KEY_PATTERN, OC_PATTERN), + action = PatternActivity.ACTION_REQUEST_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) assertTrue(prefPattern.isChecked) @@ -252,8 +252,8 @@ class SettingsSecurityFragmentTest { launchTest() mockIntent( - extras = Pair(PatternLockActivity.KEY_PATTERN, OC_PATTERN), - action = PatternLockActivity.ACTION_REQUEST_WITH_RESULT + extras = Pair(PatternActivity.KEY_PATTERN, OC_PATTERN), + action = PatternActivity.ACTION_REQUEST_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) assertFalse(prefPattern.isChecked) @@ -328,8 +328,8 @@ class SettingsSecurityFragmentTest { firstEnablePattern() mockIntent( - extras = Pair(PatternLockActivity.KEY_CHECK_RESULT, true), - action = PatternLockActivity.ACTION_CHECK_WITH_RESULT + extras = Pair(PatternActivity.KEY_CHECK_RESULT, true), + action = PatternActivity.ACTION_CHECK_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) assertFalse(prefPattern.isChecked) @@ -347,8 +347,8 @@ class SettingsSecurityFragmentTest { firstEnablePattern() mockIntent( - extras = Pair(PatternLockActivity.KEY_CHECK_RESULT, true), - action = PatternLockActivity.ACTION_CHECK_WITH_RESULT + extras = Pair(PatternActivity.KEY_CHECK_RESULT, true), + action = PatternActivity.ACTION_CHECK_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) assertTrue(prefPattern.isChecked) @@ -454,8 +454,8 @@ class SettingsSecurityFragmentTest { every { securityViewModel.handleEnablePattern(any()) } returns UIResult.Success() mockIntent( - extras = Pair(PatternLockActivity.KEY_PATTERN, OC_PATTERN), - action = PatternLockActivity.ACTION_REQUEST_WITH_RESULT + extras = Pair(PatternActivity.KEY_PATTERN, OC_PATTERN), + action = PatternActivity.ACTION_REQUEST_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) } diff --git a/owncloudApp/src/main/AndroidManifest.xml b/owncloudApp/src/main/AndroidManifest.xml index 87490a89d67..4d7c4beffa8 100644 --- a/owncloudApp/src/main/AndroidManifest.xml +++ b/owncloudApp/src/main/AndroidManifest.xml @@ -237,7 +237,7 @@ android:name=".ui.preview.PreviewVideoActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:theme="@style/Theme.ownCloud.Fullscreen" /> - + . */ -package com.owncloud.android.ui.activity; +package com.owncloud.android.presentation.ui.security; import android.content.Intent; import android.content.SharedPreferences; @@ -49,7 +49,7 @@ import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.PREFERENCE_LAST_UNLOCK_TIMESTAMP; -public class PatternLockActivity extends AppCompatActivity { +public class PatternActivity extends AppCompatActivity { public final static String PREFERENCE_SET_PATTERN = "set_pattern"; public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT"; diff --git a/owncloudApp/src/main/java/com/owncloud/android/authentication/PatternManager.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.java similarity index 90% rename from owncloudApp/src/main/java/com/owncloud/android/authentication/PatternManager.java rename to owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.java index 25bee1747c1..283a11a49fc 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/authentication/PatternManager.java +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.java @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -package com.owncloud.android.authentication; +package com.owncloud.android.presentation.ui.security; import android.app.Activity; import android.content.Context; @@ -29,10 +29,9 @@ import android.os.PowerManager; import com.owncloud.android.MainApp; +import com.owncloud.android.authentication.BiometricManager; import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; -import com.owncloud.android.presentation.ui.security.LockTimeout; -import com.owncloud.android.ui.activity.PatternLockActivity; import java.util.HashSet; import java.util.Set; @@ -49,7 +48,7 @@ public class PatternManager { static { sExemptOfPatternActivites = new HashSet<>(); - sExemptOfPatternActivites.add(PatternLockActivity.class); + sExemptOfPatternActivites.add(PatternActivity.class); } private static PatternManager mPatternManagerInstance = null; @@ -100,12 +99,12 @@ private boolean patternShouldBeRequested() { } public boolean isPatternEnabled() { - return preferencesProvider.getBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false); + return preferencesProvider.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false); } private void askUserForPattern(Activity activity) { - Intent i = new Intent(MainApp.Companion.getAppContext(), PatternLockActivity.class); - i.setAction(PatternLockActivity.ACTION_CHECK); + Intent i = new Intent(MainApp.Companion.getAppContext(), PatternActivity.class); + i.setAction(PatternActivity.ACTION_CHECK); i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); activity.startActivity(i); } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt index d6e6fd3a796..9d93ed3236a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt @@ -40,7 +40,7 @@ import com.owncloud.android.presentation.ui.security.LockTimeout import com.owncloud.android.presentation.viewmodels.settings.SettingsSecurityViewModel import com.owncloud.android.ui.activity.BiometricActivity import com.owncloud.android.presentation.ui.security.PassCodeActivity -import com.owncloud.android.ui.activity.PatternLockActivity +import com.owncloud.android.presentation.ui.security.PatternActivity import org.koin.androidx.viewmodel.ext.android.viewModel class SettingsSecurityFragment : PreferenceFragmentCompat() { @@ -114,7 +114,7 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() { screenSecurity = findPreference(SCREEN_SECURITY) prefPasscode = findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE) - prefPattern = findPreference(PatternLockActivity.PREFERENCE_SET_PATTERN) + prefPattern = findPreference(PatternActivity.PREFERENCE_SET_PATTERN) prefBiometric = findPreference(BiometricActivity.PREFERENCE_SET_BIOMETRIC) prefLockApplication = findPreference(PREFERENCE_LOCK_TIMEOUT)?.apply { entries = listOf( @@ -154,12 +154,12 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() { if (securityViewModel.isPasscodeSet()) { showMessageInSnackbar(getString(R.string.passcode_already_set)) } else { - val intent = Intent(activity, PatternLockActivity::class.java) + val intent = Intent(activity, PatternActivity::class.java) if (newValue as Boolean) { - intent.action = PatternLockActivity.ACTION_REQUEST_WITH_RESULT + intent.action = PatternActivity.ACTION_REQUEST_WITH_RESULT enablePatternLauncher.launch(intent) } else { - intent.action = PatternLockActivity.ACTION_CHECK_WITH_RESULT + intent.action = PatternActivity.ACTION_CHECK_WITH_RESULT disablePatternLauncher.launch(intent) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt index 0a4b807e67a..6314e74f423 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt @@ -26,27 +26,27 @@ import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvid import com.owncloud.android.presentation.UIResult import com.owncloud.android.presentation.ui.settings.fragments.SettingsSecurityFragment import com.owncloud.android.presentation.ui.security.PassCodeActivity -import com.owncloud.android.ui.activity.PatternLockActivity +import com.owncloud.android.presentation.ui.security.PatternActivity class SettingsSecurityViewModel( private val preferencesProvider: SharedPreferencesProvider ) : ViewModel() { - fun isPatternSet() = preferencesProvider.getBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false) + fun isPatternSet() = preferencesProvider.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) fun isPasscodeSet() = preferencesProvider.getBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) fun handleEnablePattern(data: Intent?): UIResult { - val pattern = data?.getStringExtra(PatternLockActivity.KEY_PATTERN) ?: return UIResult.Error() - preferencesProvider.putString(PatternLockActivity.KEY_PATTERN, pattern) - preferencesProvider.putBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, true) + val pattern = data?.getStringExtra(PatternActivity.KEY_PATTERN) ?: return UIResult.Error() + preferencesProvider.putString(PatternActivity.KEY_PATTERN, pattern) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) return UIResult.Success() } fun handleDisablePattern(data: Intent?): UIResult { - data?.getBooleanExtra(PatternLockActivity.KEY_CHECK_RESULT, false).takeIf { it == true } + data?.getBooleanExtra(PatternActivity.KEY_CHECK_RESULT, false).takeIf { it == true } ?: return UIResult.Error() - preferencesProvider.putBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) return UIResult.Success() } diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt index f81e80cdccc..e070d3a874c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt @@ -57,7 +57,7 @@ import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.providers.cursors.FileCursor import com.owncloud.android.providers.cursors.RootCursor import com.owncloud.android.presentation.ui.security.PassCodeActivity -import com.owncloud.android.ui.activity.PatternLockActivity +import com.owncloud.android.presentation.ui.security.PatternActivity import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.NotificationUtils import timber.log.Timber @@ -231,7 +231,7 @@ class DocumentsStorageProvider : DocumentsProvider() { // If OwnCloud is protected with passcode or pattern, return empty cursor. val preferences = PreferenceManager.getDefaultSharedPreferences(context) val passCodeState = preferences.getBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) - val patternState = preferences.getBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false) + val patternState = preferences.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) if (passCodeState || patternState) { return result.apply { addProtectedRoot(contextApp, passCodeState) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java index 90be2d0fb3d..0df917a3284 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java @@ -34,7 +34,7 @@ import androidx.biometric.BiometricPrompt; import com.owncloud.android.R; -import com.owncloud.android.authentication.PatternManager; +import com.owncloud.android.presentation.ui.security.PatternManager; import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; import com.owncloud.android.presentation.ui.security.PassCodeActivity; diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java index 3d5d8687acc..064763090a7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java @@ -36,6 +36,7 @@ import com.owncloud.android.R; import com.owncloud.android.extensions.ActivityExtKt; import com.owncloud.android.presentation.ui.security.PassCodeActivity; +import com.owncloud.android.presentation.ui.security.PatternActivity; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.PreferenceUtils; import timber.log.Timber; @@ -102,7 +103,7 @@ protected Boolean doInBackground(Void... params) { .getDefaultSharedPreferences(getApplicationContext()); boolean passCodeEnable = appPrefs.getBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false); - boolean patternEnabled = appPrefs.getBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false); + boolean patternEnabled = appPrefs.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false); boolean biometricEnabled = appPrefs.getBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, false); final String passcodeString = appPrefs.getString(PREFERENCE_PASSCODE, null); @@ -117,7 +118,7 @@ protected Boolean doInBackground(Void... params) { String patternValue = ""; if (patternEnabled) { - patternValue = appPrefs.getString(PatternLockActivity.KEY_PATTERN, null); + patternValue = appPrefs.getString(PatternActivity.KEY_PATTERN, null); } // Clear data @@ -137,14 +138,14 @@ protected Boolean doInBackground(Void... params) { // Recover pattern if (patternEnabled) { - appPrefsEditor.putString(PatternLockActivity.KEY_PATTERN, patternValue); + appPrefsEditor.putString(PatternActivity.KEY_PATTERN, patternValue); } // Reenable biometric appPrefsEditor.putBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, biometricEnabled); appPrefsEditor.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, passCodeEnable); - appPrefsEditor.putBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, patternEnabled); + appPrefsEditor.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, patternEnabled); result = result && appPrefsEditor.commit(); return result; diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt index d432da5c29f..af5b5308d01 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt @@ -25,7 +25,7 @@ import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvid import com.owncloud.android.presentation.ui.settings.fragments.SettingsSecurityFragment import com.owncloud.android.presentation.viewmodels.ViewModelTest import com.owncloud.android.presentation.ui.security.PassCodeActivity -import com.owncloud.android.ui.activity.PatternLockActivity +import com.owncloud.android.presentation.ui.security.PatternActivity import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -61,7 +61,7 @@ class SettingsSecurityViewModelTest : ViewModelTest() { assertTrue(patternSet) verify(exactly = 1) { - preferencesProvider.getBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false) + preferencesProvider.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) } } @@ -74,7 +74,7 @@ class SettingsSecurityViewModelTest : ViewModelTest() { assertFalse(patternSet) verify(exactly = 1) { - preferencesProvider.getBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false) + preferencesProvider.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) } } @@ -116,9 +116,9 @@ class SettingsSecurityViewModelTest : ViewModelTest() { assertTrue(patternEnableResult.isSuccess) verify(exactly = 1) { - data.getStringExtra(PatternLockActivity.KEY_PATTERN) - preferencesProvider.putString(PatternLockActivity.KEY_PATTERN, pattern) - preferencesProvider.putBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, true) + data.getStringExtra(PatternActivity.KEY_PATTERN) + preferencesProvider.putString(PatternActivity.KEY_PATTERN, pattern) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) } } @@ -140,7 +140,7 @@ class SettingsSecurityViewModelTest : ViewModelTest() { assertTrue(patternEnableResult.isError) verify(exactly = 1) { - data.getStringExtra(PatternLockActivity.KEY_PATTERN) + data.getStringExtra(PatternActivity.KEY_PATTERN) } } @@ -155,8 +155,8 @@ class SettingsSecurityViewModelTest : ViewModelTest() { assertTrue(patternDisableResult.isSuccess) verify(exactly = 1) { - data.getBooleanExtra(PatternLockActivity.KEY_CHECK_RESULT, false) - preferencesProvider.putBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false) + data.getBooleanExtra(PatternActivity.KEY_CHECK_RESULT, false) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) } } @@ -178,7 +178,7 @@ class SettingsSecurityViewModelTest : ViewModelTest() { assertTrue(patternDisableResult.isError) verify(exactly = 1) { - data.getBooleanExtra(PatternLockActivity.KEY_CHECK_RESULT, false) + data.getBooleanExtra(PatternActivity.KEY_CHECK_RESULT, false) } } From 18f76fbf4a945144fa8393a988cfa33dbed92ca7 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Mon, 20 Sep 2021 12:44:23 +0200 Subject: [PATCH 02/27] Made some adaptations for PassCodeActivity to extend AppCompatActivity instead of BaseActivity unnecessarily --- .../com/owncloud/android/extensions/ActivityExt.kt | 13 +++++++++++++ .../com/owncloud/android/extensions/FragmentExt.kt | 14 ++++++++++++++ .../presentation/ui/security/PassCodeActivity.kt | 5 +++-- .../owncloud/android/ui/activity/BaseActivity.java | 11 ----------- .../android/ui/fragment/OCFileListFragment.java | 3 ++- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt index 92015b3a87e..d1b69c7963f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt @@ -20,8 +20,10 @@ package com.owncloud.android.extensions import android.app.Activity +import android.content.Context import android.content.Intent import android.net.Uri +import android.view.inputmethod.InputMethodManager import android.widget.Toast import com.google.android.material.snackbar.Snackbar @@ -78,3 +80,14 @@ fun Activity.sendEmail( } startActivity(intent) } + +fun Activity.hideSoftKeyboard() { + val focusedView = currentFocus + focusedView?.let { + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + focusedView.windowToken, + 0 + ) + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt index f1c9ba28053..588316cfda6 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt @@ -19,8 +19,11 @@ package com.owncloud.android.extensions +import android.app.Activity import android.app.AlertDialog +import android.content.Context import android.content.DialogInterface +import android.view.inputmethod.InputMethodManager import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar import com.owncloud.android.R @@ -54,3 +57,14 @@ fun Fragment.showAlertDialog( .setNegativeButton(negativeButtonText, negativeButtonListener) .show() } + +fun Fragment.hideSoftKeyboard() { + val focusedView = requireActivity().currentFocus + focusedView?.let { + val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + focusedView.windowToken, + 0 + ) + } +} \ No newline at end of file diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt index 48db4c02b6f..1defa88b63b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt @@ -42,10 +42,11 @@ import android.view.KeyEvent import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Button +import androidx.appcompat.app.AppCompatActivity import com.owncloud.android.BuildConfig import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl +import com.owncloud.android.extensions.hideSoftKeyboard import com.owncloud.android.presentation.viewmodels.security.PassCodeViewModel -import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.utils.PreferenceUtils import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber @@ -53,7 +54,7 @@ import java.lang.IllegalArgumentException import java.lang.StringBuilder import java.util.Arrays -class PassCodeActivity : BaseActivity() { +class PassCodeActivity : AppCompatActivity() { // ViewModel private val passCodeViewModel by viewModel() diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java index b143357a481..273b6051578 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java @@ -272,17 +272,6 @@ public void run(AccountManagerFuture future) { } } - public void hideSoftKeyboard() { - View focusedView = getCurrentFocus(); - if (focusedView != null) { - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow( - focusedView.getWindowToken(), - 0 - ); - } - } - /** * Show loading dialog */ diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 96b6c586429..55411cbb859 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -61,6 +61,7 @@ import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.extensions.FragmentExtKt; import com.owncloud.android.files.FileMenuFilter; import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.presentation.ui.common.BottomSheetFragmentItemView; @@ -692,7 +693,7 @@ void loadStateFrom(Bundle savedInstanceState) { } private void clearLocalSearchView() { - ((FileActivity) requireActivity()).hideSoftKeyboard(); + FragmentExtKt.hideSoftKeyboard(this); mFileListAdapter.clearFilterBySearch(); if (mSearchView != null) { mSearchView.onActionViewCollapsed(); From c9eb1799f45961cb7677f63e329e76fe431d91c4 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 09:10:51 +0200 Subject: [PATCH 03/27] Creation of PatternViewModel --- .../dependecyinjection/ViewModelModule.kt | 2 + .../viewmodels/security/PatternViewModel.kt | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index 6c44dd9bab8..0881e0442d8 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -26,6 +26,7 @@ import com.owncloud.android.presentation.viewmodels.capabilities.OCCapabilityVie import com.owncloud.android.presentation.viewmodels.drawer.DrawerViewModel import com.owncloud.android.presentation.viewmodels.oauth.OAuthViewModel import com.owncloud.android.presentation.viewmodels.security.PassCodeViewModel +import com.owncloud.android.presentation.viewmodels.security.PatternViewModel import com.owncloud.android.presentation.viewmodels.settings.SettingsLogsViewModel import com.owncloud.android.presentation.viewmodels.settings.SettingsMoreViewModel import com.owncloud.android.presentation.viewmodels.settings.SettingsPictureUploadsViewModel @@ -59,4 +60,5 @@ val viewModelModule = module { viewModel { SettingsVideoUploadsViewModel(get(), get(), get(), get(), get(), get()) } viewModel { RemoveAccountDialogViewModel(get(), get(), get(), get()) } viewModel { PassCodeViewModel(get(), get()) } + viewModel { PatternViewModel(get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt new file mode 100644 index 00000000000..79fac07927c --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt @@ -0,0 +1,61 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.viewmodels.security + +import androidx.lifecycle.ViewModel +import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.presentation.ui.security.PassCodeActivity + +class PatternViewModel( + private val preferencesProvider: SharedPreferencesProvider +) : ViewModel() { + + fun setPassCode(passcode: String) { + preferencesProvider.putString(PassCodeActivity.PREFERENCE_PASSCODE, passcode) + preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, true) + } + + fun removePassCode() { + preferencesProvider.removePreference(PassCodeActivity.PREFERENCE_PASSCODE) + preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) + } + + fun checkPassCodeIsValid(passCodeDigits: Array): Boolean { + val passCodeString = preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, loadPinFromOldFormatIfPossible()) + if (passCodeString.isNullOrEmpty()) return false + var isValid = true + var i = 0 + while (i < passCodeDigits.size && isValid) { + val originalDigit = passCodeString[i].toString() + isValid = passCodeDigits[i] != null && passCodeDigits[i] == originalDigit + i++ + } + return isValid + } + + private fun loadPinFromOldFormatIfPossible(): String { + var pinString = "" + for (i in 1..4) + pinString += preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE_D + i, null) + + return pinString + } +} From 0e4b71228d4fb77bdeba9a532b8b491dc724bfae Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 09:11:11 +0200 Subject: [PATCH 04/27] Kotlinization of PatternActivity --- .../ui/security/PatternActivity.java | 328 ----------------- .../ui/security/PatternActivity.kt | 330 ++++++++++++++++++ 2 files changed, 330 insertions(+), 328 deletions(-) delete mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.java create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.java deleted file mode 100644 index bbb8e0bfa9d..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.java +++ /dev/null @@ -1,328 +0,0 @@ -/** - * ownCloud Android client application - * - * @author Shashvat Kedia - * @author Christian Schabesberger - * @author David González Verdugo - * @author Abel García de Prada - * Copyright (C) 2020 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.presentation.ui.security; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.KeyEvent; -import android.view.View; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; -import com.andrognito.patternlockview.PatternLockView; -import com.andrognito.patternlockview.listener.PatternLockViewListener; -import com.andrognito.patternlockview.utils.PatternLockUtils; -import com.owncloud.android.BuildConfig; -import com.owncloud.android.R; -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; -import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; -import com.owncloud.android.utils.DocumentProviderUtils; -import com.owncloud.android.utils.PreferenceUtils; -import timber.log.Timber; - -import java.util.List; - -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.PREFERENCE_LAST_UNLOCK_TIMESTAMP; - -public class PatternActivity extends AppCompatActivity { - - public final static String PREFERENCE_SET_PATTERN = "set_pattern"; - public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT"; - public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT"; - public final static String ACTION_CHECK = "ACTION_CHECK_PATTERN"; - - public final static String KEY_PATTERN = "KEY_PATTERN"; - public final static String KEY_CHECK_RESULT = "KEY_CHECK_PATTERN_RESULT"; - - private static String KEY_CONFIRMING_PATTERN = "CONFIRMING_PATTERN"; - private static String KEY_PATTERN_STRING = "PATTERN_STRING"; - private static String PATTERN_HEADER_VIEW_TEXT = "PATTERN_HEADER_VIEW_TEXT"; - private static String PATTERN_EXP_VIEW_STATE = "PATTERN_EXP_VIEW_STATE"; - private static String COUNT_VALUE = "COUNT_VALUE"; - - private boolean mPatternPresent = false; - private String mPatternValue; - private String mNewPatternValue; - - private TextView mPatternHeader; - private TextView mPatternExplanation; - private PatternLockView mPatternLockView; - private TextView mPatternError; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (!BuildConfig.DEBUG) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); - } - setContentView(R.layout.activity_pattern_lock); - - // Allow or disallow touches with other visible windows - LinearLayout activityPatternLockLayout = findViewById(R.id.activityPatternLockLayout); - activityPatternLockLayout.setFilterTouchesWhenObscured( - PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(this) - ); - - String mPatternHeaderViewText = ""; - /** - * mPatternExpShouldVisible holds the boolean value that signifies weather the patternExpView should be - * visible or not. - * it is set to true when the pattern is set and when the pattern is removed. - */ - boolean mPatternExpShouldVisible = false; - mPatternHeader = findViewById(R.id.header_pattern); - mPatternExplanation = findViewById(R.id.explanation_pattern); - mPatternLockView = findViewById(R.id.pattern_lock_view); - mPatternLockView.clearPattern(); - mPatternError = findViewById(R.id.error_pattern); - if (ACTION_CHECK.equals(getIntent().getAction())) { - /** - * This block is executed when the user opens the app after setting the pattern lock - * this block takes the pattern input by the user and check it with the pattern intially set by the user. - */ - mPatternHeader.setText(R.string.pattern_enter_pattern); - mPatternExplanation.setVisibility(View.INVISIBLE); - setCancelButtonEnabled(false); - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) { - /** - * This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) - */ - if (savedInstanceState != null) { - mPatternPresent = savedInstanceState.getBoolean(KEY_CONFIRMING_PATTERN); - mPatternValue = savedInstanceState.getString(KEY_PATTERN_STRING); - mPatternHeaderViewText = savedInstanceState.getString(PATTERN_HEADER_VIEW_TEXT); - mPatternExpShouldVisible = savedInstanceState.getBoolean(PATTERN_EXP_VIEW_STATE); - } - if (mPatternPresent) { - mPatternHeader.setText(mPatternHeaderViewText); - if (!mPatternExpShouldVisible) { - mPatternExplanation.setVisibility(View.INVISIBLE); - } - checkPattern(); - } else { - mPatternHeader.setText(R.string.pattern_configure_pattern); - mPatternExplanation.setVisibility(View.VISIBLE); - setCancelButtonEnabled(true); - } - } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - /** - * This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) - */ - mPatternHeader.setText(R.string.pattern_remove_pattern); - mPatternExplanation.setText(getResources().getString(R.string.pattern_no_longer_required)); - mPatternExplanation.setVisibility(View.VISIBLE); - setCancelButtonEnabled(true); - } else { - throw new IllegalArgumentException(R.string.illegal_argument_exception_message + " "); - } - setPatternListener(); - } - - /** - * Binds the appropiate listener to the pattern view. - */ - protected void setPatternListener() { - mPatternLockView.addPatternLockListener(new PatternLockViewListener() { - @Override - public void onStarted() { - Timber.d("Pattern Drawing Started"); - } - - @Override - public void onProgress(List list) { - Timber.d("Pattern Progress %s", PatternLockUtils.patternToString(mPatternLockView, list)); - } - - @Override - public void onComplete(List list) { - if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) { - /** - * This block gets executed when the pattern has to be set. - * count variable holds the number of time the pattern has been input. - * if the value of count is two then the pattern input first (which is stored in patternValue - * variable) - * is compared with the pattern value input the second time - * (which is stored in newPatternValue) if both the variables hold the same value - * then the pattern is set. - */ - if (mPatternValue == null || mPatternValue.length() <= 0) { - mPatternValue = PatternLockUtils.patternToString(mPatternLockView, list); - } else { - mNewPatternValue = PatternLockUtils.patternToString(mPatternLockView, list); - } - } else { - mPatternValue = PatternLockUtils.patternToString(mPatternLockView, list); - } - Timber.d("Pattern %s", PatternLockUtils.patternToString(mPatternLockView, list)); - processPattern(); - } - - @Override - public void onCleared() { - Timber.d("Pattern has been cleared"); - } - }); - } - - private void processPattern() { - if (ACTION_CHECK.equals(getIntent().getAction())) { - /** - * This block is executed when the user opens the app after setting the pattern lock - * this block takes the pattern input by the user and check it with the pattern intially set by the user. - */ - if (checkPattern()) { - mPatternError.setVisibility(View.INVISIBLE); - SharedPreferencesProvider preferencesProvider = new SharedPreferencesProviderImpl(getApplicationContext()); - preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()); - finish(); - } else { - showErrorAndRestart(R.string.pattern_incorrect_pattern, - R.string.pattern_enter_pattern, View.INVISIBLE); - } - } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - //This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) - if (checkPattern()) { - Intent result = new Intent(); - result.putExtra(KEY_CHECK_RESULT, true); - setResult(RESULT_OK, result); - mPatternError.setVisibility(View.INVISIBLE); - DocumentProviderUtils.Companion.notifyDocumentProviderRoots(getApplicationContext()); - finish(); - } else { - showErrorAndRestart(R.string.pattern_incorrect_pattern, - R.string.pattern_enter_pattern, View.INVISIBLE); - } - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) { - //This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) - if (!mPatternPresent) { - mPatternError.setVisibility(View.INVISIBLE); - requestPatternConfirmation(); - } else if (confirmPattern()) { - savePatternAndExit(); - } else { - showErrorAndRestart(R.string.pattern_not_same_pattern, - R.string.pattern_enter_pattern, View.VISIBLE); - } - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(KEY_CONFIRMING_PATTERN, mPatternPresent); - outState.putString(KEY_PATTERN_STRING, mPatternValue); - outState.putString(PATTERN_HEADER_VIEW_TEXT, mPatternHeader.getText().toString()); - if (mPatternExplanation.getVisibility() == View.VISIBLE) { - outState.putBoolean(PATTERN_EXP_VIEW_STATE, true); - } else { - outState.putBoolean(PATTERN_EXP_VIEW_STATE, false); - } - } - - private void savePatternAndExit() { - Intent result = new Intent(); - result.putExtra(KEY_PATTERN, mPatternValue); - setResult(RESULT_OK, result); - DocumentProviderUtils.Companion.notifyDocumentProviderRoots(getApplicationContext()); - finish(); - } - - /** - * Ask to the user to re-enter the pattern just entered before saving it as the current pattern. - */ - protected void requestPatternConfirmation() { - mPatternLockView.clearPattern(); - mPatternHeader.setText(R.string.pattern_reenter_pattern); - mPatternExplanation.setVisibility(View.INVISIBLE); - mPatternPresent = true; - } - - protected boolean confirmPattern() { - mPatternPresent = false; - return mNewPatternValue != null && mNewPatternValue.equals(mPatternValue); - } - - private void showErrorAndRestart(int errorMessage, int headerMessage, - int explanationVisibility) { - mPatternValue = null; - mPatternError.setText(errorMessage); - mPatternError.setVisibility(View.VISIBLE); - mPatternHeader.setText(headerMessage); - mPatternExplanation.setVisibility(explanationVisibility); - } - - protected boolean checkPattern() { - SharedPreferences appPrefs = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); - String savedPattern = appPrefs.getString(KEY_PATTERN, null); - return savedPattern != null && savedPattern.equals(mPatternValue); - } - - /** - * Enables or disables the cancel button to allow the user interrupt the ACTION - * requested to the activity. - * - * @param enabled 'True' makes the cancel button available, 'false' hides it. - */ - protected void setCancelButtonEnabled(boolean enabled) { - Button cancelButton = findViewById(R.id.cancel_pattern); - if (enabled) { - cancelButton.setVisibility(View.VISIBLE); - cancelButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }); - } else { - cancelButton.setVisibility(View.GONE); - cancelButton.setVisibility(View.INVISIBLE); - cancelButton.setOnClickListener(null); - } - } - - /** - * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while - * preventing than ACTION_CHECK may be worked around. - * - * @param keyCode Key code of the key that triggered the down event. - * @param event Event triggered. - * @return 'True' when the key event was processed by this method. - */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) || - ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - finish(); - } // else, do nothing, but report that the key was consumed to stay alive - return true; - } - return super.onKeyDown(keyCode, event); - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt new file mode 100644 index 00000000000..f7030f1ada4 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt @@ -0,0 +1,330 @@ +/** + * ownCloud Android client application + * + * @author Shashvat Kedia + * @author Christian Schabesberger + * @author David González Verdugo + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.ui.security + +import android.content.Intent +import android.os.Bundle +import android.preference.PreferenceManager +import android.view.KeyEvent +import android.view.View +import android.view.WindowManager +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.andrognito.patternlockview.PatternLockView +import com.andrognito.patternlockview.PatternLockView.Dot +import com.andrognito.patternlockview.listener.PatternLockViewListener +import com.andrognito.patternlockview.utils.PatternLockUtils +import com.owncloud.android.BuildConfig +import com.owncloud.android.R +import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl +import com.owncloud.android.presentation.viewmodels.security.PatternViewModel +import com.owncloud.android.utils.DocumentProviderUtils.Companion.notifyDocumentProviderRoots +import com.owncloud.android.utils.PreferenceUtils +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber +import java.lang.IllegalArgumentException + +class PatternActivity : AppCompatActivity() { + + // ViewModel + private val patternViewModel by viewModel() + + private var confirmingPattern = false + private var patternValue: String? = null + private var newPatternValue: String? = null + + private lateinit var bCancel: Button + private lateinit var patternHeader: TextView + private lateinit var patternExplanation: TextView + private lateinit var patternLockView: PatternLockView + private lateinit var patternError: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (!BuildConfig.DEBUG) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + + setContentView(R.layout.activity_pattern_lock) + + val activityPatternLockLayout = findViewById(R.id.activityPatternLockLayout) + bCancel = findViewById(R.id.cancel_pattern) + patternHeader = findViewById(R.id.header_pattern) + patternExplanation = findViewById(R.id.explanation_pattern) + patternLockView = findViewById(R.id.pattern_lock_view) + patternLockView.clearPattern() + patternError = findViewById(R.id.error_pattern) + + // Allow or disallow touches with other visible windows + activityPatternLockLayout.filterTouchesWhenObscured = + PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(this) + + /** + * patternExpShouldVisible holds the boolean value that signifies whether the patternExplanation should be + * visible or not. + * it is set to true when the pattern is set and when the pattern is removed. + */ + var patternExpShouldVisible = false + + if (ACTION_CHECK == intent.action) { + /** + * This block is executed when the user opens the app after setting the pattern lock + * this block takes the pattern input by the user and checks it with the pattern initially set by the user. + */ + patternHeader.text = getString(R.string.pattern_enter_pattern) + patternExplanation.visibility = View.INVISIBLE + setCancelButtonEnabled(false) + } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { + /** + * This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) + */ + var patternHeaderViewText = "" + if (savedInstanceState != null) { + confirmingPattern = savedInstanceState.getBoolean(KEY_CONFIRMING_PATTERN) + patternValue = savedInstanceState.getString(KEY_PATTERN_STRING) + patternHeaderViewText = savedInstanceState.getString(PATTERN_HEADER_VIEW_TEXT)!! + patternExpShouldVisible = savedInstanceState.getBoolean(PATTERN_EXP_VIEW_STATE) + } + if (confirmingPattern) { + patternHeader.text = patternHeaderViewText + if (!patternExpShouldVisible) { + patternExplanation.visibility = View.INVISIBLE + } + checkPattern() + } else { + patternHeader.text = getString(R.string.pattern_configure_pattern) + patternExplanation.visibility = View.VISIBLE + setCancelButtonEnabled(true) + } + } else if (ACTION_CHECK_WITH_RESULT == intent.action) { + /** + * This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) + */ + patternHeader.text = getString(R.string.pattern_remove_pattern) + patternExplanation.text = getString(R.string.pattern_no_longer_required) + patternExplanation.visibility = View.VISIBLE + setCancelButtonEnabled(true) + } else { + throw IllegalArgumentException(R.string.illegal_argument_exception_message.toString() + " ") + } + + setPatternListener() + } + + /** + * Enables or disables the cancel button to allow the user interrupt the ACTION + * requested to the activity. + * + * @param enabled 'True' makes the cancel button available, 'false' hides it. + */ + protected fun setCancelButtonEnabled(enabled: Boolean) { + if (enabled) { + bCancel.visibility = View.VISIBLE + bCancel.setOnClickListener { finish() } + } else { + bCancel.visibility = View.INVISIBLE + bCancel.setOnClickListener(null) + } + } + + protected fun checkPattern(): Boolean { + val appPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) + val savedPattern = appPrefs.getString(KEY_PATTERN, null) + return savedPattern != null && savedPattern == patternValue + } + + /** + * Binds the appropriate listener to the pattern view. + */ + protected fun setPatternListener() { + patternLockView.addPatternLockListener(object : PatternLockViewListener { + override fun onStarted() { + Timber.d("Pattern Drawing Started") + } + + override fun onProgress(list: List) { + Timber.d("Pattern Progress %s", PatternLockUtils.patternToString(patternLockView, list)) + } + + override fun onComplete(list: List) { + if (ACTION_REQUEST_WITH_RESULT == intent.action) { + /** + * This block gets executed when the pattern has to be set. + * count variable holds the number of time the pattern has been input. + * if the value of count is two then the pattern input first (which is stored in patternValue + * variable) + * is compared with the pattern value input the second time + * (which is stored in newPatternValue) if both the variables hold the same value + * then the pattern is set. + */ + if (patternValue.isNullOrEmpty()) { + patternValue = PatternLockUtils.patternToString(patternLockView, list) + } else { + newPatternValue = PatternLockUtils.patternToString(patternLockView, list) + } + } else { + patternValue = PatternLockUtils.patternToString(patternLockView, list) + } + Timber.d("Pattern %s", PatternLockUtils.patternToString(patternLockView, list)) + processPattern() + } + + override fun onCleared() { + Timber.d("Pattern has been cleared") + } + }) + } + + private fun processPattern() { + if (ACTION_CHECK == intent.action) { + /** + * This block is executed when the user opens the app after setting the pattern lock + * this block takes the pattern input by the user and checks it with the pattern initially set by the user. + */ + if (checkPattern()) { + patternError.visibility = View.INVISIBLE + val preferencesProvider = SharedPreferencesProviderImpl(applicationContext) + preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) + finish() + } else { + showErrorAndRestart( + R.string.pattern_incorrect_pattern, + R.string.pattern_enter_pattern, View.INVISIBLE + ) + } + } else if (ACTION_CHECK_WITH_RESULT == intent.action) { + //This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) + if (checkPattern()) { + val result = Intent() + result.putExtra(KEY_CHECK_RESULT, true) + setResult(RESULT_OK, result) + patternError.visibility = View.INVISIBLE + notifyDocumentProviderRoots(applicationContext) + finish() + } else { + showErrorAndRestart( + R.string.pattern_incorrect_pattern, + R.string.pattern_enter_pattern, View.INVISIBLE + ) + } + } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { + //This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) + if (!confirmingPattern) { + patternError.visibility = View.INVISIBLE + requestPatternConfirmation() + } else if (confirmPattern()) { + savePatternAndExit() + } else { + showErrorAndRestart( + R.string.pattern_not_same_pattern, + R.string.pattern_enter_pattern, View.VISIBLE + ) + } + } + } + + private fun showErrorAndRestart( + errorMessage: Int, headerMessage: Int, + explanationVisibility: Int + ) { + patternValue = null + patternError.setText(errorMessage) + patternError.visibility = View.VISIBLE + patternHeader.setText(headerMessage) + patternExplanation.visibility = explanationVisibility + } + + /** + * Ask to the user to re-enter the pattern just entered before saving it as the current pattern. + */ + protected fun requestPatternConfirmation() { + patternLockView.clearPattern() + patternHeader.setText(R.string.pattern_reenter_pattern) + patternExplanation.visibility = View.INVISIBLE + confirmingPattern = true + } + + protected fun confirmPattern(): Boolean { + confirmingPattern = false + return newPatternValue != null && newPatternValue == patternValue + } + + private fun savePatternAndExit() { + val result = Intent() + result.putExtra(KEY_PATTERN, patternValue) + setResult(RESULT_OK, result) + notifyDocumentProviderRoots(applicationContext) + finish() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.apply { + putBoolean(KEY_CONFIRMING_PATTERN, confirmingPattern) + putString(KEY_PATTERN_STRING, patternValue) + putString(PATTERN_HEADER_VIEW_TEXT, patternHeader.text.toString()) + if (patternExplanation.visibility == View.VISIBLE) { + putBoolean(PATTERN_EXP_VIEW_STATE, true) + } else { + putBoolean(PATTERN_EXP_VIEW_STATE, false) + } + } + } + + /** + * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while + * preventing than ACTION_CHECK may be worked around. + * + * @param keyCode Key code of the key that triggered the down event. + * @param event Event triggered. + * @return 'True' when the key event was processed by this method. + */ + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) { + if (ACTION_REQUEST_WITH_RESULT == intent.action || ACTION_CHECK_WITH_RESULT == intent.action) { + finish() + } // else, do nothing, but report that the key was consumed to stay alive + return true + } + return super.onKeyDown(keyCode, event) + } + + companion object { + const val PREFERENCE_SET_PATTERN = "set_pattern" + const val ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT" + const val ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT" + const val ACTION_CHECK = "ACTION_CHECK_PATTERN" + + const val KEY_PATTERN = "KEY_PATTERN" + const val KEY_CHECK_RESULT = "KEY_CHECK_PATTERN_RESULT" + + private const val KEY_CONFIRMING_PATTERN = "CONFIRMING_PATTERN" + private const val KEY_PATTERN_STRING = "PATTERN_STRING" + private const val PATTERN_HEADER_VIEW_TEXT = "PATTERN_HEADER_VIEW_TEXT" + private const val PATTERN_EXP_VIEW_STATE = "PATTERN_EXP_VIEW_STATE" + } +} From 5d67381ccdc16ce14c29b2c6e1964fcfd619437a Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 09:20:23 +0200 Subject: [PATCH 05/27] Removed unused imports --- .../main/java/com/owncloud/android/extensions/FragmentExt.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt index 588316cfda6..0300ab015b2 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt @@ -19,14 +19,12 @@ package com.owncloud.android.extensions -import android.app.Activity import android.app.AlertDialog import android.content.Context import android.content.DialogInterface import android.view.inputmethod.InputMethodManager import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar -import com.owncloud.android.R fun Fragment.showErrorInSnackbar(genericErrorMessageId: Int, throwable: Throwable?) = throwable?.let { From c2b6241041ea304ce44d477462875dbd444eb2e5 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 12:02:06 +0200 Subject: [PATCH 06/27] Logic moved to the new ViewModel --- .../ui/security/PatternActivity.kt | 21 ++++------- .../fragments/SettingsSecurityFragment.kt | 10 ++---- .../viewmodels/security/PatternViewModel.kt | 36 ++++++------------- .../settings/SettingsSecurityViewModel.kt | 14 -------- .../ui/activity/ManageSpaceActivity.java | 4 +-- 5 files changed, 21 insertions(+), 64 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt index f7030f1ada4..8ef2ddc3084 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt @@ -114,7 +114,6 @@ class PatternActivity : AppCompatActivity() { if (!patternExpShouldVisible) { patternExplanation.visibility = View.INVISIBLE } - checkPattern() } else { patternHeader.text = getString(R.string.pattern_configure_pattern) patternExplanation.visibility = View.VISIBLE @@ -151,12 +150,6 @@ class PatternActivity : AppCompatActivity() { } } - protected fun checkPattern(): Boolean { - val appPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) - val savedPattern = appPrefs.getString(KEY_PATTERN, null) - return savedPattern != null && savedPattern == patternValue - } - /** * Binds the appropriate listener to the pattern view. */ @@ -205,7 +198,7 @@ class PatternActivity : AppCompatActivity() { * This block is executed when the user opens the app after setting the pattern lock * this block takes the pattern input by the user and checks it with the pattern initially set by the user. */ - if (checkPattern()) { + if (patternViewModel.checkPatternIsValid(patternValue)) { patternError.visibility = View.INVISIBLE val preferencesProvider = SharedPreferencesProviderImpl(applicationContext) preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) @@ -218,9 +211,9 @@ class PatternActivity : AppCompatActivity() { } } else if (ACTION_CHECK_WITH_RESULT == intent.action) { //This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) - if (checkPattern()) { + if (patternViewModel.checkPatternIsValid(patternValue)) { + patternViewModel.removePattern() val result = Intent() - result.putExtra(KEY_CHECK_RESULT, true) setResult(RESULT_OK, result) patternError.visibility = View.INVISIBLE notifyDocumentProviderRoots(applicationContext) @@ -275,7 +268,7 @@ class PatternActivity : AppCompatActivity() { private fun savePatternAndExit() { val result = Intent() - result.putExtra(KEY_PATTERN, patternValue) + patternViewModel.setPattern(patternValue!!) setResult(RESULT_OK, result) notifyDocumentProviderRoots(applicationContext) finish() @@ -314,13 +307,13 @@ class PatternActivity : AppCompatActivity() { } companion object { - const val PREFERENCE_SET_PATTERN = "set_pattern" const val ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT" const val ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT" const val ACTION_CHECK = "ACTION_CHECK_PATTERN" - const val KEY_PATTERN = "KEY_PATTERN" - const val KEY_CHECK_RESULT = "KEY_CHECK_PATTERN_RESULT" + // NOTE: PREFERENCE_SET_PATTERN must have the same value as settings_security.xml-->android:key for pattern preference + const val PREFERENCE_SET_PATTERN = "set_pattern" + const val PREFERENCE_PATTERN = "KEY_PATTERN" private const val KEY_CONFIRMING_PATTERN = "CONFIRMING_PATTERN" private const val KEY_PATTERN_STRING = "PATTERN_STRING" diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt index 9d93ed3236a..7267d0ba3eb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt @@ -83,29 +83,23 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() { private val enablePatternLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult - val patternEnableResult = securityViewModel.handleEnablePattern(result.data) - if (patternEnableResult.isSuccess) { + else { prefPattern?.isChecked = true // Allow to use biometric lock and lock delay since Pattern lock has been enabled enableBiometricAndLockApplication() - } else { - showMessageInSnackbar(getString(R.string.pattern_error_set)) } } private val disablePatternLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult - val patternDisableResult = securityViewModel.handleDisablePattern(result.data) - if (patternDisableResult.isSuccess) { + else { prefPattern?.isChecked = false // Do not allow to use biometric lock nor lock delay since Pattern lock has been disabled disableBiometric() prefLockApplication?.isEnabled = false - } else { - showMessageInSnackbar(getString(R.string.pattern_error_remove)) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt index 79fac07927c..d162743f4ca 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModel.kt @@ -22,40 +22,24 @@ package com.owncloud.android.presentation.viewmodels.security import androidx.lifecycle.ViewModel import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.presentation.ui.security.PassCodeActivity +import com.owncloud.android.presentation.ui.security.PatternActivity class PatternViewModel( private val preferencesProvider: SharedPreferencesProvider ) : ViewModel() { - fun setPassCode(passcode: String) { - preferencesProvider.putString(PassCodeActivity.PREFERENCE_PASSCODE, passcode) - preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, true) + fun setPattern(pattern: String) { + preferencesProvider.putString(PatternActivity.PREFERENCE_PATTERN, pattern) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) } - fun removePassCode() { - preferencesProvider.removePreference(PassCodeActivity.PREFERENCE_PASSCODE) - preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) + fun removePattern() { + preferencesProvider.removePreference(PatternActivity.PREFERENCE_PATTERN) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) } - fun checkPassCodeIsValid(passCodeDigits: Array): Boolean { - val passCodeString = preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, loadPinFromOldFormatIfPossible()) - if (passCodeString.isNullOrEmpty()) return false - var isValid = true - var i = 0 - while (i < passCodeDigits.size && isValid) { - val originalDigit = passCodeString[i].toString() - isValid = passCodeDigits[i] != null && passCodeDigits[i] == originalDigit - i++ - } - return isValid - } - - private fun loadPinFromOldFormatIfPossible(): String { - var pinString = "" - for (i in 1..4) - pinString += preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE_D + i, null) - - return pinString + fun checkPatternIsValid(patternValue: String?): Boolean { + val savedPattern = preferencesProvider.getString(PatternActivity.PREFERENCE_PATTERN, null) + return savedPattern != null && savedPattern == patternValue } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt index 6314e74f423..4358e0a3930 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt @@ -36,20 +36,6 @@ class SettingsSecurityViewModel( fun isPasscodeSet() = preferencesProvider.getBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) - fun handleEnablePattern(data: Intent?): UIResult { - val pattern = data?.getStringExtra(PatternActivity.KEY_PATTERN) ?: return UIResult.Error() - preferencesProvider.putString(PatternActivity.KEY_PATTERN, pattern) - preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) - return UIResult.Success() - } - - fun handleDisablePattern(data: Intent?): UIResult { - data?.getBooleanExtra(PatternActivity.KEY_CHECK_RESULT, false).takeIf { it == true } - ?: return UIResult.Error() - preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) - return UIResult.Success() - } - fun setPrefTouchesWithOtherVisibleWindows(value: Boolean) = preferencesProvider.putBoolean(SettingsSecurityFragment.PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS, value) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java index 064763090a7..6956eca0951 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java @@ -118,7 +118,7 @@ protected Boolean doInBackground(Void... params) { String patternValue = ""; if (patternEnabled) { - patternValue = appPrefs.getString(PatternActivity.KEY_PATTERN, null); + patternValue = appPrefs.getString(PatternActivity.PREFERENCE_PATTERN, null); } // Clear data @@ -138,7 +138,7 @@ protected Boolean doInBackground(Void... params) { // Recover pattern if (patternEnabled) { - appPrefsEditor.putString(PatternActivity.KEY_PATTERN, patternValue); + appPrefsEditor.putString(PatternActivity.PREFERENCE_PATTERN, patternValue); } // Reenable biometric From 20847dafbe8cf56d8f18e5be120cf59913e02a76 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 12:49:44 +0200 Subject: [PATCH 07/27] Adapted existing unit and UI tests --- ...ternLockTest.kt => PatternActivityTest.kt} | 42 ++++++++-- .../security/SettingsSecurityFragmentTest.kt | 43 +--------- .../settings/SettingsSecurityViewModelTest.kt | 84 ------------------- 3 files changed, 38 insertions(+), 131 deletions(-) rename owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/{OCSettingsPatternLockTest.kt => PatternActivityTest.kt} (74%) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/OCSettingsPatternLockTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt similarity index 74% rename from owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/OCSettingsPatternLockTest.kt rename to owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt index e497c7439bc..cf9b1300401 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/OCSettingsPatternLockTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt @@ -2,7 +2,9 @@ * ownCloud Android client application * * @author Jesus Recio (@jesmrec) - * Copyright (C) 2020 ownCloud GmbH. + * @author Juan Carlos Garrote Gascón (@JuancaG05) + * + * Copyright (C) 2021 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -30,20 +32,48 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule import com.owncloud.android.R import com.owncloud.android.presentation.ui.security.PatternActivity +import com.owncloud.android.presentation.viewmodels.security.PatternViewModel +import io.mockk.mockk import org.junit.After +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module -class OCSettingsPatternLockTest { +class PatternActivityTest { @Rule @JvmField val activityRule = ActivityTestRule(PatternActivity::class.java, true, false) + private val intent = Intent() private val context = InstrumentationRegistry.getInstrumentation().targetContext private val patternToSave = "1234" + private lateinit var patternViewModel: PatternViewModel + + @Before + fun setUp() { + patternViewModel = mockk(relaxUnitFun = true) + + stopKoin() + + startKoin { + context + modules( + module(override = true) { + viewModel { + patternViewModel + } + } + ) + } + } + @After fun tearDown() { //Clean preferences @@ -75,9 +105,11 @@ class OCSettingsPatternLockTest { private fun storePattern() { val appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit() - appPrefs.putString(PatternActivity.KEY_PATTERN, patternToSave) - appPrefs.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) - appPrefs.apply() + appPrefs.apply { + putString(PatternActivity.PREFERENCE_PATTERN, patternToSave) + putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) + apply() + } } private fun openPatternActivity(mode: String) { diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt index c2cff829d93..8b0d58e118f 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt @@ -38,7 +38,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import com.owncloud.android.R import com.owncloud.android.authentication.BiometricManager -import com.owncloud.android.presentation.UIResult import com.owncloud.android.presentation.ui.security.PREFERENCE_LOCK_TIMEOUT import com.owncloud.android.presentation.ui.settings.fragments.SettingsSecurityFragment import com.owncloud.android.presentation.viewmodels.settings.SettingsSecurityViewModel @@ -232,34 +231,17 @@ class SettingsSecurityFragmentTest { @Test fun patternLockEnabledOk() { every { securityViewModel.isPasscodeSet() } returns false - every { securityViewModel.handleEnablePattern(any()) } returns UIResult.Success() launchTest() mockIntent( - extras = Pair(PatternActivity.KEY_PATTERN, OC_PATTERN), + extras = Pair(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN), action = PatternActivity.ACTION_REQUEST_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) assertTrue(prefPattern.isChecked) } - @Test - fun patternLockEnabledError() { - every { securityViewModel.isPasscodeSet() } returns false - every { securityViewModel.handleEnablePattern(any()) } returns UIResult.Error() - - launchTest() - - mockIntent( - extras = Pair(PatternActivity.KEY_PATTERN, OC_PATTERN), - action = PatternActivity.ACTION_REQUEST_WITH_RESULT - ) - onView(withText(R.string.prefs_pattern)).perform(click()) - assertFalse(prefPattern.isChecked) - onView(withText(R.string.pattern_error_set)).check(matches(isDisplayed())) - } - @Test fun enablePasscodeEnablesBiometricLockAndLockApplication() { launchTest() @@ -322,13 +304,10 @@ class SettingsSecurityFragmentTest { @Test fun disablePatternOk() { - every { securityViewModel.handleDisablePattern(any()) } returns UIResult.Success() - launchTest() firstEnablePattern() mockIntent( - extras = Pair(PatternActivity.KEY_CHECK_RESULT, true), action = PatternActivity.ACTION_CHECK_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) @@ -339,24 +318,6 @@ class SettingsSecurityFragmentTest { assertFalse(prefLockApplication.isEnabled) } - @Test - fun disablePatternError() { - every { securityViewModel.handleDisablePattern(any()) } returns UIResult.Error() - - launchTest() - - firstEnablePattern() - mockIntent( - extras = Pair(PatternActivity.KEY_CHECK_RESULT, true), - action = PatternActivity.ACTION_CHECK_WITH_RESULT - ) - onView(withText(R.string.prefs_pattern)).perform(click()) - assertTrue(prefPattern.isChecked) - onView(withText(R.string.prefs_biometric)).check(matches(isEnabled())) - assertTrue(prefBiometric!!.isEnabled) - onView(withText(R.string.pattern_error_remove)).check(matches(isDisplayed())) - } - @Test fun enableBiometricLockWithPasscodeEnabled() { every { biometricManager.hasEnrolledBiometric() } returns true @@ -451,10 +412,8 @@ class SettingsSecurityFragmentTest { private fun firstEnablePattern() { every { securityViewModel.isPasscodeSet() } returns false - every { securityViewModel.handleEnablePattern(any()) } returns UIResult.Success() mockIntent( - extras = Pair(PatternActivity.KEY_PATTERN, OC_PATTERN), action = PatternActivity.ACTION_REQUEST_WITH_RESULT ) onView(withText(R.string.prefs_pattern)).perform(click()) diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt index af5b5308d01..33830b3b4c7 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt @@ -20,7 +20,6 @@ package com.owncloud.android.presentation.viewmodels.settings -import android.content.Intent import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider import com.owncloud.android.presentation.ui.settings.fragments.SettingsSecurityFragment import com.owncloud.android.presentation.viewmodels.ViewModelTest @@ -47,11 +46,6 @@ class SettingsSecurityViewModelTest : ViewModelTest() { securityViewModel = SettingsSecurityViewModel(preferencesProvider) } - @After - override fun tearDown() { - super.tearDown() - } - @Test fun `is pattern set - ok - true`() { every { preferencesProvider.getBoolean(any(), any()) } returns true @@ -104,84 +98,6 @@ class SettingsSecurityViewModelTest : ViewModelTest() { } } - @Test - fun `handle enable pattern - ok`() { - val data: Intent = mockk() - val pattern = "pattern" - - every { data.getStringExtra(any()) } returns pattern - - val patternEnableResult = securityViewModel.handleEnablePattern(data) - - assertTrue(patternEnableResult.isSuccess) - - verify(exactly = 1) { - data.getStringExtra(PatternActivity.KEY_PATTERN) - preferencesProvider.putString(PatternActivity.KEY_PATTERN, pattern) - preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) - } - } - - @Test - fun `handle enable pattern - ko - data intent is null`() { - val patternEnableResult = securityViewModel.handleEnablePattern(null) - - assertTrue(patternEnableResult.isError) - } - - @Test - fun `handle enable pattern - ko - pattern is null`() { - val data: Intent = mockk() - - every { data.getStringExtra(any()) } returns null - - val patternEnableResult = securityViewModel.handleEnablePattern(data) - - assertTrue(patternEnableResult.isError) - - verify(exactly = 1) { - data.getStringExtra(PatternActivity.KEY_PATTERN) - } - } - - @Test - fun `handle disable pattern - ok`() { - val data: Intent = mockk() - - every { data.getBooleanExtra(any(), any()) } returns true - - val patternDisableResult = securityViewModel.handleDisablePattern(data) - - assertTrue(patternDisableResult.isSuccess) - - verify(exactly = 1) { - data.getBooleanExtra(PatternActivity.KEY_CHECK_RESULT, false) - preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) - } - } - - @Test - fun `handle disable pattern - ko - data intent is null`() { - val patternDisableResult = securityViewModel.handleDisablePattern(null) - - assertTrue(patternDisableResult.isError) - } - - @Test - fun `handle disable pattern - ko - key check result is false`() { - val data: Intent = mockk() - - every { data.getBooleanExtra(any(), any()) } returns false - - val patternDisableResult = securityViewModel.handleDisablePattern(data) - - assertTrue(patternDisableResult.isError) - - verify(exactly = 1) { - data.getBooleanExtra(PatternActivity.KEY_CHECK_RESULT, false) - } - } - @Test fun `set pref touches with other visible windows - ok - true`() { securityViewModel.setPrefTouchesWithOtherVisibleWindows(true) From d1b39fe53f7975aeb6d423f7736d2319621dc915 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 14:10:17 +0200 Subject: [PATCH 08/27] Removed unused imports --- .../viewmodels/settings/SettingsSecurityViewModelTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt index 33830b3b4c7..f0f880f0b28 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt @@ -29,7 +29,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before From 7588a24d1a623b43c1d4eab0a9faed30d189a3b2 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 14:13:43 +0200 Subject: [PATCH 09/27] More unused imports removals --- .../android/presentation/ui/security/PatternActivity.kt | 1 - .../viewmodels/settings/SettingsSecurityViewModel.kt | 2 -- 2 files changed, 3 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt index 8ef2ddc3084..e91faf1f936 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt @@ -25,7 +25,6 @@ package com.owncloud.android.presentation.ui.security import android.content.Intent import android.os.Bundle -import android.preference.PreferenceManager import android.view.KeyEvent import android.view.View import android.view.WindowManager diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt index 4358e0a3930..65f8c6594fd 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModel.kt @@ -20,10 +20,8 @@ package com.owncloud.android.presentation.viewmodels.settings -import android.content.Intent import androidx.lifecycle.ViewModel import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.presentation.UIResult import com.owncloud.android.presentation.ui.settings.fragments.SettingsSecurityFragment import com.owncloud.android.presentation.ui.security.PassCodeActivity import com.owncloud.android.presentation.ui.security.PatternActivity From 35115d568f7de9fb3ee7542550ccf12b6ff5c741 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 21 Sep 2021 15:44:25 +0200 Subject: [PATCH 10/27] Added unit tests for the new ViewModel --- .../security/PatternViewModelTest.kt | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt new file mode 100644 index 00000000000..2d5fff9a4e0 --- /dev/null +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt @@ -0,0 +1,113 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.viewmodels.security + +import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.presentation.ui.security.PatternActivity +import com.owncloud.android.presentation.viewmodels.ViewModelTest +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class PatternViewModelTest : ViewModelTest() { + private lateinit var patternViewModel: PatternViewModel + private lateinit var preferencesProvider: SharedPreferencesProvider + + private val pattern = "1234" + + @Before + fun setUp() { + preferencesProvider = mockk(relaxUnitFun = true) + patternViewModel = PatternViewModel(preferencesProvider) + } + + @Test + fun `set pattern - ok`() { + patternViewModel.setPattern(pattern) + + verify(exactly = 1) { + preferencesProvider.putString(PatternActivity.PREFERENCE_PATTERN, pattern) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) + } + } + + @Test + fun `remove pattern - ok`() { + patternViewModel.removePattern() + + verify(exactly = 1) { + preferencesProvider.removePreference(PatternActivity.PREFERENCE_PATTERN) + preferencesProvider.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) + } + } + + @Test + fun `check pattern is valid - ok`() { + every { preferencesProvider.getString(any(), any()) } returns pattern + + val patternValue = "1234" + + val patternCheckResult = patternViewModel.checkPatternIsValid(patternValue) + + assertTrue(patternCheckResult) + + verify(exactly = 1) { + preferencesProvider.getString(PatternActivity.PREFERENCE_PATTERN, null) + } + } + + @Test + fun `check pattern is valid - ko - saved pattern is null`() { + every { preferencesProvider.getString(any(), any()) } returns null + + val patternValue = "1234" + + val patternCheckResult = patternViewModel.checkPatternIsValid(patternValue) + + assertFalse(patternCheckResult) + + verify(exactly = 1) { + preferencesProvider.getString(PatternActivity.PREFERENCE_PATTERN, null) + } + } + + @Test + fun `check pattern is valid - ko - different pattern`() { + every { preferencesProvider.getString(any(), any()) } returns pattern + + val patternValue = "1235" + + val patternCheckResult = patternViewModel.checkPatternIsValid(patternValue) + + assertFalse(patternCheckResult) + + verify(exactly = 1) { + preferencesProvider.getString(PatternActivity.PREFERENCE_PATTERN, null) + } + } + +} From e1623ddaa30bace8f4bf39c109b97d043bb06a4c Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Wed, 22 Sep 2021 08:58:17 +0200 Subject: [PATCH 11/27] Kotlinizing PatternManager --- .../main/java/com/owncloud/android/MainApp.kt | 4 +- .../authentication/BiometricManager.java | 4 +- .../ui/security/PatternManager.java | 115 ------------------ .../ui/security/PatternManager.kt | 85 +++++++++++++ .../ui/activity/BiometricActivity.java | 4 +- 5 files changed, 91 insertions(+), 121 deletions(-) delete mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.java create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt index 47d5ed09130..03cef915dfd 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt @@ -116,7 +116,7 @@ class MainApp : Application() { override fun onActivityStarted(activity: Activity) { Timber.v("${activity.javaClass.simpleName} onStart() starting") PassCodeManager.onActivityStarted(activity) - PatternManager.getPatternManager().onActivityStarted(activity) + PatternManager.onActivityStarted(activity) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { BiometricManager.getBiometricManager(activity).onActivityStarted(activity) } @@ -133,7 +133,7 @@ class MainApp : Application() { override fun onActivityStopped(activity: Activity) { Timber.v("${activity.javaClass.simpleName} onStop() ending") PassCodeManager.onActivityStopped(activity) - PatternManager.getPatternManager().onActivityStopped(activity) + PatternManager.onActivityStopped(activity) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { BiometricManager.getBiometricManager(activity).onActivityStopped(activity) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/authentication/BiometricManager.java b/owncloudApp/src/main/java/com/owncloud/android/authentication/BiometricManager.java index ef95a5d66bd..1d2937920f1 100755 --- a/owncloudApp/src/main/java/com/owncloud/android/authentication/BiometricManager.java +++ b/owncloudApp/src/main/java/com/owncloud/android/authentication/BiometricManager.java @@ -87,9 +87,9 @@ public void onActivityStarted(Activity activity) { // Cancel biometric lock and use passcode unlock method PassCodeManager.INSTANCE.onBiometricCancelled(activity); mVisibleActivitiesCounter++; - } else if (PatternManager.getPatternManager().isPatternEnabled()) { + } else if (PatternManager.INSTANCE.isPatternEnabled()) { // Cancel biometric lock and use pattern unlock method - PatternManager.getPatternManager().onBiometricCancelled(activity); + PatternManager.INSTANCE.onBiometricCancelled(activity); mVisibleActivitiesCounter++; } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.java deleted file mode 100644 index 283a11a49fc..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.java +++ /dev/null @@ -1,115 +0,0 @@ -/** - * ownCloud Android client application - * - * @author Shashvat Kedia - * @author Christian Schabesberger - * @author Juan Carlos Garrote Gascón - *

- * Copyright (C) 2021 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.presentation.ui.security; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.PowerManager; - -import com.owncloud.android.MainApp; -import com.owncloud.android.authentication.BiometricManager; -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; -import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; - -import java.util.HashSet; -import java.util.Set; - -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.PREFERENCE_LAST_UNLOCK_TIMESTAMP; -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.PREFERENCE_LOCK_TIMEOUT; -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.bayPassUnlockOnce; - -public class PatternManager { - - private static final Set sExemptOfPatternActivites; - private int mVisibleActivitiesCounter = 0; - private final SharedPreferencesProvider preferencesProvider = new SharedPreferencesProviderImpl(MainApp.Companion.getAppContext()); - - static { - sExemptOfPatternActivites = new HashSet<>(); - sExemptOfPatternActivites.add(PatternActivity.class); - } - - private static PatternManager mPatternManagerInstance = null; - - public static PatternManager getPatternManager() { - if (mPatternManagerInstance == null) { - mPatternManagerInstance = new PatternManager(); - } - return mPatternManagerInstance; - } - - private PatternManager() { - } - - public void onActivityStarted(Activity activity) { - if (!sExemptOfPatternActivites.contains(activity.getClass()) && patternShouldBeRequested()) { - - // Do not ask for pattern if biometric is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.getBiometricManager(activity).isBiometricEnabled()) { - mVisibleActivitiesCounter++; - return; - } - - askUserForPattern(activity); - } - - mVisibleActivitiesCounter++; - } - - public void onActivityStopped(Activity activity) { - if (mVisibleActivitiesCounter > 0) { - mVisibleActivitiesCounter--; - } - bayPassUnlockOnce(); - PowerManager powerMgr = (PowerManager) activity.getSystemService(Context.POWER_SERVICE); - if (isPatternEnabled() && powerMgr != null && !powerMgr.isScreenOn()) { - activity.moveTaskToBack(true); - } - } - - private boolean patternShouldBeRequested() { - long lastUnlockTimestamp = preferencesProvider.getLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, 0); - int timeout = LockTimeout.valueOf(preferencesProvider.getString(PREFERENCE_LOCK_TIMEOUT, LockTimeout.IMMEDIATELY.name())).toMilliseconds(); - if (System.currentTimeMillis() - lastUnlockTimestamp > timeout && mVisibleActivitiesCounter <= 0) { - return isPatternEnabled(); - } - return false; - } - - public boolean isPatternEnabled() { - return preferencesProvider.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false); - } - - private void askUserForPattern(Activity activity) { - Intent i = new Intent(MainApp.Companion.getAppContext(), PatternActivity.class); - i.setAction(PatternActivity.ACTION_CHECK); - i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - activity.startActivity(i); - } - - public void onBiometricCancelled(Activity activity) { - askUserForPattern(activity); - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt new file mode 100644 index 00000000000..9d38a5151e2 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt @@ -0,0 +1,85 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.ui.security + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.PowerManager +import com.owncloud.android.MainApp.Companion.appContext +import com.owncloud.android.authentication.BiometricManager +import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl + +object PatternManager { + + private val exemptOfPatternActivities: MutableSet> = mutableSetOf(PatternActivity::class.java) + private var visibleActivitiesCounter = 0 + private val preferencesProvider = SharedPreferencesProviderImpl(appContext) + + fun onActivityStarted(activity: Activity) { + if (!exemptOfPatternActivities.contains(activity.javaClass) && patternShouldBeRequested()) { + + // Do not ask for pattern if biometric is enabled + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.getBiometricManager(activity).isBiometricEnabled) { + visibleActivitiesCounter++ + return + } + + askUserForPattern(activity) + } + + visibleActivitiesCounter++ + } + + fun onActivityStopped(activity: Activity) { + if (visibleActivitiesCounter > 0) visibleActivitiesCounter-- + + bayPassUnlockOnce() + val powerMgr = activity.getSystemService(Context.POWER_SERVICE) as PowerManager + if (isPatternEnabled() && !powerMgr.isScreenOn) { + activity.moveTaskToBack(true) + } + } + + private fun patternShouldBeRequested(): Boolean { + val lastUnlockTimestamp = preferencesProvider.getLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, 0) + val timeout = LockTimeout.valueOf(preferencesProvider.getString(PREFERENCE_LOCK_TIMEOUT, LockTimeout.IMMEDIATELY.name)!!).toMilliseconds() + return if (System.currentTimeMillis() - lastUnlockTimestamp > timeout && visibleActivitiesCounter <= 0) isPatternEnabled() + else false + } + + fun isPatternEnabled(): Boolean { + return preferencesProvider.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) + } + + private fun askUserForPattern(activity: Activity) { + val i = Intent(appContext, PatternActivity::class.java).apply { + action = PatternActivity.ACTION_CHECK + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + activity.startActivity(i) + } + + fun onBiometricCancelled(activity: Activity) { + askUserForPattern(activity) + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java index 0df917a3284..bb5bd828c74 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/BiometricActivity.java @@ -163,8 +163,8 @@ public void onAuthenticationFailed() { private void authError() { if (PassCodeManager.INSTANCE.isPassCodeEnabled()) { PassCodeManager.INSTANCE.onBiometricCancelled(mActivity); - } else if (PatternManager.getPatternManager().isPatternEnabled()) { - PatternManager.getPatternManager().onBiometricCancelled(mActivity); + } else if (PatternManager.INSTANCE.isPatternEnabled()) { + PatternManager.INSTANCE.onBiometricCancelled(mActivity); } mActivity.finish(); From 30e9f9aedd9b547dd7f34ff515b5bd9346eef218 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Wed, 29 Sep 2021 11:48:25 +0200 Subject: [PATCH 12/27] Moved all classes to presentation.ui.security package --- .../settings/security/SettingsSecurityFragmentTest.kt | 4 ++-- owncloudApp/src/main/AndroidManifest.xml | 2 +- owncloudApp/src/main/java/com/owncloud/android/MainApp.kt | 4 ++-- .../java/com/owncloud/android/db/PreferenceManager.java | 2 +- .../ui/security}/BiometricActivity.java | 5 +---- .../ui/security}/BiometricManager.java | 6 +----- .../android/presentation/ui/security/PassCodeManager.kt | 1 - .../android/presentation/ui/security/PatternManager.kt | 1 - .../ui/settings/fragments/SettingsSecurityFragment.kt | 4 ++-- .../owncloud/android/ui/activity/ManageSpaceActivity.java | 1 + 10 files changed, 11 insertions(+), 19 deletions(-) rename owncloudApp/src/main/java/com/owncloud/android/{ui/activity => presentation/ui/security}/BiometricActivity.java (97%) rename owncloudApp/src/main/java/com/owncloud/android/{authentication => presentation/ui/security}/BiometricManager.java (94%) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt index 8b0d58e118f..c0bd353f3b6 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt @@ -37,11 +37,11 @@ import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import com.owncloud.android.R -import com.owncloud.android.authentication.BiometricManager +import com.owncloud.android.presentation.ui.security.BiometricManager import com.owncloud.android.presentation.ui.security.PREFERENCE_LOCK_TIMEOUT import com.owncloud.android.presentation.ui.settings.fragments.SettingsSecurityFragment import com.owncloud.android.presentation.viewmodels.settings.SettingsSecurityViewModel -import com.owncloud.android.ui.activity.BiometricActivity +import com.owncloud.android.presentation.ui.security.BiometricActivity import com.owncloud.android.presentation.ui.security.PassCodeActivity import com.owncloud.android.presentation.viewmodels.security.PassCodeViewModel import com.owncloud.android.testutil.security.OC_PASSCODE_4_DIGITS diff --git a/owncloudApp/src/main/AndroidManifest.xml b/owncloudApp/src/main/AndroidManifest.xml index 4d7c4beffa8..586175a7af8 100644 --- a/owncloudApp/src/main/AndroidManifest.xml +++ b/owncloudApp/src/main/AndroidManifest.xml @@ -238,7 +238,7 @@ android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:theme="@style/Theme.ownCloud.Fullscreen" /> - + . */ -package com.owncloud.android.ui.activity; +package com.owncloud.android.presentation.ui.security; import android.content.Intent; import android.os.Build; @@ -34,11 +34,8 @@ import androidx.biometric.BiometricPrompt; import com.owncloud.android.R; -import com.owncloud.android.presentation.ui.security.PatternManager; import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; -import com.owncloud.android.presentation.ui.security.PassCodeActivity; -import com.owncloud.android.presentation.ui.security.PassCodeManager; import timber.log.Timber; import javax.crypto.Cipher; diff --git a/owncloudApp/src/main/java/com/owncloud/android/authentication/BiometricManager.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.java similarity index 94% rename from owncloudApp/src/main/java/com/owncloud/android/authentication/BiometricManager.java rename to owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.java index 1d2937920f1..4e7e70d0f0d 100755 --- a/owncloudApp/src/main/java/com/owncloud/android/authentication/BiometricManager.java +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.java @@ -21,7 +21,7 @@ * along with this program. If not, see . */ -package com.owncloud.android.authentication; +package com.owncloud.android.presentation.ui.security; import android.app.Activity; import android.content.Context; @@ -33,10 +33,6 @@ import com.owncloud.android.MainApp; import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; -import com.owncloud.android.presentation.ui.security.LockTimeout; -import com.owncloud.android.presentation.ui.security.PassCodeManager; -import com.owncloud.android.presentation.ui.security.PatternManager; -import com.owncloud.android.ui.activity.BiometricActivity; import java.util.HashSet; import java.util.Set; diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt index 320d2cc83ac..469292ad338 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt @@ -26,7 +26,6 @@ import android.content.Intent import android.os.Build import android.os.PowerManager import com.owncloud.android.MainApp.Companion.appContext -import com.owncloud.android.authentication.BiometricManager import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl object PassCodeManager { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt index 9d38a5151e2..ca27ade173c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt @@ -26,7 +26,6 @@ import android.content.Intent import android.os.Build import android.os.PowerManager import com.owncloud.android.MainApp.Companion.appContext -import com.owncloud.android.authentication.BiometricManager import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl object PatternManager { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt index 7267d0ba3eb..e8a73a3325a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt @@ -33,12 +33,12 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen import com.owncloud.android.R -import com.owncloud.android.authentication.BiometricManager +import com.owncloud.android.presentation.ui.security.BiometricManager import com.owncloud.android.extensions.showMessageInSnackbar import com.owncloud.android.presentation.ui.security.PREFERENCE_LOCK_TIMEOUT import com.owncloud.android.presentation.ui.security.LockTimeout import com.owncloud.android.presentation.viewmodels.settings.SettingsSecurityViewModel -import com.owncloud.android.ui.activity.BiometricActivity +import com.owncloud.android.presentation.ui.security.BiometricActivity import com.owncloud.android.presentation.ui.security.PassCodeActivity import com.owncloud.android.presentation.ui.security.PatternActivity import org.koin.androidx.viewmodel.ext.android.viewModel diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java index 6956eca0951..de6d0a3197a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.java @@ -35,6 +35,7 @@ import com.google.android.material.snackbar.Snackbar; import com.owncloud.android.R; import com.owncloud.android.extensions.ActivityExtKt; +import com.owncloud.android.presentation.ui.security.BiometricActivity; import com.owncloud.android.presentation.ui.security.PassCodeActivity; import com.owncloud.android.presentation.ui.security.PatternActivity; import com.owncloud.android.utils.FileStorageUtils; From 68c0b07f7d7015ed9299b5117e988286c4ead992 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Wed, 29 Sep 2021 11:49:30 +0200 Subject: [PATCH 13/27] Bumped biometric dependency from 1.0.1 to 1.1.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f73f4c24557..d3b7eeb055d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { appCompat = "1.2.0" sqliteVersion = "2.1.0" lifecycleLiveData = "2.3.1" - biometricVersion="1.0.1" + biometricVersion="1.1.0" // Kotlin kotlinVersion = "1.5.21" From ebcc72692b47f05db6575acf819f2dc7e2453ad1 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Thu, 30 Sep 2021 09:23:18 +0200 Subject: [PATCH 14/27] Kotlinized BiometricActivity --- .../ui/security/BiometricActivity.java | 244 ------------------ .../ui/security/BiometricActivity.kt | 216 ++++++++++++++++ 2 files changed, 216 insertions(+), 244 deletions(-) delete mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.java create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.java deleted file mode 100644 index 97442d36942..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.java +++ /dev/null @@ -1,244 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David González Verdugo - * Copyright (C) 2020 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.presentation.ui.security; - -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyPermanentlyInvalidatedException; -import android.security.keystore.KeyProperties; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.AppCompatActivity; -import androidx.biometric.BiometricManager; -import androidx.biometric.BiometricPrompt; - -import com.owncloud.android.R; -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; -import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; -import timber.log.Timber; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.util.concurrent.Executor; - -import static com.owncloud.android.presentation.ui.security.PassCodeActivity.PASSCODE_MIN_LENGTH; -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.PREFERENCE_LAST_UNLOCK_TIMESTAMP; - -public class BiometricActivity extends AppCompatActivity { - - private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; - - public final static String PREFERENCE_SET_BIOMETRIC = "set_biometric"; - - private static final String KEY_NAME = "default_key"; - - private KeyStore mKeyStore; - private KeyGenerator mKeyGenerator; - private Cipher mCipher; - private BiometricPrompt.CryptoObject mCryptoObject; - private BiometricActivity mActivity; - - /** - * Initializes the activity. - *

- * An intent with a valid ACTION is expected; if none is found, an - * {@link IllegalArgumentException} will be thrown. - * - * @param savedInstanceState Previously saved state - irrelevant in this case - */ - @RequiresApi(api = Build.VERSION_CODES.M) - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mActivity = this; - - generateAndStoreKey(); - - if (initCipher()) { - mCryptoObject = new BiometricPrompt.CryptoObject(mCipher); - } - - BiometricManager biometricManager = BiometricManager.from(this); - if (biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { - showBiometricPrompt(); - } else { - authError(); - } - } - - private Handler handler = new Handler(); - - private Executor executor = new Executor() { - @Override - public void execute(Runnable command) { - handler.post(command); - } - }; - - private void showBiometricPrompt() { - BiometricPrompt.PromptInfo promptInfo = - new BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.biometric_prompt_title)) - .setSubtitle(getString(R.string.biometric_prompt_subtitle)) - .setNegativeButtonText(getString(android.R.string.cancel)) - .setConfirmationRequired(true) - .setDeviceCredentialAllowed(false) - .build(); - - BiometricPrompt biometricPrompt = new BiometricPrompt(BiometricActivity.this, - executor, new BiometricPrompt.AuthenticationCallback() { - @Override - public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { - super.onAuthenticationError(errorCode, errString); - Timber.e("onAuthenticationError (" + errorCode + "): " + errString); - authError(); - } - - @Override - public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { - super.onAuthenticationSucceeded(result); - SharedPreferencesProvider preferencesProvider = new SharedPreferencesProviderImpl(getApplicationContext()); - // In this line, null is only provisional until the rearchitecture of BiometricActivity - String passCode = preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, null); - int passCodeDigits = getApplicationContext().getResources().getInteger(R.integer.passcode_digits); - if (passCodeDigits < PASSCODE_MIN_LENGTH) passCodeDigits = PASSCODE_MIN_LENGTH; - if (passCode != null && passCode.length() < passCodeDigits) { - preferencesProvider.removePreference(PassCodeActivity.PREFERENCE_PASSCODE); - preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false); - Intent intent = new Intent(getBaseContext(), PassCodeActivity.class); - intent.setAction(PassCodeActivity.ACTION_REQUEST_WITH_RESULT); - intent.putExtra(PassCodeActivity.EXTRAS_MIGRATION, true); - startActivity(intent); - } - preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()); - mActivity.finish(); - } - - @Override - public void onAuthenticationFailed() { - super.onAuthenticationFailed(); - Timber.e("onAuthenticationFailed"); - } - }); - - // Displays the "log in" prompt. - biometricPrompt.authenticate(promptInfo, mCryptoObject); - } - - private void authError() { - if (PassCodeManager.INSTANCE.isPassCodeEnabled()) { - PassCodeManager.INSTANCE.onBiometricCancelled(mActivity); - } else if (PatternManager.INSTANCE.isPatternEnabled()) { - PatternManager.INSTANCE.onBiometricCancelled(mActivity); - } - - mActivity.finish(); - } - - /** - * Generate encryption key involved in biometric authentication process and store it securely on the device using - * the Android Keystore system - */ - @RequiresApi(api = Build.VERSION_CODES.M) - private void generateAndStoreKey() { - - try { - // Access Android Keystore container, used to safely store cryptographic keys on Android devices - mKeyStore = KeyStore.getInstance(ANDROID_KEY_STORE); - - } catch (KeyStoreException e) { - Timber.e(e, "Failed while getting KeyStore instance"); - } - - try { - // Access Android KeyGenerator to create the encryption key involved in biometric authentication process - mKeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE); - - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - Timber.e(e, "Failed while getting KeyGenerator instance"); - } - - try { - // Generate and save the encryption key - mKeyStore.load(null); - mKeyGenerator.init(new - KeyGenParameterSpec.Builder(KEY_NAME, - KeyProperties.PURPOSE_ENCRYPT | - KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_CBC) - .setUserAuthenticationRequired(true) - .setEncryptionPaddings( - KeyProperties.ENCRYPTION_PADDING_PKCS7) - .build()); - mKeyGenerator.generateKey(); - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertificateException | IOException e) { - Timber.e(e, "Failed while generating and saving the encryption key"); - } - } - - /** - * Init mCipher that will be used to create the encrypted {@link BiometricPrompt.CryptoObject} instance. This - * CryptoObject will be used during the biometric authentication process - * - * @return true if mCipher is properly initialized, false otherwise - */ - @RequiresApi(api = Build.VERSION_CODES.M) - private boolean initCipher() { - - try { - mCipher = Cipher.getInstance( - KeyProperties.KEY_ALGORITHM_AES + "/" - + KeyProperties.BLOCK_MODE_CBC + "/" - + KeyProperties.ENCRYPTION_PADDING_PKCS7); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - Timber.e(e, "Error while generating and saving the encryption key"); - } - - try { - mKeyStore.load(null); - // Initialize the cipher with the key stored in the Keystore container - SecretKey key = (SecretKey) mKeyStore.getKey(KEY_NAME, null); - mCipher.init(Cipher.ENCRYPT_MODE, key); - return true; - } catch (KeyPermanentlyInvalidatedException e) { - Timber.e(e, "Key permanently invalidated while initializing the cipher"); - return false; - } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | - NoSuchAlgorithmException | InvalidKeyException e) { - Timber.e(e, "Failed while initializing the cipher"); - return false; - } - } -} \ No newline at end of file diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt new file mode 100644 index 00000000000..cd8424f9642 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt @@ -0,0 +1,216 @@ +/** + * ownCloud Android client application + * + * @author David González Verdugo + * @author Juan Carlos Garrote Gascón + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.ui.security + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyPermanentlyInvalidatedException +import android.security.keystore.KeyProperties +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.PromptInfo +import com.owncloud.android.R +import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl +import com.owncloud.android.presentation.ui.security.PassCodeManager.isPassCodeEnabled +import com.owncloud.android.presentation.ui.security.PatternManager.isPatternEnabled +import timber.log.Timber +import java.security.KeyStore +import java.security.KeyStoreException +import java.util.concurrent.Executor +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +class BiometricActivity : AppCompatActivity() { + + private lateinit var keyStore: KeyStore + private lateinit var keyGenerator: KeyGenerator + private lateinit var cipher: Cipher + private lateinit var cryptoObject: BiometricPrompt.CryptoObject + private lateinit var activity: BiometricActivity + private val handler = Handler() + private val executor = Executor { command -> handler.post(command) } + + /** + * Initializes the activity. + *

+ * An intent with a valid ACTION is expected; if none is found, an + * [IllegalArgumentException] will be thrown. + * + * @param savedInstanceState Previously saved state - irrelevant in this case + */ + @RequiresApi(api = Build.VERSION_CODES.M) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + activity = this + + generateAndStoreKey() + + if (initCipher()) { + cryptoObject = BiometricPrompt.CryptoObject(cipher) + } + + val biometricManager = BiometricManager.from(this) + if (biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) { + showBiometricPrompt() + } else { + authError() + } + } + + /** + * Generate encryption key involved in biometric authentication process and store it securely on the device using + * the Android Keystore system + */ + @RequiresApi(api = Build.VERSION_CODES.M) + private fun generateAndStoreKey() { + try { + // Access Android Keystore container, used to safely store cryptographic keys on Android devices + keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) + } catch (e: KeyStoreException) { + Timber.e(e, "Failed while getting KeyStore instance") + } + try { + // Access Android KeyGenerator to create the encryption key involved in biometric authentication process + keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) + } catch (e: Exception) { + Timber.e(e, "Failed while getting KeyGenerator instance") + } + try { + // Generate and save the encryption key + keyStore.load(null) + keyGenerator.init( + KeyGenParameterSpec.Builder(KEY_NAME, + KeyProperties.PURPOSE_ENCRYPT or + KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setUserAuthenticationRequired(true) + .setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_PKCS7) + .build() + ) + keyGenerator.generateKey() + } catch (e: Exception) { + Timber.e(e, "Failed while generating and saving the encryption key") + } + } + + /** + * Init cipher that will be used to create the encrypted [BiometricPrompt.CryptoObject] instance. This + * CryptoObject will be used during the biometric authentication process + * + * @return true if cipher is properly initialized, false otherwise + */ + @RequiresApi(api = Build.VERSION_CODES.M) + private fun initCipher(): Boolean { + try { + cipher = Cipher.getInstance( + KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7) + } catch (e: Exception) { + Timber.e(e, "Error while generating and saving the encryption key") + } + + try { + keyStore.load(null) + // Initialize the cipher with the key stored in the Keystore container + val key = keyStore.getKey(KEY_NAME, null) as SecretKey + cipher.init(Cipher.ENCRYPT_MODE, key) + return true + } catch (e: KeyPermanentlyInvalidatedException) { + Timber.e(e, "Key permanently invalidated while initializing the cipher") + return false + } catch (e: Exception) { + Timber.e(e, "Failed while initializing the cipher") + return false + } + } + + private fun showBiometricPrompt() { + val promptInfo = PromptInfo.Builder() + .setTitle(getString(R.string.biometric_prompt_title)) + .setSubtitle(getString(R.string.biometric_prompt_subtitle)) + .setNegativeButtonText(getString(android.R.string.cancel)) + .setConfirmationRequired(true) + .build() + val biometricPrompt = BiometricPrompt(this@BiometricActivity, + executor, object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Timber.e("onAuthenticationError ($errorCode): $errString") + authError() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + val preferencesProvider: SharedPreferencesProvider = SharedPreferencesProviderImpl(applicationContext) + // In this line, null is only provisional until the rearchitecture of BiometricActivity + val passCode = preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, null) + var passCodeDigits = applicationContext.resources.getInteger(R.integer.passcode_digits) + if (passCodeDigits < PassCodeActivity.PASSCODE_MIN_LENGTH) passCodeDigits = PassCodeActivity.PASSCODE_MIN_LENGTH + if (passCode != null && passCode.length < passCodeDigits) { + preferencesProvider.removePreference(PassCodeActivity.PREFERENCE_PASSCODE) + preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) + val intent = Intent(baseContext, PassCodeActivity::class.java) + intent.action = PassCodeActivity.ACTION_REQUEST_WITH_RESULT + intent.putExtra(PassCodeActivity.EXTRAS_MIGRATION, true) + startActivity(intent) + } + preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) + activity.finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Timber.e("onAuthenticationFailed") + } + }) + + // Displays the "log in" prompt. + biometricPrompt.authenticate(promptInfo, cryptoObject) + } + + private fun authError() { + if (isPassCodeEnabled()) { + PassCodeManager.onBiometricCancelled(activity) + } else if (isPatternEnabled()) { + PatternManager.onBiometricCancelled(activity) + } + + activity.finish() + } + + companion object { + const val PREFERENCE_SET_BIOMETRIC = "set_biometric" + + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val KEY_NAME = "default_key" + } +} \ No newline at end of file From 0a66d4c521ceb646b26dde46703b754d7720a9f8 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Thu, 30 Sep 2021 10:24:34 +0200 Subject: [PATCH 15/27] Kotlinized some more aspects in BiometricActivity --- .../android/presentation/ui/security/BiometricActivity.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt index cd8424f9642..9bbf24419f4 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt @@ -138,18 +138,18 @@ class BiometricActivity : AppCompatActivity() { Timber.e(e, "Error while generating and saving the encryption key") } - try { + return try { keyStore.load(null) // Initialize the cipher with the key stored in the Keystore container val key = keyStore.getKey(KEY_NAME, null) as SecretKey cipher.init(Cipher.ENCRYPT_MODE, key) - return true + true } catch (e: KeyPermanentlyInvalidatedException) { Timber.e(e, "Key permanently invalidated while initializing the cipher") - return false + false } catch (e: Exception) { Timber.e(e, "Failed while initializing the cipher") - return false + false } } From 24bf40758ccb7959fc4936a7c03d037cc1449bd5 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Mon, 4 Oct 2021 12:41:14 +0200 Subject: [PATCH 16/27] BiometricViewModel created --- .../dependecyinjection/ViewModelModule.kt | 2 + .../ui/security/BiometricActivity.kt | 104 ++------------ .../viewmodels/security/BiometricViewModel.kt | 127 ++++++++++++++++++ 3 files changed, 141 insertions(+), 92 deletions(-) create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index 0881e0442d8..1b109104e83 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -25,6 +25,7 @@ import com.owncloud.android.presentation.viewmodels.authentication.OCAuthenticat import com.owncloud.android.presentation.viewmodels.capabilities.OCCapabilityViewModel import com.owncloud.android.presentation.viewmodels.drawer.DrawerViewModel import com.owncloud.android.presentation.viewmodels.oauth.OAuthViewModel +import com.owncloud.android.presentation.viewmodels.security.BiometricViewModel import com.owncloud.android.presentation.viewmodels.security.PassCodeViewModel import com.owncloud.android.presentation.viewmodels.security.PatternViewModel import com.owncloud.android.presentation.viewmodels.settings.SettingsLogsViewModel @@ -61,4 +62,5 @@ val viewModelModule = module { viewModel { RemoveAccountDialogViewModel(get(), get(), get(), get()) } viewModel { PassCodeViewModel(get(), get()) } viewModel { PatternViewModel(get()) } + viewModel { BiometricViewModel(get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt index 9bbf24419f4..921ba3d78b4 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt @@ -24,9 +24,6 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.os.Handler -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyPermanentlyInvalidatedException -import android.security.keystore.KeyProperties import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager @@ -36,21 +33,16 @@ import androidx.biometric.BiometricPrompt.PromptInfo import com.owncloud.android.R import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl -import com.owncloud.android.presentation.ui.security.PassCodeManager.isPassCodeEnabled -import com.owncloud.android.presentation.ui.security.PatternManager.isPatternEnabled +import com.owncloud.android.presentation.viewmodels.security.BiometricViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber -import java.security.KeyStore -import java.security.KeyStoreException import java.util.concurrent.Executor -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey class BiometricActivity : AppCompatActivity() { - private lateinit var keyStore: KeyStore - private lateinit var keyGenerator: KeyGenerator - private lateinit var cipher: Cipher + // ViewModel + private val biometricViewModel by viewModel() + private lateinit var cryptoObject: BiometricPrompt.CryptoObject private lateinit var activity: BiometricActivity private val handler = Handler() @@ -70,10 +62,8 @@ class BiometricActivity : AppCompatActivity() { activity = this - generateAndStoreKey() - - if (initCipher()) { - cryptoObject = BiometricPrompt.CryptoObject(cipher) + biometricViewModel.initCipher()?.let { + cryptoObject = BiometricPrompt.CryptoObject(it) } val biometricManager = BiometricManager.from(this) @@ -84,75 +74,6 @@ class BiometricActivity : AppCompatActivity() { } } - /** - * Generate encryption key involved in biometric authentication process and store it securely on the device using - * the Android Keystore system - */ - @RequiresApi(api = Build.VERSION_CODES.M) - private fun generateAndStoreKey() { - try { - // Access Android Keystore container, used to safely store cryptographic keys on Android devices - keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) - } catch (e: KeyStoreException) { - Timber.e(e, "Failed while getting KeyStore instance") - } - try { - // Access Android KeyGenerator to create the encryption key involved in biometric authentication process - keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) - } catch (e: Exception) { - Timber.e(e, "Failed while getting KeyGenerator instance") - } - try { - // Generate and save the encryption key - keyStore.load(null) - keyGenerator.init( - KeyGenParameterSpec.Builder(KEY_NAME, - KeyProperties.PURPOSE_ENCRYPT or - KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_CBC) - .setUserAuthenticationRequired(true) - .setEncryptionPaddings( - KeyProperties.ENCRYPTION_PADDING_PKCS7) - .build() - ) - keyGenerator.generateKey() - } catch (e: Exception) { - Timber.e(e, "Failed while generating and saving the encryption key") - } - } - - /** - * Init cipher that will be used to create the encrypted [BiometricPrompt.CryptoObject] instance. This - * CryptoObject will be used during the biometric authentication process - * - * @return true if cipher is properly initialized, false otherwise - */ - @RequiresApi(api = Build.VERSION_CODES.M) - private fun initCipher(): Boolean { - try { - cipher = Cipher.getInstance( - KeyProperties.KEY_ALGORITHM_AES + "/" - + KeyProperties.BLOCK_MODE_CBC + "/" - + KeyProperties.ENCRYPTION_PADDING_PKCS7) - } catch (e: Exception) { - Timber.e(e, "Error while generating and saving the encryption key") - } - - return try { - keyStore.load(null) - // Initialize the cipher with the key stored in the Keystore container - val key = keyStore.getKey(KEY_NAME, null) as SecretKey - cipher.init(Cipher.ENCRYPT_MODE, key) - true - } catch (e: KeyPermanentlyInvalidatedException) { - Timber.e(e, "Key permanently invalidated while initializing the cipher") - false - } catch (e: Exception) { - Timber.e(e, "Failed while initializing the cipher") - false - } - } - private fun showBiometricPrompt() { val promptInfo = PromptInfo.Builder() .setTitle(getString(R.string.biometric_prompt_title)) @@ -183,7 +104,7 @@ class BiometricActivity : AppCompatActivity() { intent.putExtra(PassCodeActivity.EXTRAS_MIGRATION, true) startActivity(intent) } - preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) + biometricViewModel.setLastUnlockTimestamp() activity.finish() } @@ -198,9 +119,9 @@ class BiometricActivity : AppCompatActivity() { } private fun authError() { - if (isPassCodeEnabled()) { + if (PassCodeManager.isPassCodeEnabled()) { PassCodeManager.onBiometricCancelled(activity) - } else if (isPatternEnabled()) { + } else if (PatternManager.isPatternEnabled()) { PatternManager.onBiometricCancelled(activity) } @@ -210,7 +131,6 @@ class BiometricActivity : AppCompatActivity() { companion object { const val PREFERENCE_SET_BIOMETRIC = "set_biometric" - private const val ANDROID_KEY_STORE = "AndroidKeyStore" - private const val KEY_NAME = "default_key" + const val KEY_NAME = "default_key" } -} \ No newline at end of file +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt new file mode 100644 index 00000000000..2ff94a866fd --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt @@ -0,0 +1,127 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.viewmodels.security + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyPermanentlyInvalidatedException +import android.security.keystore.KeyProperties +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricPrompt +import androidx.lifecycle.ViewModel +import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.presentation.ui.security.BiometricActivity +import com.owncloud.android.presentation.ui.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP +import timber.log.Timber +import java.security.KeyStore +import java.security.KeyStoreException +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +class BiometricViewModel( + private val preferencesProvider: SharedPreferencesProvider +) : ViewModel() { + + private lateinit var keyStore: KeyStore + private lateinit var keyGenerator: KeyGenerator + private lateinit var cipher: Cipher + + /** + * Init cipher that will be used to create the encrypted [BiometricPrompt.CryptoObject] instance. This + * CryptoObject will be used during the biometric authentication process + * + * @return the cipher if it is properly initialized, null otherwise + */ + @RequiresApi(api = Build.VERSION_CODES.M) + fun initCipher(): Cipher? { + generateAndStoreKey() + + try { + cipher = Cipher.getInstance( + KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7) + } catch (e: Exception) { + Timber.e(e, "Error while generating and saving the encryption key") + } + + return try { + keyStore.load(null) + // Initialize the cipher with the key stored in the Keystore container + val key = keyStore.getKey(BiometricActivity.KEY_NAME, null) as SecretKey + cipher.init(Cipher.ENCRYPT_MODE, key) + cipher + } catch (e: KeyPermanentlyInvalidatedException) { + Timber.e(e, "Key permanently invalidated while initializing the cipher") + null + } catch (e: Exception) { + Timber.e(e, "Failed while initializing the cipher") + null + } + } + + fun setLastUnlockTimestamp() { + preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) + } + + /** + * Generate encryption key involved in biometric authentication process and store it securely on the device using + * the Android Keystore system + */ + @RequiresApi(api = Build.VERSION_CODES.M) + private fun generateAndStoreKey() { + try { + // Access Android Keystore container, used to safely store cryptographic keys on Android devices + keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) + } catch (e: KeyStoreException) { + Timber.e(e, "Failed while getting KeyStore instance") + } + try { + // Access Android KeyGenerator to create the encryption key involved in biometric authentication process + keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) + } catch (e: Exception) { + Timber.e(e, "Failed while getting KeyGenerator instance") + } + try { + // Generate and save the encryption key + keyStore.load(null) + keyGenerator.init( + KeyGenParameterSpec.Builder( + BiometricActivity.KEY_NAME, + KeyProperties.PURPOSE_ENCRYPT or + KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setUserAuthenticationRequired(true) + .setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_PKCS7) + .build() + ) + keyGenerator.generateKey() + } catch (e: Exception) { + Timber.e(e, "Failed while generating and saving the encryption key") + } + } + + companion object { + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + } +} From 376a08a1780854a45b363ffbb475a556335e7201 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Mon, 4 Oct 2021 13:19:10 +0200 Subject: [PATCH 17/27] Unit tests created --- .../security/BiometricViewModelTest.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt new file mode 100644 index 00000000000..bda00c7b04b --- /dev/null +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt @@ -0,0 +1,51 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.viewmodels.security + +import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.presentation.ui.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP +import com.owncloud.android.presentation.viewmodels.ViewModelTest +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class BiometricViewModelTest : ViewModelTest() { + private lateinit var biometricViewModel: BiometricViewModel + private lateinit var preferencesProvider: SharedPreferencesProvider + + @Before + fun setUp() { + preferencesProvider = mockk(relaxUnitFun = true) + biometricViewModel = BiometricViewModel(preferencesProvider) + } + + @Test + fun `set last unlock timestamp - ok`() { + biometricViewModel.setLastUnlockTimestamp() + + verify(exactly = 1) { + preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, any()) + } + } +} From b50544b8ed7dfebf82e055d6ee90812a582816f3 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 5 Oct 2021 09:09:06 +0200 Subject: [PATCH 18/27] Kotlinized BiometricManager --- .../ui/security/BiometricManager.java | 129 ------------------ .../ui/security/BiometricManager.kt | 94 +++++++++++++ 2 files changed, 94 insertions(+), 129 deletions(-) delete mode 100755 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.java create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.java deleted file mode 100755 index 4e7e70d0f0d..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David González Verdugo - * @author Christian Schabesberger - * @author Roberto Bermejo - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2021 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.presentation.ui.security; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.PowerManager; - -import androidx.annotation.RequiresApi; -import com.owncloud.android.MainApp; -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider; -import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl; - -import java.util.HashSet; -import java.util.Set; - -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.PREFERENCE_LAST_UNLOCK_TIMESTAMP; -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.PREFERENCE_LOCK_TIMEOUT; -import static com.owncloud.android.presentation.ui.security.SecurityUtilsKt.bayPassUnlockOnce; - -@RequiresApi(api = Build.VERSION_CODES.M) -/** - * Handle biometric requests. Besides, is a facade to access some - * {@link androidx.biometric.BiometricMananager} methods - */ -public class BiometricManager { - - private static final Set sExemptOfBiometricActivites; - private int mVisibleActivitiesCounter = 0; - private androidx.biometric.BiometricManager mBiometricManager; - private final SharedPreferencesProvider preferencesProvider = new SharedPreferencesProviderImpl(MainApp.Companion.getAppContext()); - - static { - sExemptOfBiometricActivites = new HashSet<>(); - sExemptOfBiometricActivites.add(BiometricActivity.class); - // other activities may be exempted, if needed - } - - private static BiometricManager mBiometricManagerInstance = null; - - public static BiometricManager getBiometricManager(Context context) { - if (mBiometricManagerInstance == null) { - mBiometricManagerInstance = new BiometricManager(); - mBiometricManagerInstance.mBiometricManager = androidx.biometric.BiometricManager.from(context); - } - return mBiometricManagerInstance; - } - - private BiometricManager() { - } - - public void onActivityStarted(Activity activity) { - if (!sExemptOfBiometricActivites.contains(activity.getClass()) && biometricShouldBeRequested()) { - - if (isHardwareDetected() && hasEnrolledBiometric()) { - // Use biometric lock - Intent i = new Intent(MainApp.Companion.getAppContext(), BiometricActivity.class); - activity.startActivity(i); - } else if (PassCodeManager.INSTANCE.isPassCodeEnabled()) { - // Cancel biometric lock and use passcode unlock method - PassCodeManager.INSTANCE.onBiometricCancelled(activity); - mVisibleActivitiesCounter++; - } else if (PatternManager.INSTANCE.isPatternEnabled()) { - // Cancel biometric lock and use pattern unlock method - PatternManager.INSTANCE.onBiometricCancelled(activity); - mVisibleActivitiesCounter++; - } - - } - - mVisibleActivitiesCounter++; // keep it AFTER biometricShouldBeRequested was checked - } - - public void onActivityStopped(Activity activity) { - if (mVisibleActivitiesCounter > 0) { - mVisibleActivitiesCounter--; - } - bayPassUnlockOnce(); - PowerManager powerMgr = (PowerManager) activity.getSystemService(Context.POWER_SERVICE); - if (isBiometricEnabled() && powerMgr != null && !powerMgr.isScreenOn()) { - activity.moveTaskToBack(true); - } - } - - private boolean biometricShouldBeRequested() { - long lastUnlockTimestamp = preferencesProvider.getLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, 0); - int timeout = LockTimeout.valueOf(preferencesProvider.getString(PREFERENCE_LOCK_TIMEOUT, LockTimeout.IMMEDIATELY.name())).toMilliseconds(); - if (System.currentTimeMillis() - lastUnlockTimestamp > timeout && mVisibleActivitiesCounter <= 0) { - return isBiometricEnabled(); - } - return false; - } - - public boolean isBiometricEnabled() { - return (preferencesProvider.getBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, false)); - } - - public boolean isHardwareDetected() { - return mBiometricManager.canAuthenticate() != androidx.biometric.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE && - mBiometricManager.canAuthenticate() != androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE; - } - - public boolean hasEnrolledBiometric() { - return mBiometricManager.canAuthenticate() != androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED; - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt new file mode 100644 index 00000000000..6b57d501c47 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt @@ -0,0 +1,94 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.ui.security + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.PowerManager +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import com.owncloud.android.MainApp +import com.owncloud.android.MainApp.Companion.appContext +import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl +import com.owncloud.android.presentation.ui.security.PassCodeManager.isPassCodeEnabled +import com.owncloud.android.presentation.ui.security.PatternManager.isPatternEnabled + +object BiometricManager { + + private val exemptOfBiometricActivities: MutableSet> = mutableSetOf(BiometricActivity::class.java) + private var visibleActivitiesCounter = 0 + private val preferencesProvider = SharedPreferencesProviderImpl(appContext) + private val biometricManager: BiometricManager = BiometricManager.from(appContext) + + fun onActivityStarted(activity: Activity) { + if (!exemptOfBiometricActivities.contains(activity.javaClass) && biometricShouldBeRequested()) { + + if (isHardwareDetected() && hasEnrolledBiometric()) { + // Use biometric lock + val i = Intent(appContext, BiometricActivity::class.java) + activity.startActivity(i) + } else if (isPassCodeEnabled()) { + // Cancel biometric lock and use passcode unlock method + PassCodeManager.onBiometricCancelled(activity) + visibleActivitiesCounter++ + } else if (isPatternEnabled()) { + // Cancel biometric lock and use pattern unlock method + PatternManager.onBiometricCancelled(activity) + visibleActivitiesCounter++ + } + + } + + visibleActivitiesCounter++ // keep it AFTER biometricShouldBeRequested was checked + + } + + fun onActivityStopped(activity: Activity) { + if (visibleActivitiesCounter > 0) visibleActivitiesCounter-- + + bayPassUnlockOnce() + val powerMgr = activity.getSystemService(Context.POWER_SERVICE) as PowerManager + if (isBiometricEnabled() && !powerMgr.isScreenOn) { + activity.moveTaskToBack(true) + } + } + + private fun biometricShouldBeRequested(): Boolean { + val lastUnlockTimestamp = preferencesProvider.getLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, 0) + val timeout = LockTimeout.valueOf(preferencesProvider.getString(PREFERENCE_LOCK_TIMEOUT, LockTimeout.IMMEDIATELY.name)!!).toMilliseconds() + return if (System.currentTimeMillis() - lastUnlockTimestamp > timeout && visibleActivitiesCounter <= 0) isBiometricEnabled() + else false + } + + fun isBiometricEnabled(): Boolean { + return preferencesProvider.getBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, false) + } + + fun isHardwareDetected(): Boolean { + return biometricManager.canAuthenticate(BIOMETRIC_WEAK) != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE && + biometricManager.canAuthenticate(BIOMETRIC_WEAK) != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE + } + + fun hasEnrolledBiometric(): Boolean { + return biometricManager.canAuthenticate(BIOMETRIC_WEAK) != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + } +} From d9635c6a9440fb8fef67df03f54f31695dfe4b0a Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 5 Oct 2021 09:34:12 +0200 Subject: [PATCH 19/27] Adapted invocations to BiometricManager from other classes --- .../security/SettingsSecurityFragmentTest.kt | 15 +++++---------- .../src/main/java/com/owncloud/android/MainApp.kt | 4 ++-- .../presentation/ui/security/BiometricManager.kt | 1 - .../presentation/ui/security/PassCodeManager.kt | 2 +- .../presentation/ui/security/PatternManager.kt | 2 +- .../fragments/SettingsSecurityFragment.kt | 8 ++------ 6 files changed, 11 insertions(+), 21 deletions(-) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt index c0bd353f3b6..0bbe98852b4 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt @@ -75,8 +75,6 @@ class SettingsSecurityFragmentTest { private lateinit var prefLockApplication: ListPreference private lateinit var prefTouchesWithOtherVisibleWindows: CheckBoxPreference - private lateinit var biometricManager: BiometricManager - private lateinit var securityViewModel: SettingsSecurityViewModel private lateinit var passCodeViewModel: PassCodeViewModel private lateinit var context: Context @@ -87,9 +85,6 @@ class SettingsSecurityFragmentTest { securityViewModel = mockk(relaxUnitFun = true) passCodeViewModel = mockk(relaxUnitFun = true) mockkStatic(BiometricManager::class) - biometricManager = mockk(relaxUnitFun = true) - - every { BiometricManager.getBiometricManager(any()) } returns biometricManager stopKoin() @@ -120,7 +115,7 @@ class SettingsSecurityFragmentTest { } private fun launchTest(withBiometrics: Boolean = true) { - every { biometricManager.isHardwareDetected } returns withBiometrics + every { BiometricManager.isHardwareDetected() } returns withBiometrics fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud) fragmentScenario.onFragment { fragment -> @@ -320,7 +315,7 @@ class SettingsSecurityFragmentTest { @Test fun enableBiometricLockWithPasscodeEnabled() { - every { biometricManager.hasEnrolledBiometric() } returns true + every { BiometricManager.hasEnrolledBiometric() } returns true launchTest() @@ -331,7 +326,7 @@ class SettingsSecurityFragmentTest { @Test fun enableBiometricLockWithPatternEnabled() { - every { biometricManager.hasEnrolledBiometric() } returns true + every { BiometricManager.hasEnrolledBiometric() } returns true launchTest() @@ -342,7 +337,7 @@ class SettingsSecurityFragmentTest { @Test fun enableBiometricLockNoEnrolledBiometric() { - every { biometricManager.hasEnrolledBiometric() } returns false + every { BiometricManager.hasEnrolledBiometric() } returns false launchTest() @@ -354,7 +349,7 @@ class SettingsSecurityFragmentTest { @Test fun disableBiometricLock() { - every { biometricManager.hasEnrolledBiometric() } returns true + every { BiometricManager.hasEnrolledBiometric() } returns true launchTest() diff --git a/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt index 1fc4eae2fd3..82f9ee15d2d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt @@ -118,7 +118,7 @@ class MainApp : Application() { PassCodeManager.onActivityStarted(activity) PatternManager.onActivityStarted(activity) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - BiometricManager.getBiometricManager(activity).onActivityStarted(activity) + BiometricManager.onActivityStarted(activity) } } @@ -135,7 +135,7 @@ class MainApp : Application() { PassCodeManager.onActivityStopped(activity) PatternManager.onActivityStopped(activity) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - BiometricManager.getBiometricManager(activity).onActivityStopped(activity) + BiometricManager.onActivityStopped(activity) } if (activity is PassCodeActivity || activity is PatternActivity || diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt index 6b57d501c47..d1715ae482c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricManager.kt @@ -26,7 +26,6 @@ import android.content.Intent import android.os.PowerManager import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK -import com.owncloud.android.MainApp import com.owncloud.android.MainApp.Companion.appContext import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl import com.owncloud.android.presentation.ui.security.PassCodeManager.isPassCodeEnabled diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt index 469292ad338..25f154aaa8a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeManager.kt @@ -38,7 +38,7 @@ object PassCodeManager { if (!exemptOfPasscodeActivities.contains(activity.javaClass) && passCodeShouldBeRequested()) { // Do not ask for passcode if biometric is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.getBiometricManager(activity).isBiometricEnabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.isBiometricEnabled()) { visibleActivitiesCounter++ return } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt index ca27ade173c..3f205bcf282 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternManager.kt @@ -38,7 +38,7 @@ object PatternManager { if (!exemptOfPatternActivities.contains(activity.javaClass) && patternShouldBeRequested()) { // Do not ask for pattern if biometric is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.getBiometricManager(activity).isBiometricEnabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.isBiometricEnabled()) { visibleActivitiesCounter++ return } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt index e8a73a3325a..e4d7c787bbc 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsSecurityFragment.kt @@ -55,8 +55,6 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() { private var prefLockApplication: ListPreference? = null private var prefTouchesWithOtherVisibleWindows: CheckBoxPreference? = null - private var biometricManager: BiometricManager? = null - private val enablePasscodeLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult @@ -164,9 +162,7 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { screenSecurity?.removePreference(prefBiometric) } else if (prefBiometric != null) { - biometricManager = BiometricManager.getBiometricManager(activity) - - if (biometricManager?.isHardwareDetected == false) { // Biometric not supported + if (!BiometricManager.isHardwareDetected()) { // Biometric not supported screenSecurity?.removePreference(prefBiometric) } else { if (prefPasscode?.isChecked == false && prefPattern?.isChecked == false) { // Disable biometric lock if Passcode or Pattern locks are disabled @@ -177,7 +173,7 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() { val incomingValue = newValue as Boolean // No biometric enrolled yet - if (incomingValue && biometricManager?.hasEnrolledBiometric() == false) { + if (incomingValue && !BiometricManager.hasEnrolledBiometric()) { showMessageInSnackbar(getString(R.string.biometric_not_enrolled)) return@setOnPreferenceChangeListener false } From dbaac15e98750988c63425ca4e157f7d24a7e383 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 5 Oct 2021 09:41:41 +0200 Subject: [PATCH 20/27] Fixed some lint errors --- .../presentation/viewmodels/security/BiometricViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt index 2ff94a866fd..fb41ff36e63 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt @@ -57,9 +57,9 @@ class BiometricViewModel( try { cipher = Cipher.getInstance( - KeyProperties.KEY_ALGORITHM_AES + "/" - + KeyProperties.BLOCK_MODE_CBC + "/" - + KeyProperties.ENCRYPTION_PADDING_PKCS7) + KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7) } catch (e: Exception) { Timber.e(e, "Error while generating and saving the encryption key") } From c2ab5d061b5579106260da0f9cd8ca48982577a6 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 5 Oct 2021 10:53:51 +0200 Subject: [PATCH 21/27] Fixed tests which use BiometricManager --- .../settings/security/SettingsSecurityFragmentTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt index 0bbe98852b4..35a00a70290 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt @@ -51,7 +51,7 @@ import com.owncloud.android.utils.matchers.verifyPreference import com.owncloud.android.utils.mockIntent import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic +import io.mockk.mockkObject import org.hamcrest.Matchers.not import org.junit.After import org.junit.Assert.assertFalse @@ -83,8 +83,7 @@ class SettingsSecurityFragmentTest { fun setUp() { context = InstrumentationRegistry.getInstrumentation().targetContext securityViewModel = mockk(relaxUnitFun = true) - passCodeViewModel = mockk(relaxUnitFun = true) - mockkStatic(BiometricManager::class) + mockkObject(BiometricManager) stopKoin() From a4324cc3ad305aa9e829467943f1a735b813c7bb Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 5 Oct 2021 13:15:02 +0200 Subject: [PATCH 22/27] Fixed tests after rebase --- .../android/settings/security/SettingsSecurityFragmentTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt index 35a00a70290..cfe69ed0a37 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt @@ -83,6 +83,7 @@ class SettingsSecurityFragmentTest { fun setUp() { context = InstrumentationRegistry.getInstrumentation().targetContext securityViewModel = mockk(relaxUnitFun = true) + passCodeViewModel = mockk(relaxUnitFun = true) mockkObject(BiometricManager) stopKoin() From 12baf045a92c4485f2645b716684e01107570df3 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 5 Oct 2021 13:50:40 +0200 Subject: [PATCH 23/27] Moved some logic to BiometricVM after rebase --- .../dependecyinjection/ViewModelModule.kt | 2 +- .../ui/security/BiometricActivity.kt | 10 +-- .../viewmodels/security/BiometricViewModel.kt | 26 +++++++- .../security/BiometricViewModelTest.kt | 63 ++++++++++++++++++- 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index 1b109104e83..544422fd12b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -62,5 +62,5 @@ val viewModelModule = module { viewModel { RemoveAccountDialogViewModel(get(), get(), get(), get()) } viewModel { PassCodeViewModel(get(), get()) } viewModel { PatternViewModel(get()) } - viewModel { BiometricViewModel(get()) } + viewModel { BiometricViewModel(get(), get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt index 921ba3d78b4..80540fcb35a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt @@ -91,14 +91,8 @@ class BiometricActivity : AppCompatActivity() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) - val preferencesProvider: SharedPreferencesProvider = SharedPreferencesProviderImpl(applicationContext) - // In this line, null is only provisional until the rearchitecture of BiometricActivity - val passCode = preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, null) - var passCodeDigits = applicationContext.resources.getInteger(R.integer.passcode_digits) - if (passCodeDigits < PassCodeActivity.PASSCODE_MIN_LENGTH) passCodeDigits = PassCodeActivity.PASSCODE_MIN_LENGTH - if (passCode != null && passCode.length < passCodeDigits) { - preferencesProvider.removePreference(PassCodeActivity.PREFERENCE_PASSCODE) - preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) + if (biometricViewModel.shouldAskForNewPassCode()) { + biometricViewModel.removePassCode() val intent = Intent(baseContext, PassCodeActivity::class.java) intent.action = PassCodeActivity.ACTION_REQUEST_WITH_RESULT intent.putExtra(PassCodeActivity.EXTRAS_MIGRATION, true) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt index fb41ff36e63..4da9122dbcf 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModel.kt @@ -27,9 +27,12 @@ import android.security.keystore.KeyProperties import androidx.annotation.RequiresApi import androidx.biometric.BiometricPrompt import androidx.lifecycle.ViewModel +import com.owncloud.android.R import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider import com.owncloud.android.presentation.ui.security.BiometricActivity import com.owncloud.android.presentation.ui.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP +import com.owncloud.android.presentation.ui.security.PassCodeActivity +import com.owncloud.android.providers.ContextProvider import timber.log.Timber import java.security.KeyStore import java.security.KeyStoreException @@ -38,7 +41,8 @@ import javax.crypto.KeyGenerator import javax.crypto.SecretKey class BiometricViewModel( - private val preferencesProvider: SharedPreferencesProvider + private val preferencesProvider: SharedPreferencesProvider, + private val contextProvider: ContextProvider ) : ViewModel() { private lateinit var keyStore: KeyStore @@ -83,6 +87,17 @@ class BiometricViewModel( preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) } + fun shouldAskForNewPassCode(): Boolean { + val passCode = preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, loadPinFromOldFormatIfPossible()) + val passCodeDigits = maxOf(contextProvider.getInt(R.integer.passcode_digits), PassCodeActivity.PASSCODE_MIN_LENGTH) + return (passCode != null && passCode.length < passCodeDigits) + } + + fun removePassCode() { + preferencesProvider.removePreference(PassCodeActivity.PREFERENCE_PASSCODE) + preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) + } + /** * Generate encryption key involved in biometric authentication process and store it securely on the device using * the Android Keystore system @@ -121,6 +136,15 @@ class BiometricViewModel( } } + private fun loadPinFromOldFormatIfPossible(): String? { + var pinString = "" + for (i in 1..4) { + val pinChar = preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE_D + i, null) + pinChar?.let { pinString += pinChar } + } + return if (pinString.isEmpty()) null else pinString + } + companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" } diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt index bda00c7b04b..7b6d62fec08 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt @@ -20,11 +20,18 @@ package com.owncloud.android.presentation.viewmodels.security +import com.owncloud.android.R import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider import com.owncloud.android.presentation.ui.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP +import com.owncloud.android.presentation.ui.security.PassCodeActivity import com.owncloud.android.presentation.viewmodels.ViewModelTest +import com.owncloud.android.providers.ContextProvider +import com.owncloud.android.testutil.security.OC_PASSCODE_4_DIGITS +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Test @@ -33,11 +40,13 @@ import org.junit.Test class BiometricViewModelTest : ViewModelTest() { private lateinit var biometricViewModel: BiometricViewModel private lateinit var preferencesProvider: SharedPreferencesProvider + private lateinit var contextProvider: ContextProvider @Before fun setUp() { preferencesProvider = mockk(relaxUnitFun = true) - biometricViewModel = BiometricViewModel(preferencesProvider) + contextProvider = mockk(relaxUnitFun = true) + biometricViewModel = BiometricViewModel(preferencesProvider, contextProvider) } @Test @@ -48,4 +57,56 @@ class BiometricViewModelTest : ViewModelTest() { preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, any()) } } + + @Test + fun `should ask for new passcode - ok - true`() { + every { preferencesProvider.getString(any(), any()) } returns OC_PASSCODE_4_DIGITS + every { contextProvider.getInt(any()) } returns 6 + + val shouldAsk = biometricViewModel.shouldAskForNewPassCode() + assertTrue(shouldAsk) + + verify(exactly = 1) { + preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, any()) + contextProvider.getInt(R.integer.passcode_digits) + } + } + + @Test + fun `should ask for new passcode - ok - false`() { + every { preferencesProvider.getString(any(), any()) } returns OC_PASSCODE_4_DIGITS + every { contextProvider.getInt(any()) } returns 4 + + val shouldAsk = biometricViewModel.shouldAskForNewPassCode() + assertFalse(shouldAsk) + + verify(exactly = 1) { + preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, any()) + contextProvider.getInt(R.integer.passcode_digits) + } + } + + @Test + fun `should ask for new passcode - ko - passcode is null`() { + every { preferencesProvider.getString(any(), any()) } returns null + every { contextProvider.getInt(any()) } returns 4 + + val shouldAsk = biometricViewModel.shouldAskForNewPassCode() + assertFalse(shouldAsk) + + verify(exactly = 1) { + preferencesProvider.getString(PassCodeActivity.PREFERENCE_PASSCODE, any()) + contextProvider.getInt(R.integer.passcode_digits) + } + } + + @Test + fun `remove passcode - ok`() { + biometricViewModel.removePassCode() + + verify(exactly = 1) { + preferencesProvider.removePreference(PassCodeActivity.PREFERENCE_PASSCODE) + preferencesProvider.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) + } + } } From 474d3a441d0f33ca666bae99c33486b403b5cb55 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Tue, 5 Oct 2021 13:55:08 +0200 Subject: [PATCH 24/27] Removed unused imports --- .../android/presentation/ui/security/BiometricActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt index 80540fcb35a..5673eb97b29 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt @@ -31,8 +31,6 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt.PromptInfo import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl import com.owncloud.android.presentation.viewmodels.security.BiometricViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber From fa44b0497f490037d6bfc38f5670420053089e80 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Thu, 7 Oct 2021 12:44:00 +0200 Subject: [PATCH 25/27] Requested changes --- .../settings/security/PassCodeActivityTest.kt | 40 ++-- .../settings/security/PatternActivityTest.kt | 26 +-- .../android/extensions/FragmentExt.kt | 11 - .../ui/security/BiometricActivity.kt | 11 +- .../ui/security/PassCodeActivity.kt | 207 ++++++++++-------- .../ui/security/PatternActivity.kt | 186 +++++++++------- .../ui/fragment/OCFileListFragment.java | 4 +- 7 files changed, 259 insertions(+), 226 deletions(-) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt index 0280f5c6e2f..0ae44202785 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt @@ -23,23 +23,23 @@ package com.owncloud.android.settings.security import android.app.Activity +import android.content.Context import android.content.Intent import android.preference.PreferenceManager +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ActivityTestRule import com.owncloud.android.R import com.owncloud.android.presentation.ui.security.PassCodeActivity import com.owncloud.android.utils.matchers.isDisplayed import com.owncloud.android.utils.matchers.withText import org.junit.After import org.junit.Assert.assertTrue -import org.junit.Rule import org.junit.Test import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -59,13 +59,10 @@ import withChildViewCount class PassCodeActivityTest { - @Rule - @JvmField - val activityRule = ActivityTestRule(PassCodeActivity::class.java, true, false) + private lateinit var activityScenario: ActivityScenario - private val intent = Intent() - private val errorMessage = "PassCode Activity error" - private val context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var intent: Intent + private lateinit var context: Context private val defaultPassCode = arrayOf('1', '1', '1', '1', '1', '1') private val wrongPassCode = arrayOf('1', '1', '1', '2', '2', '2') @@ -74,6 +71,7 @@ class PassCodeActivityTest { @Before fun setUp() { + context = ApplicationProvider.getApplicationContext() passCodeViewModel = mockk(relaxUnitFun = true) stopKoin() @@ -130,7 +128,7 @@ class PassCodeActivityTest { onView(withId(R.id.cancel)).perform(click()) - assertEquals(activityRule.activityResult.resultCode, Activity.RESULT_CANCELED) + assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @Test @@ -159,9 +157,11 @@ class PassCodeActivityTest { typePasscode(defaultPassCode) //Checking that the result returned is OK - assertEquals(activityRule.activityResult.resultCode, Activity.RESULT_OK) + assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK) - assertTrue(errorMessage, activityRule.activity.isFinishing) + activityScenario.onActivity { + assertTrue(it.isFinishing) + } } @Test @@ -199,7 +199,7 @@ class PassCodeActivityTest { } onView(withId(R.id.cancel)).perform(click()) - assertTrue(errorMessage, activityRule.activity.isFinishing) + assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @Test @@ -216,7 +216,7 @@ class PassCodeActivityTest { } onView(withId(R.id.cancel)).perform(click()) - assertTrue(errorMessage, activityRule.activity.isFinishing) + assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @Test @@ -240,7 +240,7 @@ class PassCodeActivityTest { onView(withId(R.id.cancel)).perform(click()) - assertEquals(activityRule.activityResult.resultCode, Activity.RESULT_CANCELED) + assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @Test @@ -256,7 +256,9 @@ class PassCodeActivityTest { //Type correct passcode typePasscode(defaultPassCode) - assertTrue(errorMessage, activityRule.activity.isFinishing) + activityScenario.onActivity { + assertTrue(it.isFinishing) + } } @Test @@ -283,8 +285,10 @@ class PassCodeActivityTest { } private fun openPasscodeActivity(mode: String) { - intent.action = mode - activityRule.launchActivity(intent) + intent = Intent(context, PassCodeActivity::class.java).apply { + action = mode + } + activityScenario = ActivityScenario.launch(intent) } private fun typePasscode(digits: Array) { diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt index cf9b1300401..abdeeeea592 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt @@ -21,22 +21,23 @@ package com.owncloud.android.settings.security +import android.content.Context import android.content.Intent import android.preference.PreferenceManager +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ActivityTestRule import com.owncloud.android.R import com.owncloud.android.presentation.ui.security.PatternActivity import com.owncloud.android.presentation.viewmodels.security.PatternViewModel +import com.owncloud.android.testutil.security.OC_PATTERN import io.mockk.mockk import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.startKoin @@ -45,19 +46,16 @@ import org.koin.dsl.module class PatternActivityTest { - @Rule - @JvmField - val activityRule = ActivityTestRule(PatternActivity::class.java, true, false) + private lateinit var activityScenario: ActivityScenario - private val intent = Intent() - private val context = InstrumentationRegistry.getInstrumentation().targetContext - - private val patternToSave = "1234" + private lateinit var intent: Intent + private lateinit var context: Context private lateinit var patternViewModel: PatternViewModel @Before fun setUp() { + context = ApplicationProvider.getApplicationContext() patternViewModel = mockk(relaxUnitFun = true) stopKoin() @@ -106,14 +104,16 @@ class PatternActivityTest { private fun storePattern() { val appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit() appPrefs.apply { - putString(PatternActivity.PREFERENCE_PATTERN, patternToSave) + putString(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN) putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true) apply() } } private fun openPatternActivity(mode: String) { - intent.action = mode - activityRule.launchActivity(intent) + intent = Intent(context, PatternActivity::class.java).apply { + action = mode + } + activityScenario = ActivityScenario.launch(intent) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt index 0300ab015b2..b4324da810d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt @@ -54,15 +54,4 @@ fun Fragment.showAlertDialog( .setPositiveButton(positiveButtonText, positiveButtonListener) .setNegativeButton(negativeButtonText, negativeButtonListener) .show() -} - -fun Fragment.hideSoftKeyboard() { - val focusedView = requireActivity().currentFocus - focusedView?.let { - val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow( - focusedView.windowToken, - 0 - ) - } } \ No newline at end of file diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt index 5673eb97b29..34ac36d12d3 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/BiometricActivity.kt @@ -42,7 +42,6 @@ class BiometricActivity : AppCompatActivity() { private val biometricViewModel by viewModel() private lateinit var cryptoObject: BiometricPrompt.CryptoObject - private lateinit var activity: BiometricActivity private val handler = Handler() private val executor = Executor { command -> handler.post(command) } @@ -58,8 +57,6 @@ class BiometricActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - activity = this - biometricViewModel.initCipher()?.let { cryptoObject = BiometricPrompt.CryptoObject(it) } @@ -97,7 +94,7 @@ class BiometricActivity : AppCompatActivity() { startActivity(intent) } biometricViewModel.setLastUnlockTimestamp() - activity.finish() + finish() } override fun onAuthenticationFailed() { @@ -112,12 +109,12 @@ class BiometricActivity : AppCompatActivity() { private fun authError() { if (PassCodeManager.isPassCodeEnabled()) { - PassCodeManager.onBiometricCancelled(activity) + PassCodeManager.onBiometricCancelled(this) } else if (PatternManager.isPatternEnabled()) { - PatternManager.onBiometricCancelled(activity) + PatternManager.onBiometricCancelled(this) } - activity.finish() + finish() } companion object { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt index 1defa88b63b..54f3d2e5f4e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PassCodeActivity.kt @@ -99,41 +99,47 @@ class PassCodeActivity : AppCompatActivity() { inflatePasscodeTxtLine() - if (ACTION_CHECK == intent.action) { - /// this is a pass code request; the user has to input the right value - passCodeHdr.text = getString(R.string.pass_code_enter_pass_code) - passCodeHdrExplanation.visibility = View.INVISIBLE - setCancelButtonEnabled(false) // no option to cancel - } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { - if (savedInstanceState != null) { - confirmingPassCode = savedInstanceState.getBoolean(KEY_CONFIRMING_PASSCODE) - passCodeDigits = savedInstanceState.getStringArray(KEY_PASSCODE_DIGITS)!! + when (intent.action) { + ACTION_CHECK -> { + /// this is a pass code request; the user has to input the right value + passCodeHdr.text = getString(R.string.pass_code_enter_pass_code) + passCodeHdrExplanation.visibility = View.INVISIBLE + setCancelButtonEnabled(false) // no option to cancel } - if (confirmingPassCode) { - //the app was in the passcodeconfirmation - requestPassCodeConfirmation() - } else { - if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) { - passCodeHdr.text = getString(R.string.pass_code_configure_your_pass_code_migration, passCodeViewModel.getNumberOfPassCodeDigits()) + ACTION_REQUEST_WITH_RESULT -> { + if (savedInstanceState != null) { + confirmingPassCode = savedInstanceState.getBoolean(KEY_CONFIRMING_PASSCODE) + passCodeDigits = savedInstanceState.getStringArray(KEY_PASSCODE_DIGITS)!! + } + if (confirmingPassCode) { + //the app was in the passcodeconfirmation + requestPassCodeConfirmation() } else { - /// pass code preference has just been activated in Preferences; - // will receive and confirm pass code value - passCodeHdr.text = getString(R.string.pass_code_configure_your_pass_code) + if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) { + passCodeHdr.text = + getString(R.string.pass_code_configure_your_pass_code_migration, passCodeViewModel.getNumberOfPassCodeDigits()) + } else { + /// pass code preference has just been activated in Preferences; + // will receive and confirm pass code value + passCodeHdr.text = getString(R.string.pass_code_configure_your_pass_code) + } + //mPassCodeHdr.setText(R.string.pass_code_enter_pass_code); + // TODO choose a header, check iOS + passCodeHdrExplanation.visibility = View.VISIBLE + if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) setCancelButtonEnabled(false) + else setCancelButtonEnabled(true) } - //mPassCodeHdr.setText(R.string.pass_code_enter_pass_code); - // TODO choose a header, check iOS - passCodeHdrExplanation.visibility = View.VISIBLE - if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) setCancelButtonEnabled(false) - else setCancelButtonEnabled(true) } - } else if (ACTION_CHECK_WITH_RESULT == intent.action) { - /// pass code preference has just been disabled in Preferences; - // will confirm user knows pass code, then remove it - passCodeHdr.text = getString(R.string.pass_code_remove_your_pass_code) - passCodeHdrExplanation.visibility = View.INVISIBLE - setCancelButtonEnabled(true) - } else { - throw IllegalArgumentException(R.string.illegal_argument_exception_message.toString() + " ") + ACTION_CHECK_WITH_RESULT -> { + /// pass code preference has just been disabled in Preferences; + // will confirm user knows pass code, then remove it + passCodeHdr.text = getString(R.string.pass_code_remove_your_pass_code) + passCodeHdrExplanation.visibility = View.INVISIBLE + setCancelButtonEnabled(true) + } + else -> { + throw IllegalArgumentException(R.string.illegal_argument_exception_message.toString() + " ") + } } setTextListeners() @@ -159,7 +165,7 @@ class PassCodeActivity : AppCompatActivity() { * * @param enabled 'True' makes the cancel button available, 'false' hides it. */ - protected fun setCancelButtonEnabled(enabled: Boolean) { + private fun setCancelButtonEnabled(enabled: Boolean) { if (enabled) { bCancel.visibility = View.VISIBLE bCancel.setOnClickListener { finish() } @@ -172,7 +178,7 @@ class PassCodeActivity : AppCompatActivity() { /** * Binds the appropiate listeners to the input boxes receiving each digit of the pass code. */ - protected fun setTextListeners() { + private fun setTextListeners() { val numberOfPasscodeDigits = (passCodeViewModel.getPassCode()?.length ?: passCodeViewModel.getNumberOfPassCodeDigits()) for (i in 0 until numberOfPasscodeDigits) { passCodeEditTexts[i]?.addTextChangedListener(PassCodeDigitTextWatcher(i, i == numberOfPasscodeDigits - 1)) @@ -215,66 +221,85 @@ class PassCodeActivity : AppCompatActivity() { * the previously typed pass code, if any. */ private fun processFullPassCode() { - if (ACTION_CHECK == intent.action) { - if (passCodeViewModel.checkPassCodeIsValid(passCodeDigits)) { - /// pass code accepted in request, user is allowed to access the app - passCodeError.visibility = View.INVISIBLE - val preferencesProvider = SharedPreferencesProviderImpl(applicationContext) - preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) - hideSoftKeyboard() - val passCode = passCodeViewModel.getPassCode() - if (passCode != null && passCode.length < passCodeViewModel.getNumberOfPassCodeDigits()) { - passCodeViewModel.setMigrationRequired(true) - passCodeViewModel.removePassCode() - val intent = Intent(baseContext, PassCodeActivity::class.java) - intent.apply { - action = ACTION_REQUEST_WITH_RESULT - flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP - putExtra(EXTRAS_MIGRATION, true) - } - startActivity(intent) - } - finish() - } else { - showErrorAndRestart( - R.string.pass_code_wrong, getString(R.string.pass_code_enter_pass_code), - View.INVISIBLE - ) + when (intent.action) { + ACTION_CHECK -> { + handleActionCheck() } - } else if (ACTION_CHECK_WITH_RESULT == intent.action) { - if (passCodeViewModel.checkPassCodeIsValid(passCodeDigits)) { - passCodeViewModel.removePassCode() - val resultIntent = Intent() - setResult(RESULT_OK, resultIntent) - passCodeError.visibility = View.INVISIBLE - hideSoftKeyboard() - notifyDocumentProviderRoots(applicationContext) - finish() - } else { - showErrorAndRestart( - R.string.pass_code_wrong, getString(R.string.pass_code_enter_pass_code), - View.INVISIBLE - ) + ACTION_CHECK_WITH_RESULT -> { + handleActionCheckWithResult() } - } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { - // enabling pass code - if (!confirmingPassCode) { - passCodeError.visibility = View.INVISIBLE - requestPassCodeConfirmation() - } else if (confirmPassCode()) { - // confirmed: user typed the same pass code twice - if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) passCodeViewModel.setMigrationRequired(false) - savePassCodeAndExit() - } else { - val headerMessage = if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) getString(R.string.pass_code_configure_your_pass_code_migration, passCodeViewModel.getNumberOfPassCodeDigits()) - else getString(R.string.pass_code_configure_your_pass_code) - showErrorAndRestart( - R.string.pass_code_mismatch, headerMessage, View.VISIBLE - ) + ACTION_REQUEST_WITH_RESULT -> { + handleActionRequestWithResult() } } } + private fun handleActionCheck() { + if (passCodeViewModel.checkPassCodeIsValid(passCodeDigits)) { + /// pass code accepted in request, user is allowed to access the app + passCodeError.visibility = View.INVISIBLE + val preferencesProvider = SharedPreferencesProviderImpl(applicationContext) + preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) + hideSoftKeyboard() + val passCode = passCodeViewModel.getPassCode() + if (passCode != null && passCode.length < passCodeViewModel.getNumberOfPassCodeDigits()) { + passCodeViewModel.setMigrationRequired(true) + passCodeViewModel.removePassCode() + val intent = Intent(baseContext, PassCodeActivity::class.java) + intent.apply { + action = ACTION_REQUEST_WITH_RESULT + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP + putExtra(EXTRAS_MIGRATION, true) + } + startActivity(intent) + } + finish() + } else { + showErrorAndRestart( + errorMessage = R.string.pass_code_wrong, headerMessage = getString(R.string.pass_code_enter_pass_code), + explanationVisibility = View.INVISIBLE + ) + } + } + + private fun handleActionCheckWithResult() { + if (passCodeViewModel.checkPassCodeIsValid(passCodeDigits)) { + passCodeViewModel.removePassCode() + val resultIntent = Intent() + setResult(RESULT_OK, resultIntent) + passCodeError.visibility = View.INVISIBLE + hideSoftKeyboard() + notifyDocumentProviderRoots(applicationContext) + finish() + } else { + showErrorAndRestart( + errorMessage = R.string.pass_code_wrong, headerMessage = getString(R.string.pass_code_enter_pass_code), + explanationVisibility = View.INVISIBLE + ) + } + } + + private fun handleActionRequestWithResult() { + // enabling pass code + if (!confirmingPassCode) { + passCodeError.visibility = View.INVISIBLE + requestPassCodeConfirmation() + } else if (confirmPassCode()) { + // confirmed: user typed the same pass code twice + if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) passCodeViewModel.setMigrationRequired(false) + savePassCodeAndExit() + } else { + val headerMessage = if (intent.extras?.getBoolean(EXTRAS_MIGRATION) == true) getString( + R.string.pass_code_configure_your_pass_code_migration, + passCodeViewModel.getNumberOfPassCodeDigits() + ) + else getString(R.string.pass_code_configure_your_pass_code) + showErrorAndRestart( + errorMessage = R.string.pass_code_mismatch, headerMessage = headerMessage, explanationVisibility = View.VISIBLE + ) + } + } + private fun showErrorAndRestart( errorMessage: Int, headerMessage: String, explanationVisibility: Int @@ -291,7 +316,7 @@ class PassCodeActivity : AppCompatActivity() { * Ask to the user for retyping the pass code just entered before saving it as the current pass * code. */ - protected fun requestPassCodeConfirmation() { + private fun requestPassCodeConfirmation() { clearBoxes() passCodeHdr.setText(R.string.pass_code_reenter_your_pass_code) passCodeHdrExplanation.visibility = View.INVISIBLE @@ -304,7 +329,7 @@ class PassCodeActivity : AppCompatActivity() { * * @return 'True' if retyped pass code equals to the entered before. */ - protected fun confirmPassCode(): Boolean { + private fun confirmPassCode(): Boolean { confirmingPassCode = false var isValid = true var i = 0 @@ -318,7 +343,7 @@ class PassCodeActivity : AppCompatActivity() { /** * Sets the input fields to empty strings and puts the focus on the first one. */ - protected fun clearBoxes() { + private fun clearBoxes() { for (passCodeEditText in passCodeEditTexts) { passCodeEditText?.apply { isEnabled = true @@ -351,7 +376,7 @@ class PassCodeActivity : AppCompatActivity() { /** * Saves the pass code input by the user as the current pass code. */ - protected fun savePassCodeAndExit() { + private fun savePassCodeAndExit() { val resultIntent = Intent() val passCodeString = StringBuilder() for (i in 0 until passCodeViewModel.getNumberOfPassCodeDigits()) { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt index e91faf1f936..cb4d1cdd7eb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/security/PatternActivity.kt @@ -32,6 +32,7 @@ import android.widget.Button import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible import com.andrognito.patternlockview.PatternLockView import com.andrognito.patternlockview.PatternLockView.Dot import com.andrognito.patternlockview.listener.PatternLockViewListener @@ -89,45 +90,50 @@ class PatternActivity : AppCompatActivity() { */ var patternExpShouldVisible = false - if (ACTION_CHECK == intent.action) { - /** - * This block is executed when the user opens the app after setting the pattern lock - * this block takes the pattern input by the user and checks it with the pattern initially set by the user. - */ - patternHeader.text = getString(R.string.pattern_enter_pattern) - patternExplanation.visibility = View.INVISIBLE - setCancelButtonEnabled(false) - } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { - /** - * This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) - */ - var patternHeaderViewText = "" - if (savedInstanceState != null) { - confirmingPattern = savedInstanceState.getBoolean(KEY_CONFIRMING_PATTERN) - patternValue = savedInstanceState.getString(KEY_PATTERN_STRING) - patternHeaderViewText = savedInstanceState.getString(PATTERN_HEADER_VIEW_TEXT)!! - patternExpShouldVisible = savedInstanceState.getBoolean(PATTERN_EXP_VIEW_STATE) + when (intent.action) { + ACTION_CHECK -> { + /** + * This block is executed when the user opens the app after setting the pattern lock + * this block takes the pattern input by the user and checks it with the pattern initially set by the user. + */ + patternHeader.text = getString(R.string.pattern_enter_pattern) + patternExplanation.visibility = View.INVISIBLE + setCancelButtonEnabled(false) } - if (confirmingPattern) { - patternHeader.text = patternHeaderViewText - if (!patternExpShouldVisible) { - patternExplanation.visibility = View.INVISIBLE + ACTION_REQUEST_WITH_RESULT -> { + /** + * This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) + */ + var patternHeaderViewText = "" + if (savedInstanceState != null) { + confirmingPattern = savedInstanceState.getBoolean(KEY_CONFIRMING_PATTERN) + patternValue = savedInstanceState.getString(KEY_PATTERN_STRING) + patternHeaderViewText = savedInstanceState.getString(PATTERN_HEADER_VIEW_TEXT)!! + patternExpShouldVisible = savedInstanceState.getBoolean(PATTERN_EXP_VIEW_STATE) } - } else { - patternHeader.text = getString(R.string.pattern_configure_pattern) + if (confirmingPattern) { + patternHeader.text = patternHeaderViewText + if (!patternExpShouldVisible) { + patternExplanation.visibility = View.INVISIBLE + } + } else { + patternHeader.text = getString(R.string.pattern_configure_pattern) + patternExplanation.visibility = View.VISIBLE + setCancelButtonEnabled(true) + } + } + ACTION_CHECK_WITH_RESULT -> { + /** + * This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) + */ + patternHeader.text = getString(R.string.pattern_remove_pattern) + patternExplanation.text = getString(R.string.pattern_no_longer_required) patternExplanation.visibility = View.VISIBLE setCancelButtonEnabled(true) } - } else if (ACTION_CHECK_WITH_RESULT == intent.action) { - /** - * This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) - */ - patternHeader.text = getString(R.string.pattern_remove_pattern) - patternExplanation.text = getString(R.string.pattern_no_longer_required) - patternExplanation.visibility = View.VISIBLE - setCancelButtonEnabled(true) - } else { - throw IllegalArgumentException(R.string.illegal_argument_exception_message.toString() + " ") + else -> { + throw IllegalArgumentException(R.string.illegal_argument_exception_message.toString() + " ") + } } setPatternListener() @@ -139,7 +145,7 @@ class PatternActivity : AppCompatActivity() { * * @param enabled 'True' makes the cancel button available, 'false' hides it. */ - protected fun setCancelButtonEnabled(enabled: Boolean) { + private fun setCancelButtonEnabled(enabled: Boolean) { if (enabled) { bCancel.visibility = View.VISIBLE bCancel.setOnClickListener { finish() } @@ -152,7 +158,7 @@ class PatternActivity : AppCompatActivity() { /** * Binds the appropriate listener to the pattern view. */ - protected fun setPatternListener() { + private fun setPatternListener() { patternLockView.addPatternLockListener(object : PatternLockViewListener { override fun onStarted() { Timber.d("Pattern Drawing Started") @@ -192,53 +198,69 @@ class PatternActivity : AppCompatActivity() { } private fun processPattern() { - if (ACTION_CHECK == intent.action) { - /** - * This block is executed when the user opens the app after setting the pattern lock - * this block takes the pattern input by the user and checks it with the pattern initially set by the user. - */ - if (patternViewModel.checkPatternIsValid(patternValue)) { - patternError.visibility = View.INVISIBLE - val preferencesProvider = SharedPreferencesProviderImpl(applicationContext) - preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) - finish() - } else { - showErrorAndRestart( - R.string.pattern_incorrect_pattern, - R.string.pattern_enter_pattern, View.INVISIBLE - ) + when (intent.action) { + ACTION_CHECK -> { + /** + * This block is executed when the user opens the app after setting the pattern lock + * this block takes the pattern input by the user and checks it with the pattern initially set by the user. + */ + handleActionCheck() } - } else if (ACTION_CHECK_WITH_RESULT == intent.action) { - //This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) - if (patternViewModel.checkPatternIsValid(patternValue)) { - patternViewModel.removePattern() - val result = Intent() - setResult(RESULT_OK, result) - patternError.visibility = View.INVISIBLE - notifyDocumentProviderRoots(applicationContext) - finish() - } else { - showErrorAndRestart( - R.string.pattern_incorrect_pattern, - R.string.pattern_enter_pattern, View.INVISIBLE - ) + ACTION_CHECK_WITH_RESULT -> { + //This block is executed when the user is removing the pattern lock (i.e disabling the pattern lock) + handleActionCheckWithResult() } - } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { - //This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) - if (!confirmingPattern) { - patternError.visibility = View.INVISIBLE - requestPatternConfirmation() - } else if (confirmPattern()) { - savePatternAndExit() - } else { - showErrorAndRestart( - R.string.pattern_not_same_pattern, - R.string.pattern_enter_pattern, View.VISIBLE - ) + ACTION_REQUEST_WITH_RESULT -> { + //This block is executed when the user is setting the pattern lock (i.e enabling the pattern lock) + handleActionRequestWithResult() } } } + private fun handleActionCheck() { + if (patternViewModel.checkPatternIsValid(patternValue)) { + patternError.visibility = View.INVISIBLE + val preferencesProvider = SharedPreferencesProviderImpl(applicationContext) + preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, System.currentTimeMillis()) + finish() + } else { + showErrorAndRestart( + errorMessage = R.string.pattern_incorrect_pattern, + headerMessage = R.string.pattern_enter_pattern, explanationVisibility = View.INVISIBLE + ) + } + } + + private fun handleActionCheckWithResult() { + if (patternViewModel.checkPatternIsValid(patternValue)) { + patternViewModel.removePattern() + val result = Intent() + setResult(RESULT_OK, result) + patternError.visibility = View.INVISIBLE + notifyDocumentProviderRoots(applicationContext) + finish() + } else { + showErrorAndRestart( + errorMessage = R.string.pattern_incorrect_pattern, + headerMessage = R.string.pattern_enter_pattern, explanationVisibility = View.INVISIBLE + ) + } + } + + private fun handleActionRequestWithResult() { + if (!confirmingPattern) { + patternError.visibility = View.INVISIBLE + requestPatternConfirmation() + } else if (confirmPattern()) { + savePatternAndExit() + } else { + showErrorAndRestart( + errorMessage = R.string.pattern_not_same_pattern, + headerMessage = R.string.pattern_enter_pattern, explanationVisibility = View.VISIBLE + ) + } + } + private fun showErrorAndRestart( errorMessage: Int, headerMessage: Int, explanationVisibility: Int @@ -253,14 +275,14 @@ class PatternActivity : AppCompatActivity() { /** * Ask to the user to re-enter the pattern just entered before saving it as the current pattern. */ - protected fun requestPatternConfirmation() { + private fun requestPatternConfirmation() { patternLockView.clearPattern() patternHeader.setText(R.string.pattern_reenter_pattern) patternExplanation.visibility = View.INVISIBLE confirmingPattern = true } - protected fun confirmPattern(): Boolean { + private fun confirmPattern(): Boolean { confirmingPattern = false return newPatternValue != null && newPatternValue == patternValue } @@ -279,11 +301,7 @@ class PatternActivity : AppCompatActivity() { putBoolean(KEY_CONFIRMING_PATTERN, confirmingPattern) putString(KEY_PATTERN_STRING, patternValue) putString(PATTERN_HEADER_VIEW_TEXT, patternHeader.text.toString()) - if (patternExplanation.visibility == View.VISIBLE) { - putBoolean(PATTERN_EXP_VIEW_STATE, true) - } else { - putBoolean(PATTERN_EXP_VIEW_STATE, false) - } + putBoolean(PATTERN_EXP_VIEW_STATE, patternExplanation.isVisible) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 55411cbb859..9e013b4c420 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -61,7 +61,7 @@ import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.extensions.FragmentExtKt; +import com.owncloud.android.extensions.ActivityExtKt; import com.owncloud.android.files.FileMenuFilter; import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.presentation.ui.common.BottomSheetFragmentItemView; @@ -693,7 +693,7 @@ void loadStateFrom(Bundle savedInstanceState) { } private void clearLocalSearchView() { - FragmentExtKt.hideSoftKeyboard(this); + ActivityExtKt.hideSoftKeyboard(requireActivity()); mFileListAdapter.clearFilterBySearch(); if (mSearchView != null) { mSearchView.onActionViewCollapsed(); From 6aba9fcac1e71f545df44b9ef494a2ceded49ac3 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Thu, 7 Oct 2021 12:49:03 +0200 Subject: [PATCH 26/27] Removed unused imports --- .../main/java/com/owncloud/android/extensions/FragmentExt.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt index b4324da810d..cfb4d45f6e4 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt @@ -20,9 +20,7 @@ package com.owncloud.android.extensions import android.app.AlertDialog -import android.content.Context import android.content.DialogInterface -import android.view.inputmethod.InputMethodManager import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar From b8d74d3baed9a9cd57e6d7f0df4bdf60583cc65d Mon Sep 17 00:00:00 2001 From: Juan Carlos Garrote Date: Thu, 7 Oct 2021 13:53:52 +0200 Subject: [PATCH 27/27] Some improvements in tests --- .../authentication/LoginActivityTest.kt | 14 ++++---- .../settings/security/PassCodeActivityTest.kt | 26 +++++++------- .../settings/security/PatternActivityTest.kt | 34 ++++++++++++------- .../com/owncloud/android/utils/ActionsExt.kt | 6 +++- .../android/utils/matchers/MatcherExt.kt | 6 ++++ 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt index 24824c7fa94..46c77b27d3c 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt @@ -63,7 +63,7 @@ import com.owncloud.android.testutil.OC_AUTH_TOKEN_TYPE import com.owncloud.android.testutil.OC_BASIC_PASSWORD import com.owncloud.android.testutil.OC_BASIC_USERNAME import com.owncloud.android.testutil.OC_SERVER_INFO -import com.owncloud.android.utils.click +import com.owncloud.android.utils.scrollAndClick import com.owncloud.android.utils.matchers.assertVisibility import com.owncloud.android.utils.matchers.isDisplayed import com.owncloud.android.utils.matchers.isEnabled @@ -189,7 +189,7 @@ class LoginActivityTest { verify(exactly = 1) { ocAuthenticationViewModel.getServerInfo(OC_SERVER_INFO.baseUrl) } - R.id.centeredRefreshButton.click() + R.id.centeredRefreshButton.scrollAndClick() verify(exactly = 2) { ocAuthenticationViewModel.getServerInfo(OC_SERVER_INFO.baseUrl) } } @@ -232,7 +232,7 @@ class LoginActivityTest { R.id.hostUrlInput.typeText(OC_SERVER_INFO.baseUrl) - R.id.embeddedCheckServerButton.click() + R.id.embeddedCheckServerButton.scrollAndClick() verify(exactly = 1) { ocAuthenticationViewModel.getServerInfo(OC_SERVER_INFO.baseUrl) } } @@ -242,7 +242,7 @@ class LoginActivityTest { launchTest() R.id.hostUrlInput.typeText(OC_SERVER_INFO.baseUrl) - R.id.thumbnail.click() + R.id.thumbnail.scrollAndClick() verify(exactly = 1) { ocAuthenticationViewModel.getServerInfo(OC_SERVER_INFO.baseUrl) } } @@ -365,7 +365,7 @@ class LoginActivityTest { R.id.hostUrlInput.typeText("") - R.id.embeddedCheckServerButton.click() + R.id.embeddedCheckServerButton.scrollAndClick() with(R.id.server_status_text) { isDisplayed(true) @@ -427,7 +427,7 @@ class LoginActivityTest { with(R.id.loginButton) { isDisplayed(true) - click() + scrollAndClick() } verify(exactly = 1) { ocAuthenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) } @@ -445,7 +445,7 @@ class LoginActivityTest { with(R.id.loginButton) { isDisplayed(true) - click() + scrollAndClick() } verify(exactly = 1) { ocAuthenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) } diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt index 0ae44202785..43d65e2f765 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt @@ -29,7 +29,6 @@ import android.preference.PreferenceManager import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.withId @@ -41,11 +40,11 @@ import com.owncloud.android.utils.matchers.withText import org.junit.After import org.junit.Assert.assertTrue import org.junit.Test -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.owncloud.android.presentation.viewmodels.security.PassCodeViewModel import com.owncloud.android.testutil.security.OC_PASSCODE_4_DIGITS import com.owncloud.android.testutil.security.OC_PASSCODE_6_DIGITS +import com.owncloud.android.utils.click +import com.owncloud.android.utils.matchers.withChildCountAndId import io.mockk.every import io.mockk.mockk import nthChildOf @@ -55,13 +54,11 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.dsl.module -import withChildViewCount class PassCodeActivityTest { private lateinit var activityScenario: ActivityScenario - private lateinit var intent: Intent private lateinit var context: Context private val defaultPassCode = arrayOf('1', '1', '1', '1', '1', '1') @@ -112,8 +109,10 @@ class PassCodeActivityTest { } // Check if required amount of input fields are actually displayed - onView(withId(R.id.passCodeTxtLayout)).check(matches(isDisplayed())) - onView(withId(R.id.passCodeTxtLayout)).check(matches(withChildViewCount(passCodeViewModel.getNumberOfPassCodeDigits(), withId(R.id.passCodeEditText)))) + with(R.id.passCodeTxtLayout) { + isDisplayed(true) + withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText) + } with(R.id.cancel) { isDisplayed(true) @@ -126,7 +125,7 @@ class PassCodeActivityTest { //Open Activity in passcode creation mode openPasscodeActivity(PassCodeActivity.ACTION_REQUEST_WITH_RESULT) - onView(withId(R.id.cancel)).perform(click()) + R.id.cancel.click() assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @@ -198,7 +197,7 @@ class PassCodeActivityTest { onView(nthChildOf(withId(R.id.passCodeTxtLayout), i)).perform(replaceText("1")) } - onView(withId(R.id.cancel)).perform(click()) + R.id.cancel.click() assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @@ -210,12 +209,12 @@ class PassCodeActivityTest { //First typing typePasscode(defaultPassCode) - //Type incorrect passcode + //Type incomplete passcode for (i in 0..1) { onView(nthChildOf(withId(R.id.passCodeTxtLayout), i)).perform(replaceText("1")) } - onView(withId(R.id.cancel)).perform(click()) + R.id.cancel.click() assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @@ -238,8 +237,7 @@ class PassCodeActivityTest { //Open Activity in passcode deletion mode openPasscodeActivity(PassCodeActivity.ACTION_CHECK_WITH_RESULT) - onView(withId(R.id.cancel)).perform(click()) - + R.id.cancel.click() assertEquals(activityScenario.result.resultCode, Activity.RESULT_CANCELED) } @@ -285,7 +283,7 @@ class PassCodeActivityTest { } private fun openPasscodeActivity(mode: String) { - intent = Intent(context, PassCodeActivity::class.java).apply { + val intent = Intent(context, PassCodeActivity::class.java).apply { action = mode } activityScenario = ActivityScenario.launch(intent) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt index abdeeeea592..6281db47644 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt @@ -26,15 +26,12 @@ import android.content.Intent import android.preference.PreferenceManager import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import com.owncloud.android.R import com.owncloud.android.presentation.ui.security.PatternActivity import com.owncloud.android.presentation.viewmodels.security.PatternViewModel import com.owncloud.android.testutil.security.OC_PATTERN +import com.owncloud.android.utils.matchers.isDisplayed +import com.owncloud.android.utils.matchers.withText import io.mockk.mockk import org.junit.After import org.junit.Before @@ -48,7 +45,6 @@ class PatternActivityTest { private lateinit var activityScenario: ActivityScenario - private lateinit var intent: Intent private lateinit var context: Context private lateinit var patternViewModel: PatternViewModel @@ -83,10 +79,16 @@ class PatternActivityTest { //Open Activity in pattern creation mode openPatternActivity(PatternActivity.ACTION_REQUEST_WITH_RESULT) - onView(withText(R.string.pattern_configure_pattern)).check(matches(isDisplayed())) - onView(withText(R.string.pattern_configure_your_pattern_explanation)).check(matches(isDisplayed())) - onView(withId(R.id.pattern_lock_view)).check(matches(isDisplayed())) - onView(withId(R.id.cancel_pattern)).check(matches(isDisplayed())) + with(R.id.header_pattern) { + isDisplayed(true) + withText(R.string.pattern_configure_pattern) + } + with(R.id.explanation_pattern) { + isDisplayed(true) + withText(R.string.pattern_configure_your_pattern_explanation) + } + R.id.pattern_lock_view.isDisplayed(true) + R.id.cancel_pattern.isDisplayed(true) } @Test @@ -97,8 +99,14 @@ class PatternActivityTest { //Open Activity in pattern deletion mode openPatternActivity(PatternActivity.ACTION_CHECK_WITH_RESULT) - onView(withText(R.string.pattern_remove_pattern)).check(matches(isDisplayed())) - onView(withText(R.string.pattern_no_longer_required)).check(matches(isDisplayed())) + with(R.id.header_pattern) { + isDisplayed(true) + withText(R.string.pattern_remove_pattern) + } + with(R.id.explanation_pattern) { + isDisplayed(true) + withText(R.string.pattern_no_longer_required) + } } private fun storePattern() { @@ -111,7 +119,7 @@ class PatternActivityTest { } private fun openPatternActivity(mode: String) { - intent = Intent(context, PatternActivity::class.java).apply { + val intent = Intent(context, PatternActivity::class.java).apply { action = mode } activityScenario = ActivityScenario.launch(intent) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ActionsExt.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ActionsExt.kt index a3fabbe7730..1ccaeeb8b23 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ActionsExt.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ActionsExt.kt @@ -33,6 +33,10 @@ fun Int.replaceText(text: String) { onView(withId(this)).perform(scrollTo(), ViewActions.replaceText(text)) } -fun Int.click() { +fun Int.scrollAndClick() { onView(withId(this)).perform(scrollTo(), ViewActions.click()) } + +fun Int.click() { + onView(withId(this)).perform(ViewActions.click()) +} diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/MatcherExt.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/MatcherExt.kt index 27206ca8517..86dc54a6ecf 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/MatcherExt.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/MatcherExt.kt @@ -25,6 +25,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId import org.hamcrest.CoreMatchers +import withChildViewCount fun Int.isDisplayed(displayed: Boolean) { val displayMatcher = if (displayed) ViewMatchers.isDisplayed() else CoreMatchers.not(ViewMatchers.isDisplayed()) @@ -62,6 +63,11 @@ fun Int.withTextColor(resourceId: Int) { .check(matches(ViewMatchers.hasTextColor(resourceId))) } +fun Int.withChildCountAndId(count: Int, resourceId: Int) { + onView(withId(this)) + .check(matches(withChildViewCount(count, withId(resourceId)))) +} + fun Int.assertVisibility(visibility: ViewMatchers.Visibility) { onView(withId(this)) .check(matches(ViewMatchers.withEffectiveVisibility(visibility)))