Sfoglia il codice sorgente

Merge pull request #9816 from nextcloud/pin

Improve PIN lockout handling
Tobias Kaminsky 3 anni fa
parent
commit
9723c0458e

+ 1 - 0
src/main/java/com/nextcloud/client/core/Clock.kt

@@ -25,5 +25,6 @@ import java.util.TimeZone
 interface Clock {
     val currentTime: Long
     val currentDate: Date
+    val millisSinceBoot: Long
     val tz: TimeZone
 }

+ 6 - 6
src/main/java/com/nextcloud/client/core/ClockImpl.kt

@@ -19,19 +19,19 @@
  */
 package com.nextcloud.client.core
 
+import android.os.SystemClock
 import java.util.Date
 import java.util.TimeZone
 
 class ClockImpl : Clock {
     override val currentTime: Long
-        get() {
-            return System.currentTimeMillis()
-        }
+        get() = System.currentTimeMillis()
 
     override val currentDate: Date
-        get() {
-            return Date(currentTime)
-        }
+        get() = Date(currentTime)
+
+    override val millisSinceBoot: Long
+        get() = SystemClock.elapsedRealtime()
 
     override val tz: TimeZone
         get() = TimeZone.getDefault()

+ 9 - 1
src/main/java/com/nextcloud/client/di/AppModule.java

@@ -51,6 +51,9 @@ import com.nextcloud.client.migrations.MigrationsManagerImpl;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.notifications.AppNotificationManager;
 import com.nextcloud.client.notifications.AppNotificationManagerImpl;
+import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.client.utils.Throttler;
+import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.UploadsStorageManager;
@@ -61,7 +64,6 @@ import com.owncloud.android.ui.activities.data.activities.RemoteActivitiesReposi
 import com.owncloud.android.ui.activities.data.files.FilesRepository;
 import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl;
 import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository;
-import com.nextcloud.client.utils.Throttler;
 
 import org.greenrobot.eventbus.EventBus;
 
@@ -237,4 +239,10 @@ class AppModule {
     Throttler throttler(Clock clock) {
         return new Throttler(clock);
     }
+
+    @Provides
+    @Singleton
+    PassCodeManager passCodeManager(AppPreferences preferences, Clock clock) {
+        return new PassCodeManager(preferences, clock);
+    }
 }

+ 2 - 3
src/main/java/com/owncloud/android/MainApp.java

@@ -21,7 +21,6 @@
  */
 package com.owncloud.android;
 
-import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -170,7 +169,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
     @Inject
     MigrationsManager migrationsManager;
 
-    private PassCodeManager passCodeManager;
+    @Inject
+    PassCodeManager passCodeManager;
 
     @SuppressWarnings("unused")
     private boolean mBound;
@@ -262,7 +262,6 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         DisplayUtils.useCompatVectorIfNeeded();
 
         fixStoragePath();
-        passCodeManager = new PassCodeManager(preferences);
 
         MainApp.storagePath = preferences.getStoragePath(getApplicationContext().getFilesDir().getAbsolutePath());
 

+ 1 - 1
src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java

@@ -239,6 +239,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     @Inject AppPreferences preferences;
     @Inject OnboardingService onboarding;
     @Inject DeviceInfo deviceInfo;
+    @Inject PassCodeManager passCodeManager;
     private boolean onlyAdd = false;
     @SuppressLint("ResourceAsColor") @ColorInt
     private int primaryColor = R.color.primary;
@@ -675,7 +676,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         onlyAdd = intent.getBooleanExtra(KEY_ONLY_ADD, false) || checkIfViaSSO(intent);
 
         // Passcode
-        PassCodeManager passCodeManager = new PassCodeManager(preferences);
         passCodeManager.onActivityStarted(this);
 
         Uri data = intent.getData();

+ 0 - 147
src/main/java/com/owncloud/android/authentication/PassCodeManager.java

@@ -1,147 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   @author David A. Velasco
- *   Copyright (C) 2015 ownCloud Inc.
- *
- *   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 <http://www.gnu.org/licenses/>.
- *
- */
-package com.owncloud.android.authentication;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.PowerManager;
-import android.os.SystemClock;
-import android.view.Window;
-import android.view.WindowManager;
-
-import com.nextcloud.client.preferences.AppPreferences;
-import com.nextcloud.client.preferences.AppPreferencesImpl;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.ui.activity.PassCodeActivity;
-import com.owncloud.android.ui.activity.RequestCredentialsActivity;
-import com.owncloud.android.ui.activity.SettingsActivity;
-import com.owncloud.android.utils.DeviceCredentialUtils;
-
-import java.util.HashSet;
-import java.util.Set;
-
-public final class PassCodeManager {
-
-    private static final Set<Class> exemptOfPasscodeActivities;
-
-    public static final int PASSCODE_ACTIVITY = 9999;
-
-    static {
-        exemptOfPasscodeActivities = new HashSet<>();
-        exemptOfPasscodeActivities.add(PassCodeActivity.class);
-        exemptOfPasscodeActivities.add(RequestCredentialsActivity.class);
-        // other activities may be exempted, if needed
-    }
-
-    /**
-     *  Keeping a "low" positive value is the easiest way to prevent
-     *  the pass code being requested on screen rotations.
-     */
-    private static final int PASS_CODE_TIMEOUT = 5000;
-
-    private AppPreferences preferences;
-    private int visibleActivitiesCounter;
-
-
-    public PassCodeManager(AppPreferences preferences) {
-        this.preferences = preferences;
-    }
-
-    private void setSecureFlag(Activity activity) {
-        Window window = activity.getWindow();
-        if (window != null) {
-            if (isPassCodeEnabled() || deviceCredentialsAreEnabled(activity)) {
-                window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-            } else {
-                window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
-            }
-        }
-    }
-
-    public boolean onActivityStarted(Activity activity) {
-        boolean askedForPin = false;
-        Long timestamp = AppPreferencesImpl.fromContext(activity).getLockTimestamp();
-
-        setSecureFlag(activity);
-
-        if (!exemptOfPasscodeActivities.contains(activity.getClass()) && passCodeShouldBeRequested(timestamp)) {
-            askedForPin = true;
-
-            preferences.setLockTimestamp(0);
-
-            Intent i = new Intent(MainApp.getAppContext(), PassCodeActivity.class);
-            i.setAction(PassCodeActivity.ACTION_CHECK);
-            i.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
-            activity.startActivityForResult(i, PASSCODE_ACTIVITY);
-        }
-
-        if (!exemptOfPasscodeActivities.contains(activity.getClass()) &&
-            deviceCredentialsShouldBeRequested(timestamp, activity)) {
-            askedForPin = true;
-
-            preferences.setLockTimestamp(0);
-
-            Intent i = new Intent(MainApp.getAppContext(), RequestCredentialsActivity.class);
-            i.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
-            activity.startActivityForResult(i, PASSCODE_ACTIVITY);
-        } else {
-            if (!askedForPin && preferences.getLockTimestamp() != 0) {
-                preferences.setLockTimestamp(SystemClock.elapsedRealtime());
-            }
-        }
-
-        visibleActivitiesCounter++;    // keep it AFTER passCodeShouldBeRequested was checked
-
-        return askedForPin;
-    }
-
-    public void onActivityStopped(Activity activity) {
-        if (visibleActivitiesCounter > 0) {
-            visibleActivitiesCounter--;
-        }
-
-        PowerManager powerMgr = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
-        if ((isPassCodeEnabled() || deviceCredentialsAreEnabled(activity)) && powerMgr != null
-                && !powerMgr.isScreenOn()) {
-            activity.moveTaskToBack(true);
-        }
-    }
-
-    private boolean passCodeShouldBeRequested(Long timestamp) {
-        return Math.abs(SystemClock.elapsedRealtime() - timestamp) > PASS_CODE_TIMEOUT &&
-            visibleActivitiesCounter <= 0 && isPassCodeEnabled();
-    }
-
-    private boolean isPassCodeEnabled() {
-        return SettingsActivity.LOCK_PASSCODE.equals(preferences.getLockPreference());
-    }
-
-    private boolean deviceCredentialsShouldBeRequested(Long timestamp, Activity activity) {
-        return Math.abs(SystemClock.elapsedRealtime() - timestamp) > PASS_CODE_TIMEOUT && visibleActivitiesCounter <= 0 &&
-            deviceCredentialsAreEnabled(activity);
-    }
-
-    private boolean deviceCredentialsAreEnabled(Activity activity) {
-        return SettingsActivity.LOCK_DEVICE_CREDENTIALS.equals(preferences.getLockPreference())
-            || (preferences.isFingerprintUnlockEnabled()
-            && DeviceCredentialUtils.areCredentialsAvailable(activity));
-    }
-}

+ 145 - 0
src/main/java/com/owncloud/android/authentication/PassCodeManager.kt

@@ -0,0 +1,145 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   @author David A. Velasco
+ *   Copyright (C) 2015 ownCloud Inc.
+ *
+ *   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 <http://www.gnu.org/licenses/>.
+ *
+ */
+package com.owncloud.android.authentication
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.PowerManager
+import android.view.WindowManager
+import com.nextcloud.client.core.Clock
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.MainApp
+import com.owncloud.android.ui.activity.PassCodeActivity
+import com.owncloud.android.ui.activity.RequestCredentialsActivity
+import com.owncloud.android.ui.activity.SettingsActivity
+import com.owncloud.android.utils.DeviceCredentialUtils
+import kotlin.math.abs
+
+@Suppress("TooManyFunctions")
+class PassCodeManager(private val preferences: AppPreferences, private val clock: Clock) {
+    companion object {
+        private val exemptOfPasscodeActivities = setOf(
+            PassCodeActivity::class.java,
+            RequestCredentialsActivity::class.java
+        )
+        const val PASSCODE_ACTIVITY = 9999
+
+        /**
+         * Keeping a "low" positive value is the easiest way to prevent
+         * the pass code being requested on screen rotations.
+         */
+        private const val PASS_CODE_TIMEOUT = 5000
+    }
+
+    private var visibleActivitiesCounter = 0
+
+    private fun isExemptActivity(activity: Activity): Boolean {
+        return exemptOfPasscodeActivities.contains(activity.javaClass)
+    }
+
+    fun onActivityStarted(activity: Activity): Boolean {
+        var askedForPin = false
+        val timestamp = preferences.lockTimestamp
+        setSecureFlag(activity)
+
+        if (!isExemptActivity(activity)) {
+            if (passCodeShouldBeRequested(timestamp)) {
+                askedForPin = true
+                preferences.lockTimestamp = 0
+                requestPasscode(activity)
+            }
+            if (deviceCredentialsShouldBeRequested(timestamp, activity)) {
+                askedForPin = true
+                preferences.lockTimestamp = 0
+                requestCredentials(activity)
+            }
+        }
+
+        if (!askedForPin && preferences.lockTimestamp != 0L) {
+            updateLockTimestamp()
+        }
+
+        if (!isExemptActivity(activity)) {
+            visibleActivitiesCounter++ // keep it AFTER passCodeShouldBeRequested was checked
+        }
+        return askedForPin
+    }
+
+    private fun setSecureFlag(activity: Activity) {
+        val window = activity.window
+        if (window != null) {
+            if (isPassCodeEnabled() || deviceCredentialsAreEnabled(activity)) {
+                window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+            } else {
+                window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+            }
+        }
+    }
+
+    private fun requestPasscode(activity: Activity) {
+        val i = Intent(MainApp.getAppContext(), PassCodeActivity::class.java)
+        i.action = PassCodeActivity.ACTION_CHECK
+        i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+        activity.startActivityForResult(i, PASSCODE_ACTIVITY)
+    }
+
+    private fun requestCredentials(activity: Activity) {
+        val i = Intent(MainApp.getAppContext(), RequestCredentialsActivity::class.java)
+        i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+        activity.startActivityForResult(i, PASSCODE_ACTIVITY)
+    }
+
+    fun onActivityStopped(activity: Activity) {
+        if (visibleActivitiesCounter > 0 && !isExemptActivity(activity)) {
+            visibleActivitiesCounter--
+        }
+        val powerMgr = activity.getSystemService(Context.POWER_SERVICE) as PowerManager
+        if ((isPassCodeEnabled() || deviceCredentialsAreEnabled(activity)) && !powerMgr?.isScreenOn) {
+            activity.moveTaskToBack(true)
+        }
+    }
+
+    fun updateLockTimestamp() {
+        preferences.lockTimestamp = clock.millisSinceBoot
+    }
+
+    /**
+     * `true` if the time elapsed since last unlock is longer than [PASS_CODE_TIMEOUT] and no activities are visible
+     */
+    private fun shouldBeLocked(timestamp: Long) =
+        abs(clock.millisSinceBoot - timestamp) > PASS_CODE_TIMEOUT &&
+            visibleActivitiesCounter <= 0
+
+    private fun passCodeShouldBeRequested(timestamp: Long): Boolean {
+        return shouldBeLocked(timestamp) && isPassCodeEnabled()
+    }
+
+    private fun isPassCodeEnabled(): Boolean = SettingsActivity.LOCK_PASSCODE == preferences.lockPreference
+
+    private fun deviceCredentialsShouldBeRequested(timestamp: Long, activity: Activity): Boolean {
+        return shouldBeLocked(timestamp) && deviceCredentialsAreEnabled(activity)
+    }
+
+    private fun deviceCredentialsAreEnabled(activity: Activity): Boolean {
+        return SettingsActivity.LOCK_DEVICE_CREDENTIALS == preferences.lockPreference ||
+            (preferences.isFingerprintUnlockEnabled && DeviceCredentialUtils.areCredentialsAvailable(activity))
+    }
+}

+ 6 - 2
src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java

@@ -41,6 +41,7 @@ import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferencesImpl;
 import com.owncloud.android.R;
+import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.databinding.PasscodelockBinding;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.utils.theme.ThemeButtonUtils;
@@ -74,6 +75,7 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
     public final static String PREFERENCE_PASSCODE_D4 = "PrefPinCode4";
 
     @Inject AppPreferences preferences;
+    @Inject PassCodeManager passCodeManager;
     private PasscodelockBinding binding;
     private final EditText[] passCodeEditTexts = new EditText[4];
     private String [] passCodeDigits = {"","","",""};
@@ -242,7 +244,7 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
                 preferences.resetPinWrongAttempts();
 
                 /// pass code accepted in request, user is allowed to access the app
-                AppPreferencesImpl.fromContext(this).setLockTimestamp(SystemClock.elapsedRealtime());
+                passCodeManager.updateLockTimestamp();
                 hideSoftKeyboard();
                 finish();
 
@@ -254,7 +256,7 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
 
         } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
             if (checkPassCode()) {
-                preferences.setLockTimestamp(SystemClock.elapsedRealtime());
+                passCodeManager.updateLockTimestamp();
                 Intent resultIntent = new Intent();
                 resultIntent.putExtra(KEY_CHECK_RESULT, true);
                 setResult(RESULT_OK, resultIntent);
@@ -392,6 +394,8 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
 
         setResult(RESULT_OK, resultIntent);
 
+        passCodeManager.updateLockTimestamp();
+
         finish();
     }