Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ buildscript {
appCompat = "1.1.0"
sqliteVersion = "2.0.1"
lifecycleLiveData = "2.0.0"
biometricVersion="1.0.1"

// Kotlin
kotlinVersion = "1.3.61"
Expand Down
1 change: 1 addition & 0 deletions owncloudApp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {
implementation "androidx.browser:browser:1.2.0"
implementation 'commons-io:commons-io:2.6'
implementation "androidx.sqlite:sqlite:$sqliteVersion"
implementation "androidx.biometric:biometric:$biometricVersion"

// Tests
testImplementation project(':owncloudTestUtil')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ class OCSettingsSecurityTest {
fun securityView() {
onView(withText(R.string.prefs_passcode)).check(matches(isDisplayed()))
onView(withText(R.string.prefs_pattern)).check(matches(isDisplayed()))
onView(withText(R.string.prefs_fingerprint)).check(matches(isDisplayed()))
onView(withText(R.string.prefs_fingerprint_summary)).check(matches(isDisplayed()))
onView(withText(R.string.prefs_fingerprint)).check(matches(not(isEnabled())))
onView(withText(R.string.prefs_biometric)).check(matches(isDisplayed()))
onView(withText(R.string.prefs_biometric_summary)).check(matches(isDisplayed()))
onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
onView(withText(R.string.prefs_touches_with_other_visible_windows)).check(matches(isDisplayed()))
onView(withText(R.string.prefs_touches_with_other_visible_windows_summary)).check(matches(isDisplayed()))
}
Expand Down Expand Up @@ -146,13 +146,13 @@ class OCSettingsSecurityTest {
@Test
fun enablePasscodeEnablesFingerprint() {
firstEnablePasscode()
onView(withText(R.string.prefs_fingerprint)).check(matches(isEnabled()))
onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
}

@Test
fun enablePatternEnablesFingerprint() {
firstEnablePattern()
onView(withText(R.string.prefs_fingerprint)).check(matches(isEnabled()))
onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
}

@Test
Expand All @@ -179,7 +179,7 @@ class OCSettingsSecurityTest {
onView(withText(R.string.prefs_passcode)).perform(click())
assertFalse(mPrefPasscode.isChecked)
onView(withText(R.string.pass_code_removed)).check(matches(isEnabled()))
onView(withText(R.string.prefs_fingerprint)).check(matches(not(isEnabled())))
onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
}

@Test
Expand All @@ -192,7 +192,7 @@ class OCSettingsSecurityTest {
onView(withText(R.string.prefs_pattern)).perform(click())
assertFalse(mPrefPattern.isChecked)
onView(withText(R.string.pattern_removed)).check(matches(isEnabled()))
onView(withText(R.string.prefs_fingerprint)).check(matches(not(isEnabled())))
onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
}

@Test
Expand Down
2 changes: 1 addition & 1 deletion owncloudApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,6 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:theme="@style/Theme.ownCloud.Fullscreen" />
<activity android:name=".ui.activity.PatternLockActivity" />
<activity android:name=".ui.activity.FingerprintActivity" />
<activity android:name=".ui.activity.BiometricActivity" />
</application>
</manifest>
20 changes: 11 additions & 9 deletions owncloudApp/src/main/java/com/owncloud/android/MainApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.preference.PreferenceManager
import android.view.WindowManager
import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication
import com.owncloud.android.authentication.FingerprintManager
import com.owncloud.android.authentication.BiometricManager
import com.owncloud.android.authentication.PassCodeManager
import com.owncloud.android.authentication.PatternManager
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.db.PreferenceManager
import com.owncloud.android.dependecyinjection.commonModule
import com.owncloud.android.dependecyinjection.localDataSourceModule
import com.owncloud.android.dependecyinjection.remoteDataSourceModule
Expand All @@ -45,7 +45,7 @@ import com.owncloud.android.dependecyinjection.viewModelModule
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.SingleSessionManager
import com.owncloud.android.lib.common.utils.LoggingHelper
import com.owncloud.android.ui.activity.FingerprintActivity
import com.owncloud.android.ui.activity.BiometricActivity
import com.owncloud.android.ui.activity.PassCodeActivity
import com.owncloud.android.ui.activity.PatternLockActivity
import com.owncloud.android.ui.activity.WhatsNewActivity
Expand Down Expand Up @@ -77,15 +77,15 @@ class MainApp : MultiDexApplication() {
// initialise thumbnails cache on background thread
ThumbnailsCacheManager.InitDiskCacheTask().execute()

// register global protection with pass code, pattern lock and fingerprint lock
// register global protection with pass code, pattern lock and biometric lock
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
Timber.d("${activity.javaClass.simpleName} onCreate(Bundle) starting")
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
val passCodeEnabled = preferences.getBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false)
val patternCodeEnabled = preferences.getBoolean(PatternLockActivity.PREFERENCE_SET_PATTERN, false)
if (!isDeveloper) {
// To enable FingerPrint you need to enable passCode or pattern, so no need to add check to if
// To enable biometric you need to enable passCode or pattern, so no need to add check to if
if (passCodeEnabled || patternCodeEnabled) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
Expand All @@ -97,18 +97,20 @@ class MainApp : MultiDexApplication() {
// have finished
if (activity !is PassCodeActivity &&
activity !is PatternLockActivity &&
activity !is FingerprintActivity
activity !is BiometricActivity
) {
WhatsNewActivity.runIfNeeded(activity)
}

PreferenceManager.migrateFingerprintToBiometricKey(applicationContext);
}

override fun onActivityStarted(activity: Activity) {
Timber.v("${activity.javaClass.simpleName} onStart() starting")
PassCodeManager.getPassCodeManager().onActivityStarted(activity)
PatternManager.getPatternManager().onActivityStarted(activity)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
FingerprintManager.getFingerprintManager(activity).onActivityStarted(activity)
BiometricManager.getBiometricManager(activity).onActivityStarted(activity)
}
}

Expand All @@ -125,11 +127,11 @@ class MainApp : MultiDexApplication() {
PassCodeManager.getPassCodeManager().onActivityStopped(activity)
PatternManager.getPatternManager().onActivityStopped(activity)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
FingerprintManager.getFingerprintManager(activity).onActivityStopped(activity)
BiometricManager.getBiometricManager(activity).onActivityStopped(activity)
}
if (activity is PassCodeActivity ||
activity is PatternLockActivity ||
activity is FingerprintActivity
activity is BiometricActivity
) {
WhatsNewActivity.runIfNeeded(activity)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* ownCloud Android client application
*
* @author David González Verdugo
* @author Christian Schabesberger
* @author Roberto Bermejo
* Copyright (C) 2019 ownCloud GmbH.
* <p>
* 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.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.owncloud.android.authentication;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.PowerManager;
import android.preference.PreferenceManager;

import androidx.annotation.RequiresApi;
import com.owncloud.android.MainApp;
import com.owncloud.android.ui.activity.BiometricActivity;

import java.util.HashSet;
import java.util.Set;

@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<Class> sExemptOfBiometricActivites;

private androidx.biometric.BiometricManager mBiometricManager;

static {
sExemptOfBiometricActivites = new HashSet<>();
sExemptOfBiometricActivites.add(BiometricActivity.class);
// other activities may be exempted, if needed
}

private static int BIOMETRIC_TIMEOUT = 1000;
// keeping a "low" positive value is the easiest way to prevent the biometric is requested on rotations

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 Long mTimestamp = 0l;
private int mVisibleActivitiesCounter = 0;

private BiometricManager() {
}

public void onActivityStarted(Activity activity) {

if (!sExemptOfBiometricActivites.contains(activity.getClass())) {

if (biometricShouldBeRequested()) {

if (isHardwareDetected() && hasEnrolledBiometric()) {
// Use biometric lock
Intent i = new Intent(MainApp.Companion.getAppContext(), BiometricActivity.class);
activity.startActivity(i);
} else if (PassCodeManager.getPassCodeManager().isPassCodeEnabled()) {
// Cancel biometric lock and use passcode unlock method
PassCodeManager.getPassCodeManager().onBiometricCancelled(activity);
mVisibleActivitiesCounter++;
} else if (PatternManager.getPatternManager().isPatternEnabled()) {
// Cancel biometric lock and use pattern unlock method
PatternManager.getPatternManager().onBiometricCancelled(activity);
mVisibleActivitiesCounter++;
}

}
}

mVisibleActivitiesCounter++; // keep it AFTER biometricShouldBeRequested was checked
}

public void onActivityStopped(Activity activity) {
if (mVisibleActivitiesCounter > 0) {
mVisibleActivitiesCounter--;
}
setUnlockTimestamp();
PowerManager powerMgr = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
if (isBiometricEnabled() && powerMgr != null && !powerMgr.isScreenOn()) {
activity.moveTaskToBack(true);
}
}

private void setUnlockTimestamp() {
mTimestamp = System.currentTimeMillis();
}

private boolean biometricShouldBeRequested() {

if ((System.currentTimeMillis() - mTimestamp) > BIOMETRIC_TIMEOUT && mVisibleActivitiesCounter <= 0) {
return isBiometricEnabled();
}

return false;
}

protected boolean isBiometricEnabled() {
SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(MainApp.Companion.getAppContext());
return (appPrefs.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;
}

/**
* This can be used for example for onActivityResult, where you don't want to re authenticate
* again.
*
* USE WITH CARE
*/
public void bayPassUnlockOnce() {
setUnlockTimestamp();
}
}
Loading