浏览代码

Merge pull request #624 from Flole998/master

Added fingerprint support
Mario Đanić 8 年之前
父节点
当前提交
9739edbbec

+ 2 - 0
src/main/AndroidManifest.xml

@@ -59,6 +59,7 @@
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
 
 
     <application
@@ -210,6 +211,7 @@
         <service android:name=".media.MediaService" />
 
         <activity android:name=".ui.activity.PassCodeActivity" />
+        <activity android:name=".ui.activity.FingerprintActivity"/>
         <activity android:name=".ui.activity.ConflictsResolveActivity"/>
         <activity android:name=".ui.activity.GenericExplanationActivity"/>
         <activity android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity"/>

+ 26 - 2
src/main/java/com/owncloud/android/authentication/PassCodeManager.java

@@ -28,6 +28,7 @@ import android.preference.PreferenceManager;
 import android.view.WindowManager;
 
 import com.owncloud.android.MainApp;
+import com.owncloud.android.ui.activity.FingerprintActivity;
 import com.owncloud.android.ui.activity.PassCodeActivity;
 
 import java.util.HashSet;
@@ -40,6 +41,7 @@ public class PassCodeManager {
     static {
         sExemptOfPasscodeActivites = new HashSet<Class>();
         sExemptOfPasscodeActivites.add(PassCodeActivity.class);
+        sExemptOfPasscodeActivites.add(FingerprintActivity.class);
         // other activities may be exempted, if needed
     }
 
@@ -61,7 +63,7 @@ public class PassCodeManager {
     protected PassCodeManager() {};
 
     public void onActivityCreated(Activity activity) {
-        if (passCodeIsEnabled()) {
+        if (passCodeIsEnabled() || fingerprintIsEnabled()) {
             activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
         } else {
             activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
@@ -80,6 +82,17 @@ public class PassCodeManager {
 
         }
 
+        if (!sExemptOfPasscodeActivites.contains(activity.getClass()) &&
+                fingerprintShouldBeRequested() && FingerprintActivity.isFingerprintReady(MainApp.getAppContext())
+                ){
+
+            Intent i = new Intent(MainApp.getAppContext(), FingerprintActivity.class);
+            i.setAction(PassCodeActivity.ACTION_CHECK);
+            i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+            activity.startActivity(i);
+
+        }
+
         mVisibleActivitiesCounter++;    // keep it AFTER passCodeShouldBeRequested was checked
     }
 
@@ -89,7 +102,7 @@ public class PassCodeManager {
         }
         setUnlockTimestamp();
         PowerManager powerMgr = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
-        if (passCodeIsEnabled() && powerMgr != null && !powerMgr.isScreenOn()) {
+        if ((passCodeIsEnabled() || fingerprintIsEnabled())&& powerMgr != null && !powerMgr.isScreenOn()) {
             activity.moveTaskToBack(true);
         }
     }
@@ -112,4 +125,15 @@ public class PassCodeManager {
         return (appPrefs.getBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false));
     }
 
+    private boolean fingerprintShouldBeRequested() {
+        if ((System.currentTimeMillis() - mTimestamp) > PASS_CODE_TIMEOUT && mVisibleActivitiesCounter <= 0) {
+            return fingerprintIsEnabled();
+        }
+        return false;
+    }
+
+    private boolean fingerprintIsEnabled() {
+        SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(MainApp.getAppContext());
+        return (appPrefs.getBoolean(FingerprintActivity.PREFERENCE_USE_FINGERPRINT, false));
+    }
 }

+ 348 - 0
src/main/java/com/owncloud/android/ui/activity/FingerprintActivity.java

@@ -0,0 +1,348 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Florian Lentz
+ *   Copyright (C) 2017 Florian Lentz
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.activity;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.KeyEvent;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+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 javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * Activity to handle access to the app based on the fingerprint.
+ */
+public class FingerprintActivity extends AppCompatActivity {
+
+    private static final String TAG = FingerprintActivity.class.getSimpleName();
+
+    public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
+
+    public final static String PREFERENCE_USE_FINGERPRINT = "use_fingerprint";
+    public static final String ANDROID_KEY_STORE = "AndroidKeyStore";
+
+    private KeyStore keyStore;
+    // Variable used for storing the key in the Android Keystore container
+    private static final String KEY_NAME = "Nextcloud";
+    private Cipher cipher;
+
+    private FingerprintHandler helper;
+
+    private FingerprintManager.CryptoObject cryptoObject;
+
+    private CancellationSignal cancellationSignal;
+
+    /**
+     * Initializes the activity.
+     *
+     * @param savedInstanceState    Previously saved state - irrelevant in this case
+     */
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.fingerprintlock);
+    }
+
+    private void startFingerprint() {
+        TextView fingerprintTextView = (TextView) findViewById(R.id.scanfingerprinttext);
+
+        FingerprintManager fingerprintManager =
+                (FingerprintManager) MainApp.getAppContext().getSystemService(Context.FINGERPRINT_SERVICE);
+
+        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.USE_FINGERPRINT)
+                != PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+        KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
+
+        if (!keyguardManager.isKeyguardSecure()) {
+            return;
+        } else {
+            generateKey();
+
+            if (cipherInit()) {
+                cryptoObject = new FingerprintManager.CryptoObject(cipher);
+                FingerprintHandler.Callback callback = new FingerprintHandler.Callback() {
+                    @Override
+                    public void onAuthenticated() {
+                        fingerprintResult(true);
+                    }
+
+                    @Override
+                    public void onFailed(String error) {
+                        Toast.makeText(MainApp.getAppContext(), error, Toast.LENGTH_LONG).show();
+                        ImageView imageView = (ImageView) findViewById(R.id.fingerprinticon);
+                        int[][] states = new int[][]{
+                                new int[]{android.R.attr.state_activated},
+                                new int[]{-android.R.attr.state_activated}
+                        };
+                        int[] colors = new int[]{Color.parseColor("#FF0000"), Color.RED};
+                        ColorStateList csl = new ColorStateList(states, colors);
+                        Drawable drawable = DrawableCompat.wrap(imageView.getDrawable());
+                        DrawableCompat.setTintList(drawable, csl);
+                        imageView.setImageDrawable(drawable);
+                    }
+                };
+
+                helper = new FingerprintHandler(fingerprintTextView, callback);
+                cancellationSignal = new CancellationSignal();
+                if (ActivityCompat.checkSelfPermission(MainApp.getAppContext(), Manifest.permission.USE_FINGERPRINT)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    return;
+                }
+                fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, helper, null);
+            }
+        }
+    }
+
+    @Override
+    public void onResume(){
+        super.onResume();
+        startFingerprint();
+        ImageView imageView = (ImageView)findViewById(R.id.fingerprinticon);
+        imageView.setImageDrawable(getDrawable(R.drawable.ic_fingerprint));
+    }
+
+    @Override
+    public void onStop(){
+        super.onStop();
+        cancellationSignal.cancel();
+    }
+
+    /**
+     * Overrides click on the BACK arrow to prevent fingerprint from being 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){
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @TargetApi(Build.VERSION_CODES.M)
+    protected void generateKey() {
+        try {
+            keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
+        } catch (Exception e) {
+            Log_OC.e(TAG, "Error getting KeyStore", e);
+        }
+
+        KeyGenerator keyGenerator;
+        try {
+            keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
+        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+            return;
+        }
+
+        try {
+            keyStore.load(null);
+            keyGenerator.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());
+            keyGenerator.generateKey();
+        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertificateException | IOException e) {
+            return;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.M)
+    public boolean cipherInit() {
+        try {
+            cipher = Cipher.getInstance(
+                    KeyProperties.KEY_ALGORITHM_AES + "/"
+                            + KeyProperties.BLOCK_MODE_CBC + "/"
+                            + KeyProperties.ENCRYPTION_PADDING_PKCS7);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            return false;
+        }
+
+        try {
+            keyStore.load(null);
+            SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, null);
+            cipher.init(Cipher.ENCRYPT_MODE, key);
+            return true;
+        } catch (KeyPermanentlyInvalidatedException e) {
+            return false;
+        } catch (KeyStoreException
+                | CertificateException
+                | UnrecoverableKeyException
+                | IOException
+                | NoSuchAlgorithmException
+                | InvalidKeyException e) {
+            return false;
+        }
+    }
+
+    private void fingerprintResult(boolean fingerok) {
+        if (fingerok) {
+            Intent resultIntent = new Intent();
+            resultIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+            resultIntent.putExtra(KEY_CHECK_RESULT, true);
+            setResult(RESULT_OK, resultIntent);
+            finish();
+        } else {
+            showErrorAndRestart(R.string.fingerprint_unknown);
+        }
+    }
+
+    private void showErrorAndRestart(int errorMessage) {
+        CharSequence errorSeq = getString(errorMessage);
+        Toast.makeText(this, errorSeq, Toast.LENGTH_LONG).show();
+    }
+
+    final static public boolean isFingerprintCapable(Context context) {
+        try {
+            FingerprintManager fingerprintManager =
+                    (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
+
+            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                return fingerprintManager.isHardwareDetected();
+            }
+        } catch (Exception e) {
+            return false;
+        }
+        return false;
+    }
+
+    final static public boolean isFingerprintReady(Context context) {
+        try {
+            FingerprintManager fingerprintManager =
+                    (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
+
+            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                return fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints();
+            }
+        } catch (Exception e) {
+            return false;
+        }
+
+        return false;
+    }
+}
+
+@SuppressLint("NewApi")
+class FingerprintHandler extends FingerprintManager.AuthenticationCallback {
+
+    private TextView text;
+    private Callback callback;
+
+    // Constructor
+    FingerprintHandler(TextView mtext, Callback mcallback) {
+        text = mtext;
+        callback = mcallback;
+    }
+
+    @Override
+    public void onAuthenticationError(int errMsgId, CharSequence errString) {
+        // this.update(String.valueOf(errString), false);
+    }
+
+    @Override
+    public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
+        this.update(String.valueOf(helpString), false);
+    }
+
+    @Override
+    public void onAuthenticationFailed() {
+        this.update(MainApp.getAppContext().getString(R.string.fingerprint_unknown), false);
+    }
+
+    @Override
+    public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
+        this.update("Fingerprint Authentication succeeded.", true);
+    }
+
+    public void update(final String e, Boolean success) {
+        if(success) {
+            text.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onAuthenticated();
+                }
+            }, 0);
+        } else {
+            text.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onFailed(e);
+                }
+            }, 0);
+        }
+    }
+
+    interface Callback {
+        void onAuthenticated();
+        void onFailed(String error);
+    }
+}

+ 55 - 9
src/main/java/com/owncloud/android/ui/activity/Preferences.java

@@ -96,6 +96,7 @@ public class Preferences extends PreferenceActivity
     private Uri mUri;
 
     private CheckBoxPreference pCode;
+    private CheckBoxPreference fPrint;
     private CheckBoxPreference mShowHiddenFiles;
     private Preference pAboutApp;
     private AppCompatDelegate mDelegate;
@@ -159,6 +160,9 @@ public class Preferences extends PreferenceActivity
         // Register context menu for list of preferences.
         registerForContextMenu(getListView());
 
+        PreferenceCategory preferenceCategoryDetails = (PreferenceCategory) findPreference("details");
+
+
         pCode = (CheckBoxPreference) findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE);
         if (pCode != null) {
             pCode.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
@@ -181,6 +185,49 @@ public class Preferences extends PreferenceActivity
             });
         }
 
+        boolean fPrintEnabled = getResources().getBoolean(R.bool.fingerprint_enabled);
+        fPrint = (CheckBoxPreference) findPreference(FingerprintActivity.PREFERENCE_USE_FINGERPRINT);
+        if (fPrint != null) {
+            if(FingerprintActivity.isFingerprintCapable(MainApp.getAppContext()) && fPrintEnabled) {
+                fPrint.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+                    @Override
+                    public boolean onPreferenceChange(Preference preference, Object newValue) {
+                        Boolean incoming = (Boolean) newValue;
+
+                        if(FingerprintActivity.isFingerprintReady(MainApp.getAppContext())) {
+                            SharedPreferences appPrefs =
+                                    PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+                            SharedPreferences.Editor editor = appPrefs.edit();
+                            editor.putBoolean("use_fingerprint", incoming);
+                            editor.commit();
+                            return true;
+                        } else {
+                            if(incoming) {
+                                Toast.makeText(
+                                        MainApp.getAppContext(),
+                                        R.string.prefs_fingerprint_notsetup,
+                                        Toast.LENGTH_LONG)
+                                        .show();
+                                fPrint.setChecked(false);
+                            }
+                            SharedPreferences appPrefs =
+                                    PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+                            SharedPreferences.Editor editor = appPrefs.edit();
+                            editor.putBoolean("use_fingerprint", false);
+                            editor.commit();
+                            return false;
+                        }
+                    }
+                });
+                if(!FingerprintActivity.isFingerprintReady(MainApp.getAppContext())) {
+                    fPrint.setChecked(false);
+                }
+
+            } else {
+                preferenceCategoryDetails.removePreference(fPrint);
+            }
+        }
+
         mShowHiddenFiles = (CheckBoxPreference) findPreference("show_hidden_files");
         mShowHiddenFiles.setOnPreferenceClickListener(new OnPreferenceClickListener() {
             @Override
@@ -189,12 +236,12 @@ public class Preferences extends PreferenceActivity
                         PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
                 SharedPreferences.Editor editor = appPrefs.edit();
                 editor.putBoolean("show_hidden_files_pref", mShowHiddenFiles.isChecked());
-                editor.commit();
+                editor.apply();
                 return true;
             }
         });
 
-        PreferenceCategory preferenceCategory = (PreferenceCategory) findPreference("more");
+        PreferenceCategory preferenceCategoryMore = (PreferenceCategory) findPreference("more");
 
         boolean calendarContactsEnabled = getResources().getBoolean(R.bool.calendar_contacts_enabled);
         Preference pCalendarContacts = findPreference("calendar_contacts");
@@ -217,7 +264,7 @@ public class Preferences extends PreferenceActivity
                     }
                 });
             } else {
-                preferenceCategory.removePreference(pCalendarContacts);
+                preferenceCategoryMore.removePreference(pCalendarContacts);
             }
         }
 
@@ -238,7 +285,7 @@ public class Preferences extends PreferenceActivity
                     }
                 });
             } else {
-                preferenceCategory.removePreference(pHelp);
+                preferenceCategoryMore.removePreference(pHelp);
             }
         }
 
@@ -270,7 +317,7 @@ public class Preferences extends PreferenceActivity
                     }
                 });
             } else {
-                preferenceCategory.removePreference(pRecommend);
+                preferenceCategoryMore.removePreference(pRecommend);
             }
         }
 
@@ -295,7 +342,7 @@ public class Preferences extends PreferenceActivity
                     }
                 });
             } else {
-                preferenceCategory.removePreference(pFeedback);
+                preferenceCategoryMore.removePreference(pFeedback);
             }
         }
 
@@ -313,7 +360,7 @@ public class Preferences extends PreferenceActivity
                     }
                 });
             } else {
-                preferenceCategory.removePreference(pLogger);
+                preferenceCategoryMore.removePreference(pLogger);
             }
         }
 
@@ -335,7 +382,7 @@ public class Preferences extends PreferenceActivity
                     }
                 });
             } else {
-                preferenceCategory.removePreference(pImprint);
+                preferenceCategoryMore.removePreference(pImprint);
             }
         }
 
@@ -368,7 +415,6 @@ public class Preferences extends PreferenceActivity
                     return false;
                 }
             });
-
         }
 
         mPrefInstantUploadCategory = (PreferenceCategory) findPreference("instant_uploading_category");

文件差异内容过多而无法显示
+ 7 - 0
src/main/res/drawable/ic_fingerprint.xml


+ 64 - 0
src/main/res/layout/fingerprintlock.xml

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2017 Florian Lentz
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or any later version.
+
+  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 AFFERO GENERAL PUBLIC LICENSE for more details.
+
+  You should have received a copy of the GNU Affero General Public
+  License along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+              android:layout_width="fill_parent"
+              android:layout_height="fill_parent"
+              android:gravity="center_horizontal"
+              android:orientation="vertical">
+
+    <android.support.v7.widget.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        android:background="?attr/colorPrimary"
+        app:layout_scrollFlags="scroll|enterAlways|snap"
+        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
+        app:title="@string/app_name"
+        app:titleTextColor="@color/white"/>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:gravity="center_horizontal"
+        android:orientation="vertical"
+        android:padding="@dimen/standard_padding">
+
+        <TextView
+            android:id="@+id/scanfingerprinttext"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:padding="8dp"
+            android:text="@string/fingerprint_scan_finger"
+            android:textColor="@android:color/black"
+            android:textSize="32dp"/>
+
+        <ImageView
+            android:id="@+id/fingerprinticon"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:padding="16dp"
+            android:scaleType="fitCenter"
+            android:src="@drawable/ic_fingerprint"
+            android:tint="@color/navigation_bar_start_color"/>
+
+    </LinearLayout>
+</LinearLayout>

+ 1 - 0
src/main/res/values/setup.xml

@@ -88,6 +88,7 @@
     <bool name="bottom_toolbar_enabled">false</bool>
 
     <!-- Help, imprint and feedback -->
+    <bool name="fingerprint_enabled">true</bool>
     <bool name="calendar_contacts_enabled">true</bool>
     <bool name="help_enabled">true</bool>
     <bool name="imprint_enabled">false</bool> 

+ 5 - 0
src/main/res/values/strings.xml

@@ -43,6 +43,8 @@
     <string name="prefs_accounts">Accounts</string>
     <string name="prefs_manage_accounts">Manage accounts</string>
     <string name="prefs_passcode">Passcode lock</string>
+    <string name="prefs_fingerprint">Fingerprint lock</string>
+    <string name="prefs_fingerprint_notsetup">No fingerprints have been set up.</string>
     <string name="prefs_show_hidden_files">Show hidden files</string>
     <string name="prefs_instant_upload">Instant picture uploads</string>
     <string name="prefs_instant_upload_summary">Instantly upload pictures taken by camera</string>
@@ -612,6 +614,9 @@
 
     <string name="whats_new_skip">Skip</string>
 
+    <string name="fingerprint_scan_finger">Please scan your finger</string>
+    <string name="fingerprint_unknown">Finger not recognized</string>
+
     <!-- User information -->
     <string name="user_info_full_name">Full name</string>
     <string name="user_info_email">Email</string>

+ 2 - 1
src/main/res/xml/preferences.xml

@@ -78,8 +78,9 @@
                         
     </PreferenceCategory>
 
-	<PreferenceCategory android:title="@string/prefs_category_details">
+	<PreferenceCategory android:title="@string/prefs_category_details" android:key="details">
 		<android.preference.CheckBoxPreference android:title="@string/prefs_passcode" android:key="set_pincode" />
+		<android.preference.CheckBoxPreference android:title="@string/prefs_fingerprint" android:key="use_fingerprint" />
 		<android.preference.CheckBoxPreference android:title="@string/prefs_show_hidden_files" android:key="show_hidden_files" />
 	</PreferenceCategory>
 

+ 1 - 0
src/modified/res/values/setup.xml

@@ -85,6 +85,7 @@
     <bool name="bottom_toolbar_enabled">true</bool>
 
     <!-- Help, imprint and feedback -->
+    <bool name="fingerprint_enabled">true</bool>
     <bool name="calendar_contacts_enabled">true</bool>
     <bool name="help_enabled">true</bool>
     <bool name="imprint_enabled">false</bool> 

部分文件因为文件数量过多而无法显示