Browse Source

Merge branch 'fingerprint' of isundil/pass into master

isundil 7 years ago
parent
commit
de94469487
54 changed files with 948 additions and 702 deletions
  1. 7 8
      .idea/misc.xml
  2. 6 0
      .idea/vcs.xml
  3. 5 0
      app/build.gradle
  4. 1 1
      app/src/main/AndroidManifest.xml
  5. 0 31
      app/src/main/java/info/knacki/pass/Sha1Builder.java
  6. 10 9
      app/src/main/java/info/knacki/pass/git/HttpGitProtocol.java
  7. 6 9
      app/src/main/java/info/knacki/pass/git/entities/GitCommit.java
  8. 3 0
      app/src/main/java/info/knacki/pass/git/entities/GitRef.java
  9. 10 18
      app/src/main/java/info/knacki/pass/input/InputService.java
  10. 30 0
      app/src/main/java/info/knacki/pass/io/CharsetHelper.java
  11. 12 13
      app/src/main/java/info/knacki/pass/io/FileInterfaceFactory.java
  12. 16 0
      app/src/main/java/info/knacki/pass/io/FileUtils.java
  13. 0 83
      app/src/main/java/info/knacki/pass/io/FingerprintFileInterface.java
  14. 2 3
      app/src/main/java/info/knacki/pass/io/GPGFileInterface.java
  15. 20 12
      app/src/main/java/info/knacki/pass/io/GPGUtil.java
  16. 5 0
      app/src/main/java/info/knacki/pass/io/OnErrorListener.java
  17. 1 3
      app/src/main/java/info/knacki/pass/io/OnResponseListener.java
  18. 2 10
      app/src/main/java/info/knacki/pass/io/OutputStreamWithCheckSum.java
  19. 10 9
      app/src/main/java/info/knacki/pass/io/PasswordFileInterface.java
  20. 5 0
      app/src/main/java/info/knacki/pass/io/PathUtils.java
  21. 1 13
      app/src/main/java/info/knacki/pass/io/RawFileInterface.java
  22. 12 7
      app/src/main/java/info/knacki/pass/settings/SettingsManager.java
  23. 0 6
      app/src/main/java/info/knacki/pass/settings/ui/AppCompatPreferenceActivity.java
  24. 74 98
      app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java
  25. 32 49
      app/src/main/java/info/knacki/pass/ui/EditPasswordActivity.java
  26. 57 104
      app/src/main/java/info/knacki/pass/ui/GitPullActivity.java
  27. 56 93
      app/src/main/java/info/knacki/pass/ui/MainActivity.java
  28. 14 7
      app/src/main/java/info/knacki/pass/ui/alertPrompt/AlertPrompt.java
  29. 15 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/AlertPromptGenerator.java
  30. 1 1
      app/src/main/java/info/knacki/pass/ui/alertPrompt/ServiceAlertPrompt.java
  31. 17 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/ServiceAlertPromptGenerator.java
  32. 9 16
      app/src/main/java/info/knacki/pass/ui/alertPrompt/views/ConflictView.java
  33. 25 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/views/FingerprintView.java
  34. 2 3
      app/src/main/java/info/knacki/pass/ui/alertPrompt/views/PasswordTextEdit.java
  35. 7 11
      app/src/main/java/info/knacki/pass/ui/passwordList/EditablePasswordListView.java
  36. 9 13
      app/src/main/java/info/knacki/pass/ui/passwordList/PasswordListView.java
  37. 353 0
      app/src/main/java/info/knacki/pass/ui/passwordPicker/FingerprintPicker.java
  38. 32 39
      app/src/main/java/info/knacki/pass/ui/passwordPicker/PasswordPicker.java
  39. 33 0
      app/src/main/java/info/knacki/pass/ui/passwordPicker/PasswordPickerFactory.java
  40. 0 21
      app/src/main/java/info/knacki/pass/ui/passwordPicker/ServicePasswordPicker.java
  41. 6 0
      app/src/main/res/drawable/ic_fingerprint.xml
  42. 6 0
      app/src/main/res/drawable/ic_fingerprint_fail.xml
  43. 16 0
      app/src/main/res/layout/fingerprint_picker.xml
  44. 1 1
      app/src/main/res/menu/context_dir_menu.xml
  45. 2 2
      app/src/main/res/menu/context_file_menu.xml
  46. 4 1
      app/src/main/res/values-fr/lang.xml
  47. 1 0
      app/src/main/res/values/dimens.xml
  48. 4 1
      app/src/main/res/values/lang.xml
  49. 1 1
      app/src/main/res/values/strings.xml
  50. 4 0
      app/src/main/res/xml/pref_encryption.xml
  51. 1 3
      app/src/main/res/xml/provider_paths.xml
  52. 1 1
      build.gradle
  53. BIN
      gradle/wrapper/gradle-wrapper.jar
  54. 1 2
      gradle/wrapper/gradle-wrapper.properties

+ 7 - 8
.idea/misc.xml

@@ -5,30 +5,29 @@
     <option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
     <option name="myNullables">
       <value>
-        <list size="5">
+        <list size="7">
           <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
           <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
           <item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
           <item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
           <item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
+          <item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
+          <item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
         </list>
       </value>
     </option>
     <option name="myNotNulls">
       <value>
-        <list size="4">
+        <list size="6">
           <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
           <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
           <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
           <item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
+          <item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
+          <item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
         </list>
       </value>
     </option>
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
-    <output url="file://$PROJECT_DIR$/build/classes" />
-  </component>
-  <component name="ProjectType">
-    <option name="id" value="Android" />
-  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
 </project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 5 - 0
app/build.gradle

@@ -17,6 +17,11 @@ android {
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
     }
+    buildToolsVersion '28.0.3'
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -50,7 +50,7 @@
         <activity
             android:name=".ui.EditPasswordActivity"
             android:windowSoftInputMode="adjustResize" />
-        <activity android:name=".ui.GitPullActivity"></activity>
+        <activity android:name=".ui.GitPullActivity"/>
 
         <provider
             android:authorities="info.knacki.pass.provider"

+ 0 - 31
app/src/main/java/info/knacki/pass/Sha1Builder.java

@@ -1,31 +0,0 @@
-package info.knacki.pass;
-
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-public class Sha1Builder {
-    private final MessageDigest sha1Builder;
-
-    Sha1Builder() {
-        MessageDigest sha1Builder;
-        try {
-            sha1Builder = MessageDigest.getInstance("SHA1");
-        } catch (NoSuchAlgorithmException e) {
-            sha1Builder = null;
-        }
-        this.sha1Builder = sha1Builder;
-    }
-
-    public Sha1Builder Write(String in) {
-        sha1Builder.update(in.getBytes());
-        return this;
-    }
-
-    public byte[] GetHash() {
-        return sha1Builder.digest();
-    }
-
-    public static byte[] Hash(String in) {
-        return new Sha1Builder().Write(in).GetHash();
-    }
-}

+ 10 - 9
app/src/main/java/info/knacki/pass/git/HttpGitProtocol.java

@@ -135,13 +135,15 @@ class HttpGitProtocol implements GitInterface {
                 return in.getInputStream();
             } catch (Throwable e) {
                 Map<String, List<String>> headers = in.getHeaderFields();
-                lp: for (List<String> i: headers.values()) {
-                    for (String status: i) {
-                        log.severe(in.getURL().toString() + ": " + status);
-                        break lp;
-                    }
-                    break;
+                String msg = null;
+                if (!headers.values().isEmpty()) {
+                    List<String> statuses = headers.values().iterator().next();
+                    if (!statuses.isEmpty())
+                        msg = in.getURL().toString() + ": " +statuses.iterator().next();
                 }
+                if (msg == null)
+                    msg = "Tried to reach " +in.getURL().toString();
+                log.log(Level.SEVERE, msg, e);
                 throw e;
             }
         }
@@ -463,8 +465,7 @@ class HttpGitProtocol implements GitInterface {
                 return false;
             msg.write(GitPackableUtil.getObjHeader(i.GetPackableType(), pack.length))
                 .write(GitPackableUtil.deflate(pack));
-            String tmp = GitSha1.getSha1OfPackable(i);
-            log.severe("Writing pack " +i.GetPackableType() +", " +GitSha1.getSha1OfPackable(i) +new String(i.GetPack()));
+            log.info("Writing pack " +i.GetPackableType() +", " +GitSha1.getSha1OfPackable(i) +new String(i.GetPack()));
         }
         msg.writeSha1();
         return true;
@@ -509,6 +510,7 @@ class HttpGitProtocol implements GitInterface {
                                         onError("Pack error", null);
                                         return;
                                     }
+                                    result.close();
                                 }
                                 catch (IOException e) {
                                     log.log(Level.SEVERE, "Cannot git-upload-pack: " +e.getMessage(), e);
@@ -524,7 +526,6 @@ class HttpGitProtocol implements GitInterface {
                     }
                     catch (IOException e) {
                         response.onError(e.getMessage(), e);
-                        return;
                     }
                 } else {
                     response.onError("Branch " +fConfig.GetBranch() + " not found on remote for pushing", null);

+ 6 - 9
app/src/main/java/info/knacki/pass/git/entities/GitCommit.java

@@ -2,7 +2,6 @@ package info.knacki.pass.git.entities;
 
 import java.io.File;
 import java.text.SimpleDateFormat;
-import java.util.ArrayDeque;
 import java.util.Date;
 import java.util.Locale;
 import java.util.Set;
@@ -12,13 +11,13 @@ import java.util.TreeSet;
 import info.knacki.pass.git.GitSha1;
 
 public class GitCommit implements GitPackable {
-    private String fHash;
+    private final String fHash;
     private byte[] fTreeHash;
     private GitObject.GitTree fTree;
     private String fParent;
     private String fAuthor;
     private String fCommitter;
-    private String fMessage;
+    private final String fMessage;
     private Date fTime;
 
     public GitCommit(String hash, String sha1Content) {
@@ -95,8 +94,8 @@ public class GitCommit implements GitPackable {
         String dateStr = (fTime.getTime() / 1000) +" " +(new SimpleDateFormat("Z", Locale.US).format(fTime));
         String data = "tree " +GitSha1.BytesToString(fTree.GetHash()) +"\n" +
                 "parent " +GetParent() +"\n" +
-                "author " +fAuthor +" " +dateStr +"\n" +
-                "committer " +fCommitter +" " +dateStr +"\n" +
+                "author " +GetAuthor() +" " +dateStr +"\n" +
+                "committer " +GetCommitter() +" " +dateStr +"\n" +
                 "\n" +
                 fMessage +"\n";
         return data.getBytes();
@@ -105,7 +104,6 @@ public class GitCommit implements GitPackable {
     public static class Builder {
         private final GitCommit co;
         private final GitObject.GitTree fTree;
-        private final ArrayDeque<File> fFilesToPush = new ArrayDeque<>();
         private final Set<String> fToPack = new TreeSet<>();
 
         public Builder(GitCommit parent, String author, String authorEmail, String message) {
@@ -125,7 +123,6 @@ public class GitCommit implements GitPackable {
 
         public GitCommit.Builder AddFile(String relativeFilename, File f) {
             if (f.exists()) {
-                fFilesToPush.add(f);
                 GitObject obj = fTree.AddItem(relativeFilename, f);
                 do {
                     fToPack.add(obj.GetGitPath());
@@ -144,11 +141,11 @@ public class GitCommit implements GitPackable {
                     for (String i: fToPack)
                         if (i.startsWith(relativeFilename +"/"))
                             fToPack.remove(i);
-                do {
+                while (obj != null) {
                     fToPack.add(obj.GetGitPath());
                     obj.fSha1 = null;
                     obj = obj.GetParent();
-                } while (obj != null);
+                }
             }
             return this;
         }

+ 3 - 0
app/src/main/java/info/knacki/pass/git/entities/GitRef.java

@@ -1,5 +1,7 @@
 package info.knacki.pass.git.entities;
 
+import android.support.annotation.NonNull;
+
 public class GitRef {
     protected final String fHash;
     protected final String fBranch;
@@ -24,6 +26,7 @@ public class GitRef {
         return fBranch.substring(branchIndex + 1);
     }
 
+    @NonNull
     @Override
     public String toString() {
         return "ref {" + fHash + "} for branch {" + fBranch + "}";

+ 10 - 18
app/src/main/java/info/knacki/pass/input/InputService.java

@@ -22,7 +22,7 @@ import info.knacki.pass.io.PathUtils;
 import info.knacki.pass.ui.MainActivity;
 import info.knacki.pass.ui.passwordList.PasswordClickListener;
 import info.knacki.pass.ui.passwordList.PasswordListView;
-import info.knacki.pass.ui.passwordPicker.ServicePasswordPicker;
+import info.knacki.pass.ui.passwordPicker.PasswordPickerFactory;
 
 public class InputService extends InputMethodService implements PasswordClickListener {
     protected PasswordListView fPasswordListView;
@@ -43,30 +43,22 @@ public class InputService extends InputMethodService implements PasswordClickLis
         fInputView = LayoutInflater.from(this).inflate(R.layout.input, null, false);
         fPasswordListView = new PasswordListView<>(this, PathUtils.GetPassDir(this));
         ((ScrollView)fInputView.findViewById(R.id.passwordListContainer)).addView(fPasswordListView);
-        fInputView.findViewById(R.id.prevButton).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                InputMethodManager service = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
-                if (null == service) {
-                    Toast.makeText(InputService.this, "Android error", Toast.LENGTH_LONG).show();
-                    log.log(Level.SEVERE, "Cannot get Input method service");
-                    return;
-                }
-                service.showInputMethodPicker();
-            }
-        });
-        fInputView.findViewById(R.id.openAppButton).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                startActivity(new Intent(InputService.this, MainActivity.class));
+        fInputView.findViewById(R.id.prevButton).setOnClickListener(view -> {
+            InputMethodManager service = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+            if (null == service) {
+                Toast.makeText(InputService.this, "Android error", Toast.LENGTH_LONG).show();
+                log.log(Level.SEVERE, "Cannot get Input method service");
+                return;
             }
+            service.showInputMethodPicker();
         });
+        fInputView.findViewById(R.id.openAppButton).setOnClickListener(view -> startActivity(new Intent(InputService.this, MainActivity.class)));
         return fInputView;
     }
 
     @Override
     public void OnPasswordClicked(File f) {
-        FileInterfaceFactory.GetFileInterface(this, new ServicePasswordPicker(this, fInputView), f).ReadFile(new OnResponseListener<String>() {
+        FileInterfaceFactory.GetFileInterface(this, PasswordPickerFactory.GetPasswordPicker(this, fInputView), f).ReadFile(new OnResponseListener<String>() {
             @Override
             public void onResponse(String passwordContent) {
                 for (char i: passwordContent.toCharArray()) {

+ 30 - 0
app/src/main/java/info/knacki/pass/io/CharsetHelper.java

@@ -0,0 +1,30 @@
+package info.knacki.pass.io;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.logging.Logger;
+
+public class CharsetHelper {
+    private static final Logger log = Logger.getLogger(CharsetHelper.class.getName());
+    public final static String DEFAULT_CHARSET = "UTF-8";
+
+    public static String ByteArrayToString(byte[] arr) {
+        try {
+            return new String(arr, DEFAULT_CHARSET);
+        }
+        catch (UnsupportedEncodingException e) {
+            log.severe("Unsupported charset " +DEFAULT_CHARSET);
+        }
+        return new String(arr, Charset.defaultCharset());
+    }
+
+    public static byte[] StringToByteArray(String str) {
+        try {
+            return str.getBytes(DEFAULT_CHARSET);
+        }
+        catch (UnsupportedEncodingException e) {
+            log.severe("Unsupported charset " +DEFAULT_CHARSET);
+        }
+        return str.getBytes(Charset.defaultCharset());
+    }
+}

+ 12 - 13
app/src/main/java/info/knacki/pass/io/FileInterfaceFactory.java

@@ -1,15 +1,19 @@
 package info.knacki.pass.io;
 
 import android.content.Context;
+import android.os.Build;
 
 import java.io.File;
 
 import info.knacki.pass.settings.SettingsManager;
 
 public class FileInterfaceFactory {
+    public interface OnPasswordEnteredListener extends OnErrorListener {
+        boolean onResponse(String password);
+    }
+
     public interface PasswordGetter {
-        void GetPassword(OnResponseListener<String> onPassword);
-        void WrongPassword();
+        void GetPassword(OnPasswordEnteredListener onPassword);
     }
 
     public interface ChangePasswordGetter extends PasswordGetter {
@@ -22,24 +26,19 @@ public class FileInterfaceFactory {
         ChangePasswordGetter SetStep(eStep step);
     }
 
-    public final static String FINGERPRINT_SUFFIX = ".fck";
-    public final static String PASSWORD_SUFFIX = ".enc";
+    public final static String PASSWORD_SUFFIX = ".pwd";
     public final static String GPG_SUFFIX = ".gpg";
 
-    public static IFileInterface GetFileInterface(Context ctx, PasswordGetter passwordGetter, File f) {
-        if (f.getName().endsWith(FINGERPRINT_SUFFIX))
-            return new FingerprintFileInterface(ctx, f);
-        else if (f.getName().endsWith(PASSWORD_SUFFIX))
-            return new PasswordFileInterface(passwordGetter, f);
+    public static IFileInterface GetFileInterface(Context ctx, PasswordGetter passInput, File f) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && f.getName().endsWith(PASSWORD_SUFFIX))
+            return new PasswordFileInterface(passInput, f);
         else if (f.getName().endsWith(GPG_SUFFIX))
-            return new GPGFileInterface(ctx, passwordGetter, f);
+            return new GPGFileInterface(ctx, passInput, f);
         return new RawFileInterface(f);
     }
 
     public static String GetExtension(SettingsManager.EncryptionType type) {
-        if (SettingsManager.EncryptionType.TYPE_FINGERPRINT.equals(type))
-            return FINGERPRINT_SUFFIX;
-        else if (SettingsManager.EncryptionType.TYPE_PASSWORD.equals(type))
+        if (SettingsManager.EncryptionType.TYPE_PASSWORD.equals(type))
             return PASSWORD_SUFFIX;
         else if (SettingsManager.EncryptionType.TYPE_GPG.equals(type))
             return GPG_SUFFIX;

+ 16 - 0
app/src/main/java/info/knacki/pass/io/FileUtils.java

@@ -1,6 +1,8 @@
 package info.knacki.pass.io;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -17,6 +19,20 @@ public class FileUtils {
         }
     }
 
+    public static String ReadAllFile(File file) throws IOException {
+        BufferedReader buf = new BufferedReader(new FileReader(file));
+        StringBuilder result = new StringBuilder();
+        String tmp;
+
+        while ((tmp = buf.readLine()) != null) {
+            if (result.length() > 0)
+                result.append("\n");
+            result.append(tmp);
+        }
+        buf.close();
+        return result.toString();
+    }
+
     public static void pipe(InputStream in, OutputStream out) throws IOException {
         int len;
         byte[] buf = new byte[1024];

+ 0 - 83
app/src/main/java/info/knacki/pass/io/FingerprintFileInterface.java

@@ -1,83 +0,0 @@
-package info.knacki.pass.io;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
-import android.support.v4.os.CancellationSignal;
-
-import java.io.File;
-import java.security.Security;
-import java.util.Arrays;
-import java.util.logging.Logger;
-
-import javax.crypto.Cipher;
-import javax.crypto.spec.SecretKeySpec;
-
-import info.knacki.pass.Sha1Builder;
-
-class FingerprintFileInterface implements IFileInterface {
-    private final File fFile;
-    private final Context fContext;
-
-    public class CryptoObject extends FingerprintManagerCompat.CryptoObject {
-        public CryptoObject(@NonNull Cipher cipher) {
-            super(cipher);
-        }
-    }
-
-    FingerprintFileInterface(Context c, File f) {
-        fFile = f;
-        fContext = c;
-    }
-
-    @Override
-    public void ReadFile(OnResponseListener<String> onResp) {
-        //FIXME
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void WriteFile(String content, OnResponseListener<Void> resp) {
-        CancellationSignal cancelSignal = new CancellationSignal();
-        final Cipher cipher;
-        Security.removeProvider("BC");
-        /*
-            AndroidNSSP
-2018-10-07 16:21:18.582 24744-24744/info.knacki.pass E/null: >> Provider >> AndroidOpenSSL
-2018-10-07 16:21:18.582 24744-24744/info.knacki.pass E/null: >> Provider >> CertPathProvider
-2018-10-07 16:21:18.582 24744-24744/info.knacki.pass E/null: >> Provider >> AndroidKeyStoreBCWorkaround
-2018-10-07 16:21:18.582 24744-24744/info.knacki.pass E/null: >> Provider >> BC
-2018-10-07 16:21:18.583 24744-24744/info.knacki.pass E/null: >> Provider >> HarmonyJSSE
-2018-10-07 16:21:18.583 24744-24744/info.knacki.pass E/null: >> Provider >> AndroidKeyStore
-         */
-        try {
-            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
-            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(Arrays.copyOf(Sha1Builder.Hash("pass"), 16), "AES"));
-        } catch (Throwable e) {
-            resp.onError(e.getMessage(), e);
-            return;
-        }
-        Logger.getAnonymousLogger().severe("Coucou");
-        FingerprintManagerCompat.from(fContext).authenticate(this.new CryptoObject(cipher), 0, cancelSignal, new FingerprintManagerCompat.AuthenticationCallback() {
-            @Override
-            public void onAuthenticationError(int errMsgId, CharSequence errString) {
-                super.onAuthenticationError(errMsgId, errString);
-            }
-
-            @Override
-            public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
-                super.onAuthenticationHelp(helpMsgId, helpString);
-            }
-
-            @Override
-            public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
-                super.onAuthenticationSucceeded(result);
-            }
-
-            @Override
-            public void onAuthenticationFailed() {
-                super.onAuthenticationFailed();
-            }
-        }, null);
-    }
-}

+ 2 - 3
app/src/main/java/info/knacki/pass/io/GPGFileInterface.java

@@ -5,7 +5,6 @@ import android.content.Context;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.nio.charset.Charset;
 
 import info.knacki.pass.settings.SettingsManager;
 
@@ -32,7 +31,7 @@ class GPGFileInterface implements IFileInterface {
             GPGUtil.DecryptFile(fContext, fPasswordGetter, fFile, new OnResponseListener<byte[]>() {
                 @Override
                 public void onResponse(byte[] result) {
-                    resp.onResponse(new String(result, Charset.defaultCharset()));
+                    resp.onResponse(CharsetHelper.ByteArrayToString(result));
                 }
 
                 @Override
@@ -50,7 +49,7 @@ class GPGFileInterface implements IFileInterface {
         try {
             if (!SettingsManager.HasGPGKey(fContext))
                 throw new FileNotFoundException("GPG key not set");
-            GPGUtil.CryptFile(fContext, fPasswordGetter, new FileOutputStream(fFile), content.getBytes(), resp);
+            GPGUtil.CryptFile(fContext, new FileOutputStream(fFile), CharsetHelper.StringToByteArray(content), resp);
         }
         catch (Throwable e) {
             resp.onError(e.getMessage(), e);

+ 20 - 12
app/src/main/java/info/knacki/pass/io/GPGUtil.java

@@ -108,18 +108,20 @@ public class GPGUtil {
         if (priv != null) {
             resp.onResponse(priv);
         } else {
-            OnResponseListener<String> onResp = new OnResponseListener<String>() {
+            FileInterfaceFactory.OnPasswordEnteredListener onResp = new FileInterfaceFactory.OnPasswordEnteredListener() {
                 @Override
-                public void onResponse(String result) {
+                public boolean onResponse(String result) {
                     if (result == null) {
                         resp.onError("Invalid password", null);
+                        return false;
                     } else {
                         PGPPrivateKeyAndPass priv = TryPassword(pgpSecKey, result);
                         if (priv != null) {
                             resp.onResponse(priv);
+                            return true;
                         } else {
-                            passwordGetter.WrongPassword();
                             passwordGetter.GetPassword(this);
+                            return false;
                         }
                     }
                 }
@@ -217,6 +219,12 @@ public class GPGUtil {
         resp.onResponse(output);
     }
 
+    private static PGPSecretKeyRing DoChangePassword(PGPSecretKeyRing secKeyring, String oldPassword, String newPassword) throws PGPException {
+        final PBESecretKeyDecryptor keyDecryptor = CreateDecryptor(oldPassword.toCharArray());
+        final PBESecretKeyEncryptor newEncryptor = new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_256).setProvider("BC").build(newPassword.toCharArray());
+        return PGPSecretKeyRing.copyWithNewPassword(secKeyring, keyDecryptor, newEncryptor);
+    }
+
     public static void DecryptFile(final Context ctx, final FileInterfaceFactory.PasswordGetter passwordGetter, final File file, final OnResponseListener<byte[]> resp) throws IOException {
         PGPObjectFactory pgpF = new PGPObjectFactory(PGPUtil.getDecoderStream(new FileInputStream(file)), new JcaKeyFingerprintCalculator());
         Object o = pgpF.nextObject();
@@ -246,7 +254,7 @@ public class GPGUtil {
         }
     }
 
-    public static void CryptFile(final Context ctx, final FileInterfaceFactory.PasswordGetter passwordGetter, final OutputStream fileOutStream, final byte[] data, final OnResponseListener<Void> resp) {
+    public static void CryptFile(final Context ctx, final OutputStream fileOutStream, final byte[] data, final OnResponseListener<Void> resp) {
         try {
             ByteArrayOutputStream compressedDataStream = new ByteArrayOutputStream();
             PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.UNCOMPRESSED);
@@ -286,12 +294,6 @@ public class GPGUtil {
         }
     }
 
-    private static PGPSecretKeyRing DoChangePassword(PGPSecretKeyRing secKeyring, String oldPassword, String newPassword) throws PGPException {
-        final PBESecretKeyDecryptor keyDecryptor = CreateDecryptor(oldPassword.toCharArray());
-        final PBESecretKeyEncryptor newEncryptor = new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_256).setProvider("BC").build(newPassword.toCharArray());
-        return PGPSecretKeyRing.copyWithNewPassword(secKeyring, keyDecryptor, newEncryptor);
-    }
-
     public static void ChangePassword(final Context ctx, final FileInterfaceFactory.ChangePasswordGetter passwordGetter, final OnResponseListener<Void> onDone) throws IOException {
         final PGPSecretKeyRing secretKeyring = findSecretKeyring(getKeyInputStream(ctx));
         final PGPSecretKey signingKey = secretKeyring.getSecretKey();
@@ -302,9 +304,9 @@ public class GPGUtil {
                 new OnResponseListener<PGPPrivateKeyAndPass>() {
                     @Override
                     public void onResponse(final PGPPrivateKeyAndPass sKey) {
-                        passwordGetter.SetStep(FileInterfaceFactory.ChangePasswordGetter.eStep.eNewPassword).GetPassword(new OnResponseListener<String>() {
+                        passwordGetter.SetStep(FileInterfaceFactory.ChangePasswordGetter.eStep.eNewPassword).GetPassword(new FileInterfaceFactory.OnPasswordEnteredListener() {
                             @Override
-                            public void onResponse(String result) {
+                            public boolean onResponse(String result) {
                                 if (result != null) {
                                     try {
                                         ArrayList<PGPSecretKeyRing> keyringCollection = new ArrayList<>();
@@ -313,10 +315,12 @@ public class GPGUtil {
                                         new PGPSecretKeyRingCollection(keyringCollection).encode(stream);
                                         SettingsManager.SetGPGKeyContent(ctx, SettingsManager.GPG_GENERATED, new ByteArrayInputStream(stream.toByteArray()));
                                         onDone.onResponse(null);
+                                        return true;
                                     } catch (PGPException | IOException e) {
                                         onError(e.getMessage(), e);
                                     }
                                 }
+                                return false;
                             }
 
                             @Override
@@ -332,6 +336,10 @@ public class GPGUtil {
                 });
     }
 
+    public static void Generate(final OutputStream out) {
+        // FIXME
+    }
+
     public static boolean CheckIsPasswordProtected(Context ctx) throws IOException {
         return TryPassword(findSecretKey(getKeyInputStream(ctx)), "") == null;
     }

+ 5 - 0
app/src/main/java/info/knacki/pass/io/OnErrorListener.java

@@ -0,0 +1,5 @@
+package info.knacki.pass.io;
+
+public interface OnErrorListener {
+    void onError(String msg, Throwable e);
+}

+ 1 - 3
app/src/main/java/info/knacki/pass/io/OnResponseListener.java

@@ -1,7 +1,5 @@
 package info.knacki.pass.io;
 
-public interface OnResponseListener<T> {
+public interface OnResponseListener<T> extends OnErrorListener {
     void onResponse(T result);
-
-    void onError(String msg, Throwable e);
 }

+ 2 - 10
app/src/main/java/info/knacki/pass/io/OutputStreamWithCheckSum.java

@@ -9,7 +9,7 @@ import java.util.logging.Logger;
 
 public class OutputStreamWithCheckSum {
     private static final Logger log = Logger.getLogger(OutputStreamWithCheckSum.class.getName());
-    private OutputStream fOutput;
+    private final OutputStream fOutput;
     private MessageDigest fDigest;
 
     public OutputStreamWithCheckSum(OutputStream out) {
@@ -23,22 +23,14 @@ public class OutputStreamWithCheckSum {
         }
     }
 
-    public OutputStreamWithCheckSum write(byte b) throws IOException {
-        log.finer("Add 1 byte");
-        fDigest.update(b);
-        fOutput.write(new byte[] {b});
-        return this;
-    }
-
     public OutputStreamWithCheckSum write(byte []b) throws IOException {
-        log.finer("Add " +b.length +" bytes");
         fDigest.update(b);
         fOutput.write(b);
         return this;
     }
 
     public OutputStreamWithCheckSum write(String b) throws IOException {
-        return write(b.getBytes());
+        return write(CharsetHelper.StringToByteArray(b));
     }
 
     public OutputStreamWithCheckSum writeSha1() throws IOException {

+ 10 - 9
app/src/main/java/info/knacki/pass/io/PasswordFileInterface.java

@@ -27,16 +27,15 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.nio.charset.Charset;
 import java.security.SecureRandom;
 import java.security.Security;
 import java.util.Date;
 
 class PasswordFileInterface implements IFileInterface {
-    private final File fFile;
-    private final FileInterfaceFactory.PasswordGetter fPasswordGetter;
+    protected final File fFile;
+    protected final FileInterfaceFactory.PasswordGetter fPasswordGetter;
 
-    private final String passwordWrite = "mpass"; // FIXME
+    protected final String passwordWrite = "mpass"; // FIXME
 
     PasswordFileInterface(FileInterfaceFactory.PasswordGetter passwordGetter, File f) {
         fFile = f;
@@ -65,15 +64,17 @@ class PasswordFileInterface implements IFileInterface {
     }
 
     private void FindPassword(final OnResponseListener<InputStream> onResponse) {
-        OnResponseListener<String> onResp = new OnResponseListener<String>() {
+        FileInterfaceFactory.OnPasswordEnteredListener onResp = new FileInterfaceFactory.OnPasswordEnteredListener() {
             @Override
-            public void onResponse(String result) {
+            public boolean onResponse(String result) {
                 if (result == null) {
                     onResponse.onError("Invalid password", null);
+                    return false;
                 } else if (!TryPassword(result, onResponse)) {
-                    fPasswordGetter.WrongPassword();
                     fPasswordGetter.GetPassword(this);
+                    return false;
                 }
+                return true;
             }
 
             @Override
@@ -151,7 +152,7 @@ class PasswordFileInterface implements IFileInterface {
             DecryptFile(new OnResponseListener<byte[]>() {
                 @Override
                 public void onResponse(byte[] result) {
-                    resp.onResponse(new String(result, Charset.defaultCharset()));
+                    resp.onResponse(CharsetHelper.ByteArrayToString(result));
                 }
 
                 @Override
@@ -170,7 +171,7 @@ class PasswordFileInterface implements IFileInterface {
         Security.addProvider(new BouncyCastleProvider());
 
         try {
-            CryptFile(new FileOutputStream(fFile), content.getBytes());
+            CryptFile(new FileOutputStream(fFile), CharsetHelper.StringToByteArray(content));
         }
         catch (Throwable e) {
             resp.onError(e.getMessage(), e);

+ 5 - 0
app/src/main/java/info/knacki/pass/io/PathUtils.java

@@ -5,6 +5,7 @@ import android.content.Context;
 public class PathUtils {
     private static final String DATA_DIR_NAME = "pass";
     private static final String DATA_GIT_LOCAL = "gitfiles";
+    private static final String DATA_FINGER_LOCAL = "fingerprint";
 
     private static String GetAppRootDir(Context ctx) {
         return ctx.getFilesDir().getAbsolutePath();
@@ -17,4 +18,8 @@ public class PathUtils {
     public static String GetGitFile(Context ctx) {
         return GetAppRootDir(ctx) +"/" +DATA_GIT_LOCAL;
     }
+
+    public static String GetFingerprintFile(Context ctx) {
+        return GetAppRootDir(ctx) +"/" +DATA_FINGER_LOCAL;
+    }
 }

+ 1 - 13
app/src/main/java/info/knacki/pass/io/RawFileInterface.java

@@ -1,8 +1,6 @@
 package info.knacki.pass.io;
 
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
 
@@ -16,17 +14,7 @@ class RawFileInterface implements IFileInterface {
     @Override
     public void ReadFile(OnResponseListener<String> resp) {
         try {
-            BufferedReader buf = new BufferedReader(new FileReader(fFile));
-            StringBuilder result = new StringBuilder();
-            String tmp;
-
-            while ((tmp = buf.readLine()) != null) {
-                if (result.length() > 0)
-                    result.append("\n");
-                result.append(tmp);
-            }
-            buf.close();
-            resp.onResponse(result.toString());
+            resp.onResponse(FileUtils.ReadAllFile(fFile));
         }
         catch (IOException e) {
             resp.onError(e.getMessage(), e);

+ 12 - 7
app/src/main/java/info/knacki/pass/settings/SettingsManager.java

@@ -20,9 +20,8 @@ public class SettingsManager {
 
     public static class EncryptionType {
         public static final EncryptionType TYPE_RAW = new EncryptionType(0);
-        public static final EncryptionType TYPE_FINGERPRINT = new EncryptionType(1);
-        public static final EncryptionType TYPE_GPG = new EncryptionType(2);
-        public static final EncryptionType TYPE_PASSWORD = new EncryptionType(3);
+        public static final EncryptionType TYPE_GPG = new EncryptionType(1);
+        public static final EncryptionType TYPE_PASSWORD = new EncryptionType(2);
 
         private final int fType;
 
@@ -42,8 +41,6 @@ public class SettingsManager {
         public static EncryptionType FromInt(int i) {
             if (i == TYPE_RAW.fType)
                 return TYPE_RAW;
-            else if (i == TYPE_FINGERPRINT.fType)
-                return TYPE_FINGERPRINT;
             else if (i == TYPE_PASSWORD.fType)
                 return TYPE_PASSWORD;
             else if (i == TYPE_GPG.fType)
@@ -141,13 +138,13 @@ public class SettingsManager {
                 fUrl = url;
             return fUrl;
         }
+
         public String GetUrl() {
             return fUrl;
         }
 
         public String SetPassword(String pass) {
-            fPassword = pass;
-            return fPassword;
+            return fPassword = pass;
         }
 
         public String GetPassword() {
@@ -273,4 +270,12 @@ public class SettingsManager {
     public static boolean HasGPGKey(Context ctx) {
         return GetPrefManager(ctx).contains("GPGKeyFile");
     }
+
+    public static boolean IsFingerprintEnabled(Context ctx) {
+        return GetPrefManager(ctx).getBoolean("FingerprintEnabled", false);
+    }
+
+    public static void SetFingerprintEnabled(Context ctx, boolean value) {
+        GetPrefManager(ctx).edit().putBoolean("FingerprintEnabled", value).commit();
+    }
 }

+ 0 - 6
app/src/main/java/info/knacki/pass/settings/ui/AppCompatPreferenceActivity.java

@@ -5,10 +5,8 @@ import android.os.Bundle;
 import android.preference.PreferenceActivity;
 import android.support.annotation.LayoutRes;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatDelegate;
-import android.support.v7.widget.Toolbar;
 import android.view.MenuInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -38,10 +36,6 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
         return getDelegate().getSupportActionBar();
     }
 
-    public void setSupportActionBar(@Nullable Toolbar toolbar) {
-        getDelegate().setSupportActionBar(toolbar);
-    }
-
     @NonNull
     @Override
     public MenuInflater getMenuInflater() {

+ 74 - 98
app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java

@@ -44,9 +44,9 @@ import info.knacki.pass.io.GPGUtil;
 import info.knacki.pass.io.OnResponseListener;
 import info.knacki.pass.settings.SettingsManager;
 import info.knacki.pass.ui.GitPullActivity;
-import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
 import info.knacki.pass.ui.alertPrompt.views.TextView;
-import info.knacki.pass.ui.passwordPicker.PasswordPicker;
+import info.knacki.pass.ui.passwordPicker.PasswordPickerFactory;
 
 /**
  * A {@link PreferenceActivity} that presents a set of application settings. On
@@ -137,12 +137,9 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             addPreferencesFromResource(R.xml.pref_general);
             setHasOptionsMenu(true);
 
-            findPreference(getResources().getString(R.string.id_softSettings)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-                @Override
-                public boolean onPreferenceClick(Preference preference) {
-                    //FIXME
-                    return true;
-                }
+            findPreference(getResources().getString(R.string.id_softSettings)).setOnPreferenceClickListener(preference -> {
+                //FIXME
+                return true;
             });
         }
 
@@ -169,14 +166,18 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             final int encTypeIndex = SettingsManager.GetDefaultEncryptionType(getActivity()).IntValue();
             encTypePref.setSummary(encTypePref.getEntries()[encTypeIndex]);
             encTypePref.setValueIndex(encTypeIndex);
-            encTypePref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-                @Override
-                public boolean onPreferenceChange(Preference preference, Object o) {
-                    int value = Integer.parseInt(o.toString());
-                    preference.setSummary(((ListPreference) preference).getEntries()[value]);
-                    SettingsManager.SetDefaultEncryptionType(getActivity(), SettingsManager.EncryptionType.FromInt(value));
-                    return true;
-                }
+            encTypePref.setOnPreferenceChangeListener((preference, o) -> {
+                int value = Integer.parseInt(o.toString());
+                preference.setSummary(((ListPreference) preference).getEntries()[value]);
+                SettingsManager.SetDefaultEncryptionType(getActivity(), SettingsManager.EncryptionType.FromInt(value));
+                return true;
+            });
+
+            SwitchPreference fingerPref = (SwitchPreference) findPreference(getString(R.string.pref_enctype_fingerprint));
+            fingerPref.setChecked(SettingsManager.IsFingerprintEnabled(getActivity()));
+            fingerPref.setOnPreferenceChangeListener((preference, o) -> {
+                SettingsManager.SetFingerprintEnabled(getActivity(), (Boolean) o);
+                return true;
             });
         }
 
@@ -287,12 +288,9 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 }
             });
 
-            findPreference(getResources().getString(R.string.id_vcs_git_pull)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-                @Override
-                public boolean onPreferenceClick(Preference preference) {
-                    startActivity(new Intent(getActivity(), GitPullActivity.class));
-                    return true;
-                }
+            findPreference(getResources().getString(R.string.id_vcs_git_pull)).setOnPreferenceClickListener(preference -> {
+                startActivity(new Intent(getActivity(), GitPullActivity.class));
+                return true;
             });
             reload();
         }
@@ -392,38 +390,30 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 gitInterface.GetRefs(new OnResponseListener<GitRef[]>() {
                     @Override
                     public void onResponse(final GitRef[] result) {
-                        getActivity().runOnUiThread(new Runnable() {
-                            @Override
-                            public void run() {
-                                CharSequence[] refNames = new CharSequence[result.length];
-                                CharSequence[] refs = new CharSequence[result.length];
-                                int selected = -1;
-
-                                for (int i = 0; i < result.length; ++i) {
-                                    refNames[i] = result[i].GetBranchName();
-                                    refs[i] = result[i].GetBranch();
-                                    if (result[i].GetBranch().equals(versioning.GetBranch()))
-                                        selected = i;
-                                }
-                                gitBranches.setEntries(refNames);
-                                gitBranches.setEntryValues(refs);
-                                gitBranches.setEnabled(true);
-                                if (selected >= 0) {
-                                    gitBranches.setSummary(refNames[selected]);
-                                    gitBranches.setValueIndex(selected);
-                                }
+                        getActivity().runOnUiThread(() -> {
+                            CharSequence[] refNames = new CharSequence[result.length];
+                            CharSequence[] refs = new CharSequence[result.length];
+                            int selected = -1;
+
+                            for (int i = 0; i < result.length; ++i) {
+                                refNames[i] = result[i].GetBranchName();
+                                refs[i] = result[i].GetBranch();
+                                if (result[i].GetBranch().equals(versioning.GetBranch()))
+                                    selected = i;
+                            }
+                            gitBranches.setEntries(refNames);
+                            gitBranches.setEntryValues(refs);
+                            gitBranches.setEnabled(true);
+                            if (selected >= 0) {
+                                gitBranches.setSummary(refNames[selected]);
+                                gitBranches.setValueIndex(selected);
                             }
                         });
                     }
 
                     @Override
                     public void onError(final String msg, final Throwable e) {
-                        getActivity().runOnUiThread(new Runnable() {
-                            @Override
-                            public void run() {
-                                Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show();
-                            }
-                        });
+                        getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show());
                     }
                 });
             } else {
@@ -454,61 +444,47 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             addPreferencesFromResource(R.xml.pref_gpg);
             setHasOptionsMenu(true);
 
-            findPreference(getResources().getString(R.string.id_gpg_keyfile)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-                @Override
-                public boolean onPreferenceClick(Preference preference) {
-                    Intent i = new Intent(Intent.ACTION_GET_CONTENT);
-                    i.setType("*/*");
-                    i.addCategory(Intent.CATEGORY_OPENABLE);
-                    GPGPreferenceFragment.this.startActivityForResult(i, ACTIVITY_REQUEST_CODE_BROWSEGPG);
-                    return true;
-                }
+            findPreference(getResources().getString(R.string.id_gpg_keyfile)).setOnPreferenceClickListener(preference -> {
+                Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+                i.setType("*/*");
+                i.addCategory(Intent.CATEGORY_OPENABLE);
+                GPGPreferenceFragment.this.startActivityForResult(i, ACTIVITY_REQUEST_CODE_BROWSEGPG);
+                return true;
             });
-            findPreference(getResources().getString(R.string.id_gpg_password)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-                @Override
-                public boolean onPreferenceClick(Preference preference) {
-                    try {
-                        GPGUtil.ChangePassword(getActivity(), new PasswordPicker.ChangePasswordPicker(getActivity()), new OnResponseListener<Void>() {
-                            @Override
-                            public void onResponse(Void result) {
-                                updateGpgFileLabel();
-                            }
+            findPreference(getResources().getString(R.string.id_gpg_password)).setOnPreferenceClickListener(preference -> {
+                try {
+                    GPGUtil.ChangePassword(getActivity(), PasswordPickerFactory.GetPasswordEditor(getActivity()), new OnResponseListener<Void>() {
+                        @Override
+                        public void onResponse(Void result) {
+                            updateGpgFileLabel();
+                        }
 
-                            @Override
-                            public void onError(String msg, Throwable e) {
-                                Toast.makeText(getActivity(), "Error: " + msg, Toast.LENGTH_LONG).show();
-                            }
-                        });
-                    } catch (IOException e) {
-                        Toast.makeText(getActivity(), "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
-                    }
-                    return true;
+                        @Override
+                        public void onError(String msg, Throwable e) {
+                            Toast.makeText(getActivity(), "Error: " + msg, Toast.LENGTH_LONG).show();
+                        }
+                    });
+                } catch (IOException e) {
+                    Toast.makeText(getActivity(), "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
                 }
+                return true;
             });
-            findPreference(getResources().getString(R.string.id_gpg_export)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-                @Override
-                public boolean onPreferenceClick(Preference preference) {
-                    try {
-                        if (!GPGUtil.CheckIsPasswordProtected(getActivity())) {
-                            new AlertPrompt(getActivity())
-                                    .setCancelable(true)
-                                    .setPositiveButton(R.string.yes, new AlertPrompt.OnClickListener() {
-                                        @Override
-                                        public void onClick(DialogInterface dialogInterface, View view) {
-                                            DoExportKey();
-                                        }
-                                    })
-                                    .setNegativeButton(R.string.no, null)
-                                    .setTitle(R.string.are_you_sure)
-                                    .setView(new TextView(getActivity()).SetText(R.string.unprotected_key))
-                                    .show();
-                        } else {
-                            DoExportKey();
-                        }
-                        return true;
-                    } catch (IOException e) {
-                        return false;
+            findPreference(getResources().getString(R.string.id_gpg_export)).setOnPreferenceClickListener(preference -> {
+                try {
+                    if (!GPGUtil.CheckIsPasswordProtected(getActivity())) {
+                        AlertPromptGenerator.StaticMake(getActivity())
+                                .setCancelable(true)
+                                .setPositiveButton(R.string.yes, (DialogInterface dialogInterface, View view) -> DoExportKey())
+                                .setNegativeButton(R.string.no, null)
+                                .setTitle(R.string.are_you_sure)
+                                .setView(new TextView(getActivity()).SetText(R.string.unprotected_key))
+                                .show();
+                    } else {
+                        DoExportKey();
                     }
+                    return true;
+                } catch (IOException e) {
+                    return false;
                 }
             });
             updateGpgFileLabel();

+ 32 - 49
app/src/main/java/info/knacki/pass/ui/EditPasswordActivity.java

@@ -6,8 +6,6 @@ import android.support.v7.widget.AppCompatCheckBox;
 import android.support.v7.widget.AppCompatEditText;
 import android.text.Editable;
 import android.text.InputType;
-import android.view.View;
-import android.widget.CompoundButton;
 import android.widget.Toast;
 
 import java.io.File;
@@ -17,7 +15,7 @@ import java.util.logging.Logger;
 import info.knacki.pass.R;
 import info.knacki.pass.io.FileInterfaceFactory;
 import info.knacki.pass.io.OnResponseListener;
-import info.knacki.pass.ui.passwordPicker.PasswordPicker;
+import info.knacki.pass.ui.passwordPicker.PasswordPickerFactory;
 
 public class EditPasswordActivity extends AppCompatActivity {
     private static final Logger log = Logger.getLogger(EditPasswordActivity.class.getName());
@@ -37,38 +35,29 @@ public class EditPasswordActivity extends AppCompatActivity {
         }
         fOutputFile = new File(path);
         fTextEdit = findViewById(R.id.password_edit);
-        ((AppCompatCheckBox) findViewById(R.id.showPasswordCheckbox)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            @Override
-            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
-                final int cursorPos = fTextEdit.getSelectionEnd();
-                fTextEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | (compoundButton.isChecked() ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_TEXT_VARIATION_PASSWORD));
-                fTextEdit.setSelection(cursorPos);
-            }
+        ((AppCompatCheckBox) findViewById(R.id.showPasswordCheckbox)).setOnCheckedChangeListener((compoundButton, b) -> {
+            final int cursorPos = fTextEdit.getSelectionEnd();
+            fTextEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | (compoundButton.isChecked() ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_TEXT_VARIATION_PASSWORD));
+            fTextEdit.setSelection(cursorPos);
         });
-        findViewById(R.id.saveButton).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                final Editable text = fTextEdit.getText();
-                final String textStr = text == null ? "" : text.toString();
+        findViewById(R.id.saveButton).setOnClickListener(view -> {
+            final Editable text = fTextEdit.getText();
+            final String textStr = text == null ? "" : text.toString();
 
-                FileInterfaceFactory.GetFileInterface(EditPasswordActivity.this, new PasswordPicker(EditPasswordActivity.this), fOutputFile).WriteFile(textStr, new OnResponseListener<Void>() {
-                    @Override
-                    public void onResponse(Void result) {
-                        EditPasswordActivity.this.finish();
-                    }
+            FileInterfaceFactory.GetFileInterface(EditPasswordActivity.this, PasswordPickerFactory.GetPasswordPicker(EditPasswordActivity.this), fOutputFile).WriteFile(textStr, new OnResponseListener<Void>() {
+                @Override
+                public void onResponse(Void result) {
+                    EditPasswordActivity.this.finish();
+                }
 
-                    @Override
-                    public void onError(final String msg, final Throwable e) {
-                        EditPasswordActivity.this.runOnUiThread(new Runnable() {
-                            @Override
-                            public void run() {
-                                Toast.makeText(EditPasswordActivity.this, "Error: " +e.getMessage(), Toast.LENGTH_LONG).show();
-                                log.log(Level.WARNING, msg, e);
-                            }
-                        });
-                    }
-                });
-            }
+                @Override
+                public void onError(final String msg, final Throwable e) {
+                    EditPasswordActivity.this.runOnUiThread(() -> {
+                        Toast.makeText(EditPasswordActivity.this, "Error: " +msg, Toast.LENGTH_LONG).show();
+                        log.log(Level.WARNING, msg, e);
+                    });
+                }
+            });
         });
         boolean editMode = getIntent().getBooleanExtra(INTENT_EDIT, true);
         populateContent(editMode);
@@ -79,31 +68,25 @@ public class EditPasswordActivity extends AppCompatActivity {
 
     protected void populateContent(final boolean requestFocus) {
         fTextEdit.setEnabled(false);
-        FileInterfaceFactory.GetFileInterface(this, new PasswordPicker(this), fOutputFile).ReadFile(new OnResponseListener<String>() {
+        FileInterfaceFactory.GetFileInterface(this, PasswordPickerFactory.GetPasswordPicker(this), fOutputFile).ReadFile(new OnResponseListener<String>() {
             @Override
             public void onResponse(final String pass) {
-                EditPasswordActivity.this.runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        fTextEdit.setEnabled(true);
-                        fTextEdit.setText(pass);
-                        if (requestFocus) {
-                            fTextEdit.setSelection(pass.length());
-                            fTextEdit.requestFocus();
-                        }
+                EditPasswordActivity.this.runOnUiThread(() -> {
+                    fTextEdit.setEnabled(true);
+                    fTextEdit.setText(pass);
+                    if (requestFocus) {
+                        fTextEdit.setSelection(pass.length());
+                        fTextEdit.requestFocus();
                     }
                 });
             }
 
             @Override
             public void onError(final String msg, final Throwable e) {
-                EditPasswordActivity.this.runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        Toast.makeText(EditPasswordActivity.this, "Error: " +msg, Toast.LENGTH_LONG).show();
-                        log.log(Level.WARNING, msg, e);
-                        finish();
-                    }
+                EditPasswordActivity.this.runOnUiThread(() -> {
+                    Toast.makeText(EditPasswordActivity.this, "Error: " +msg, Toast.LENGTH_LONG).show();
+                    log.log(Level.WARNING, msg, e);
+                    finish();
                 });
             }
         });

+ 57 - 104
app/src/main/java/info/knacki/pass/ui/GitPullActivity.java

@@ -1,9 +1,7 @@
 package info.knacki.pass.ui;
 
-import android.content.DialogInterface;
 import android.os.Bundle;
 import android.support.v7.app.AppCompatActivity;
-import android.view.View;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
@@ -31,24 +29,20 @@ import info.knacki.pass.io.OnStreamResponseListener;
 import info.knacki.pass.io.PathUtils;
 import info.knacki.pass.settings.SettingsManager;
 import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
 import info.knacki.pass.ui.alertPrompt.views.ConflictView;
 
 public class GitPullActivity extends AppCompatActivity {
     private final static Logger log = Logger.getLogger(GitPullActivity.class.getName());
     public final static String COMMIT_MSG = "Android pass sync";
 
-    public final static int ACTIVITY_REQUEST_CODE_BROWSEGPG = 1;
-
     private GitInterface fGitInterfage;
     private GitCommit fHeadCommit;
 
     private void onMsg(final String msg) {
-        GitPullActivity.this.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                TextView logView = findViewById(R.id.logView);
-                logView.append(msg +"\n");
-            }
+        GitPullActivity.this.runOnUiThread(() -> {
+            TextView logView = findViewById(R.id.logView);
+            logView.append(msg +"\n");
         });
     }
 
@@ -76,37 +70,24 @@ public class GitPullActivity extends AppCompatActivity {
                 @Override
                 public void onResponse(GitCommit result) {
                     fHeadCommit = result;
-                    GitPullActivity.this.runOnUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            GitPullActivity.this.OnTreeStructureFetched();
-                        }
-                    });
+                    GitPullActivity.this.runOnUiThread(GitPullActivity.this::OnTreeStructureFetched);
                 }
 
                 @Override
                 public void onError(final String msg, Throwable e) {
-                    GitPullActivity.this.runOnUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            ProgressBar pg = findViewById(R.id.progressBar);
-                            pg.setIndeterminate(false);
-                            pg.setMax(1);
-                            pg.setProgress(1);
-                            TextView logView = findViewById(R.id.logView);
-                            logView.append(msg +"\n");
-                            findViewById(R.id.close_bt).setEnabled(true);
-                        }
+                    GitPullActivity.this.runOnUiThread(() -> {
+                        ProgressBar pg = findViewById(R.id.progressBar);
+                        pg.setIndeterminate(false);
+                        pg.setMax(1);
+                        pg.setProgress(1);
+                        TextView logView = findViewById(R.id.logView);
+                        logView.append(msg +"\n");
+                        findViewById(R.id.close_bt).setEnabled(true);
                     });
                 }
             });
         }
-        findViewById(R.id.close_bt).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                GitPullActivity.this.finish();
-            }
-        });
+        findViewById(R.id.close_bt).setOnClickListener(v -> GitPullActivity.this.finish());
     }
 
     public static String LOCALGIT_HASH_VERSION_FILE;
@@ -176,33 +157,22 @@ public class GitPullActivity extends AppCompatActivity {
     }
 
     void AskForConflicts(final GitLocal localVersion, final HashMap<String, GitObject.GitBlob> conflicts, final HashMap<String, GitObject.GitBlob> filesToPull, final Set<String> filesToPush) {
-        runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                AlertPrompt pt = new AlertPrompt(GitPullActivity.this)
-                        .setCancelable(true)
-                        .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
-                            @Override
-                            public void onClick(DialogInterface dialogInterface, View view) {
-                                GitPullActivity.this.finish();
-                            }
-                        })
-                        .setPositiveButton(R.string.ok, new AlertPrompt.OnClickListener() {
-                            @Override
-                            public void onClick(DialogInterface dialogInterface, View v) {
-                                ConflictView.ConflictViewResult viewResult = ((ConflictView) v).GetResult();
-                                for (String s: viewResult.fUseTheir) {
-                                    filesToPull.put(s, conflicts.get(s));
-                                }
-                                filesToPush.addAll(viewResult.fUseMine);
-                                SyncFiles(localVersion, filesToPull, filesToPush);
-                            }
-                        })
-                        .setTitle(R.string.conflictingFiles);
-                ConflictView view = new ConflictView(GitPullActivity.this, pt, conflicts.keySet());
-                pt.setView(view).show();
-                view.UpdateButtonState();
-            }
+        runOnUiThread(() -> {
+            AlertPrompt pt = AlertPromptGenerator.StaticMake(GitPullActivity.this)
+                    .setCancelable(true)
+                    .setNegativeButton(R.string.cancel, (dialogInterface, view) -> GitPullActivity.this.finish())
+                    .setPositiveButton(R.string.ok, (dialogInterface, v) -> {
+                        ConflictView.ConflictViewResult viewResult = ((ConflictView) v).GetResult();
+                        for (String s: viewResult.fUseTheir) {
+                            filesToPull.put(s, conflicts.get(s));
+                        }
+                        filesToPush.addAll(viewResult.fUseMine);
+                        SyncFiles(localVersion, filesToPull, filesToPush);
+                    })
+                    .setTitle(R.string.conflictingFiles);
+            ConflictView view = new ConflictView(GitPullActivity.this, pt, conflicts.keySet());
+            pt.setView(view).show();
+            view.UpdateButtonState();
         });
     }
 
@@ -223,31 +193,25 @@ public class GitPullActivity extends AppCompatActivity {
         final OnStreamResponseListener<Void> allDone = new OnStreamResponseListener<Void>() {
             @Override
             public void onResponse(Void result) {
-                GitPullActivity.this.runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        ProgressBar pg = findViewById(R.id.progressBar);
-                        pg.setIndeterminate(false);
-                        pg.setMax(1);
-                        pg.setProgress(1);
-                        localVersion.Write(new File(LOCALGIT_HASH_VERSION_FILE));
-                        RmEmptyDirs(new File(PathUtils.GetPassDir(GitPullActivity.this)), true);
-                        findViewById(R.id.close_bt).setEnabled(true);
-                    }
+                GitPullActivity.this.runOnUiThread(() -> {
+                    ProgressBar pg = findViewById(R.id.progressBar);
+                    pg.setIndeterminate(false);
+                    pg.setMax(1);
+                    pg.setProgress(1);
+                    localVersion.Write(new File(LOCALGIT_HASH_VERSION_FILE));
+                    RmEmptyDirs(new File(PathUtils.GetPassDir(GitPullActivity.this)), true);
+                    findViewById(R.id.close_bt).setEnabled(true);
                 });
             }
 
             @Override
             public void onError(String msg, Throwable e) {
-                GitPullActivity.this.runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        ProgressBar pg = findViewById(R.id.progressBar);
-                        pg.setIndeterminate(false);
-                        pg.setMax(1);
-                        pg.setProgress(1);
-                        findViewById(R.id.close_bt).setEnabled(true);
-                    }
+                GitPullActivity.this.runOnUiThread(() -> {
+                    ProgressBar pg = findViewById(R.id.progressBar);
+                    pg.setIndeterminate(false);
+                    pg.setMax(1);
+                    pg.setProgress(1);
+                    findViewById(R.id.close_bt).setEnabled(true);
                 });
             }
 
@@ -257,16 +221,13 @@ public class GitPullActivity extends AppCompatActivity {
             }
         };
 
-        final Runnable afterFetching = new Runnable() {
-            @Override
-            public void run() {
-                if (filesToPush.size() > 0) {
-                    GitPullActivity.this.onMsg("Updating remote repository");
-                    PushBlobs(filesToPush, localVersion, allDone);
-                } else {
-                    GitPullActivity.this.onMsg("Nothing to push");
-                    allDone.onResponse(null);
-                }
+        final Runnable afterFetching = () -> {
+            if (filesToPush.size() > 0) {
+                GitPullActivity.this.onMsg("Updating remote repository");
+                PushBlobs(filesToPush, localVersion, allDone);
+            } else {
+                GitPullActivity.this.onMsg("Nothing to push");
+                allDone.onResponse(null);
             }
         };
 
@@ -307,14 +268,11 @@ public class GitPullActivity extends AppCompatActivity {
                 final GitObject.GitBlob blob = files.peek().getValue();
                 final OnResponseListener<byte[]> _this = this;
 
-                GitPullActivity.this.runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        WriteFile(filename, localVersion, blob, result);
-                        logView.append("Done fetching " +files.peek().getValue().GetFilename() +"\n");
-                        files.pop();
-                        DownloadNext(files, localVersion, _this, resp);
-                    }
+                GitPullActivity.this.runOnUiThread(() -> {
+                    WriteFile(filename, localVersion, blob, result);
+                    logView.append("Done fetching " +files.peek().getValue().GetFilename() +"\n");
+                    files.pop();
+                    DownloadNext(files, localVersion, _this, resp);
                 });
             }
 
@@ -322,12 +280,7 @@ public class GitPullActivity extends AppCompatActivity {
             public void onError(final String msg, Throwable e) {
                 log.log(Level.SEVERE, msg, e);
 
-                GitPullActivity.this.runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        logView.append("Error while fetching " +files.peek().getValue().GetFilename() +": " +msg);
-                    }
-                });
+                GitPullActivity.this.runOnUiThread(() -> logView.append("Error while fetching " +files.peek().getValue().GetFilename() +": " +msg));
 
                 files.pop();
                 if (!files.empty()) {

+ 56 - 93
app/src/main/java/info/knacki/pass/ui/MainActivity.java

@@ -3,14 +3,12 @@ package info.knacki.pass.ui;
 import android.Manifest;
 import android.content.ClipData;
 import android.content.ClipboardManager;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
 import android.support.v7.app.AppCompatActivity;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.View;
 import android.widget.ScrollView;
 import android.widget.Toast;
 
@@ -29,13 +27,13 @@ import info.knacki.pass.io.OnResponseListener;
 import info.knacki.pass.io.PathUtils;
 import info.knacki.pass.settings.SettingsManager;
 import info.knacki.pass.settings.ui.SettingsActivity;
-import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
 import info.knacki.pass.ui.alertPrompt.views.SimpleTextEdit;
 import info.knacki.pass.ui.alertPrompt.views.TextEditAndCheckbox;
 import info.knacki.pass.ui.alertPrompt.views.TextView;
 import info.knacki.pass.ui.passwordList.EditablePasswordListView;
 import info.knacki.pass.ui.passwordList.PasswordEditListener;
-import info.knacki.pass.ui.passwordPicker.PasswordPicker;
+import info.knacki.pass.ui.passwordPicker.PasswordPickerFactory;
 
 public class MainActivity extends AppCompatActivity implements PasswordEditListener {
     private final Logger log = Logger.getLogger(MainActivity.class.getName());
@@ -44,31 +42,23 @@ public class MainActivity extends AppCompatActivity implements PasswordEditListe
     protected TextEditAndCheckbox lastFileName = null;
 
     protected void CreateNewFolder() {
-        new AlertPrompt(this)
+        AlertPromptGenerator.StaticMake(this)
                 .setCancelable(true)
                 .setView(new SimpleTextEdit(this))
-                .setPositiveButton(R.string.add, new AlertPrompt.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialogInterface, View view) {
-                        final String filename = ((SimpleTextEdit) view).getStr();
+                .setPositiveButton(R.string.add, (dialogInterface, view) -> {
+                    final String filename = ((SimpleTextEdit) view).getStr();
 
-                        if (filename.length() == 0) {
-                            Toast.makeText(MainActivity.this, "Error: Empty file name", Toast.LENGTH_LONG).show();
-                            return;
-                        }
-                        File f = new File(vPasswordListView.fCurrentDir +"/" +filename);
-                        if (!f.mkdir()) {
-                            Toast.makeText(MainActivity.this, "Error: cannot create folder", Toast.LENGTH_LONG).show();
-                        }
-                        vPasswordListView.refresh();
+                    if (filename.length() == 0) {
+                        Toast.makeText(MainActivity.this, "Error: Empty file name", Toast.LENGTH_LONG).show();
+                        return;
                     }
-                })
-                .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialogInterface, View s) {
-                        dialogInterface.cancel();
+                    File f = new File(vPasswordListView.fCurrentDir +"/" +filename);
+                    if (!f.mkdir()) {
+                        Toast.makeText(MainActivity.this, "Error: cannot create folder", Toast.LENGTH_LONG).show();
                     }
+                    vPasswordListView.refresh();
                 })
+                .setNegativeButton(R.string.cancel, (dialogInterface, s) -> dialogInterface.cancel())
                 .setTitle(R.string.add_title)
                 .show();
     }
@@ -77,45 +67,37 @@ public class MainActivity extends AppCompatActivity implements PasswordEditListe
         if (lastFileName == null) {
             lastFileName = new TextEditAndCheckbox(this);
         }
-        new AlertPrompt(this)
+        AlertPromptGenerator.StaticMake(this)
                 .setCancelable(true)
                 .setView(new TextEditAndCheckbox(this)
                     .setText(lastFileName.getStr())
                     .setCheckboxCaption(R.string.add_generate)
                     .setChecked(lastFileName.isChecked()))
-                .setPositiveButton(R.string.add, new AlertPrompt.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialogInterface, View view) {
-                        lastFileName = (TextEditAndCheckbox) view;
-                        final String filename = lastFileName.getStr();
-                        if (filename.length() == 0) {
-                            Toast.makeText(MainActivity.this, "Error: Empty file name", Toast.LENGTH_LONG).show();
-                            return;
-                        }
-                        File f = new File(vPasswordListView.fCurrentDir +"/" +filename +FileInterfaceFactory.GetExtension(SettingsManager.GetDefaultEncryptionType(MainActivity.this)));
-                        try {
-                            f.createNewFile();
-                            vPasswordListView.refresh();
-                        }
-                        catch (IOException e) {
-                            Toast.makeText(MainActivity.this, "Error: " +e.getMessage(), Toast.LENGTH_LONG).show();
-                            log.log(Level.WARNING, e.getMessage(), e);
-                            return;
-                        }
-                        if (lastFileName.isChecked()) {
-                            GeneratePassword(f);
-                        } else {
-                            EditFile(f, true);
-                        }
-                        lastFileName = null;
+                .setPositiveButton(R.string.add, (dialogInterface, view) -> {
+                    lastFileName = (TextEditAndCheckbox) view;
+                    final String filename = lastFileName.getStr();
+                    if (filename.length() == 0) {
+                        Toast.makeText(MainActivity.this, "Error: Empty file name", Toast.LENGTH_LONG).show();
+                        return;
                     }
-                })
-                .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialogInterface, View s) {
-                        dialogInterface.cancel();
+                    File f = new File(vPasswordListView.fCurrentDir +"/" +filename +FileInterfaceFactory.GetExtension(SettingsManager.GetDefaultEncryptionType(MainActivity.this)));
+                    try {
+                        f.createNewFile();
+                        vPasswordListView.refresh();
+                    }
+                    catch (IOException e) {
+                        Toast.makeText(MainActivity.this, "Error: " +e.getMessage(), Toast.LENGTH_LONG).show();
+                        log.log(Level.WARNING, e.getMessage(), e);
+                        return;
                     }
+                    if (lastFileName.isChecked()) {
+                        GeneratePassword(f);
+                    } else {
+                        EditFile(f, true);
+                    }
+                    lastFileName = null;
                 })
+                .setNegativeButton(R.string.cancel, (dialogInterface, s) -> dialogInterface.cancel())
                 .setTitle(R.string.add_title)
                 .show();
     }
@@ -181,37 +163,26 @@ public class MainActivity extends AppCompatActivity implements PasswordEditListe
     protected void GeneratePassword(final File f) {
         final PasswordGeneratorWizard wiz = new PasswordGeneratorWizard(this);
 
-        new AlertPrompt(this)
+        AlertPromptGenerator.StaticMake(this)
             .setCancelable(true)
             .setView(wiz)
-            .setPositiveButton(R.string.add, new AlertPrompt.OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialogInterface, View view) {
-                    IFileInterface writer = FileInterfaceFactory.GetFileInterface(MainActivity.this, new PasswordPicker(MainActivity.this), f);
-                    writer.WriteFile(PasswordGenerator.generate(wiz), new OnResponseListener<Void>() {
-                        @Override
-                        public void onResponse(Void result) {
-                        }
+            .setPositiveButton(R.string.add, (dialogInterface, view) -> {
+                IFileInterface writer = FileInterfaceFactory.GetFileInterface(MainActivity.this, PasswordPickerFactory.GetPasswordPicker(MainActivity.this), f);
+                writer.WriteFile(PasswordGenerator.generate(wiz), new OnResponseListener<Void>() {
+                    @Override
+                    public void onResponse(Void result) {
+                    }
 
-                        @Override
-                        public void onError(final String msg, final Throwable e) {
-                            MainActivity.this.runOnUiThread(new Runnable() {
-                                @Override
-                                public void run() {
-                                    Toast.makeText(MainActivity.this, "Error: " +msg, Toast.LENGTH_LONG).show();
-                                    log.log(Level.WARNING, msg, e);
-                                }
-                            });
-                        }
-                    });
-                }
-            })
-            .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialogInterface, View s) {
-                    dialogInterface.cancel();
-                }
+                    @Override
+                    public void onError(final String msg, final Throwable e) {
+                        MainActivity.this.runOnUiThread(() -> {
+                            Toast.makeText(MainActivity.this, "Error: " +msg, Toast.LENGTH_LONG).show();
+                            log.log(Level.WARNING, msg, e);
+                        });
+                    }
+                });
             })
+            .setNegativeButton(R.string.cancel, (dialogInterface, s) -> dialogInterface.cancel())
             .setTitle(R.string.generate_title)
             .show();
     }
@@ -246,20 +217,12 @@ public class MainActivity extends AppCompatActivity implements PasswordEditListe
     @Override
     public void OnRemoveDirectory(final File f) {
         if (f.listFiles().length > 0) {
-            new AlertPrompt(this)
+            AlertPromptGenerator.StaticMake(this)
                     .setTitle(R.string.are_you_sure)
                     .setView(new TextView(this)
                             .SetText(R.string.about_to_rm_directory))
-                    .setPositiveButton(R.string.ok, new AlertPrompt.OnClickListener() {
-                        @Override
-                        public void onClick(DialogInterface dialogInterface, View view) {
-                            DoRemoveDir(f);
-                        }
-                    })
-                    .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
-                        @Override
-                        public void onClick(DialogInterface dialogInterface, View view) {
-                        }
+                    .setPositiveButton(R.string.ok, (dialogInterface, view) -> DoRemoveDir(f))
+                    .setNegativeButton(R.string.cancel, (dialogInterface, view) -> {
                     })
                     .show();
 
@@ -270,7 +233,7 @@ public class MainActivity extends AppCompatActivity implements PasswordEditListe
 
     @Override
     public void OnCopyToClipboard(File f) {
-        FileInterfaceFactory.GetFileInterface(this, new PasswordPicker(this), f).ReadFile(new OnResponseListener<String>() {
+        FileInterfaceFactory.GetFileInterface(this, PasswordPickerFactory.GetPasswordPicker(this), f).ReadFile(new OnResponseListener<String>() {
             @Override
             public void onResponse(String result) {
                 ClipboardManager clip = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);

+ 14 - 7
app/src/main/java/info/knacki/pass/ui/alertPrompt/AlertPrompt.java

@@ -35,7 +35,7 @@ public class AlertPrompt {
         }
     }
 
-    public AlertPrompt(Context c) {
+    AlertPrompt(Context c) {
         fAlertBuilder = new AlertDialog.Builder(c);
         fOnDismiss = new DismissListener();
     }
@@ -46,18 +46,17 @@ public class AlertPrompt {
         return this;
     }
 
+    public View getView() {
+        return fView;
+    }
+
     public AlertPrompt setCancelable(boolean value) {
         fAlertBuilder.setCancelable(value);
         return this;
     }
 
     public AlertPrompt setPositiveButton(int textId, final OnClickListener listener) {
-        fAlertBuilder.setPositiveButton(textId, new DialogInterface.OnClickListener() {
-            @Override
-            public void onClick(DialogInterface dialogInterface, int i) {
-                listener.onClick(dialogInterface, fView);
-            }
-        });
+        fAlertBuilder.setPositiveButton(textId, (dialogInterface, i) -> listener.onClick(dialogInterface, fView));
         return this;
     }
 
@@ -82,6 +81,14 @@ public class AlertPrompt {
         return this;
     }
 
+    public AlertPrompt close() {
+        if (fDialog != null) {
+            fDialog.dismiss();
+            fDialog = null;
+        }
+        return this;
+    }
+
     public AlertPrompt setPositiveButtonEnabled(boolean enabled) {
         fDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
         return this;

+ 15 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/AlertPromptGenerator.java

@@ -0,0 +1,15 @@
+package info.knacki.pass.ui.alertPrompt;
+
+import android.content.Context;
+
+import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+
+public class AlertPromptGenerator {
+    public AlertPrompt Generate(Context ctx) {
+        return new AlertPrompt(ctx);
+    }
+
+    public static AlertPrompt StaticMake(Context ctx) {
+        return new AlertPrompt(ctx);
+    }
+}

+ 1 - 1
app/src/main/java/info/knacki/pass/ui/alertPrompt/ServiceAlertPrompt.java

@@ -8,7 +8,7 @@ import android.view.WindowManager;
 public class ServiceAlertPrompt extends AlertPrompt {
     protected final View fInputView;
 
-    public ServiceAlertPrompt(Context c, View inputView) {
+    ServiceAlertPrompt(Context c, View inputView) {
         super(c);
         fInputView = inputView;
     }

+ 17 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/ServiceAlertPromptGenerator.java

@@ -0,0 +1,17 @@
+package info.knacki.pass.ui.alertPrompt;
+
+import android.content.Context;
+import android.view.View;
+
+public class ServiceAlertPromptGenerator extends AlertPromptGenerator {
+    private final View fInputView;
+
+    public ServiceAlertPromptGenerator(View inputView) {
+        fInputView = inputView;
+    }
+
+    @Override
+    public AlertPrompt Generate(Context ctx) {
+        return new ServiceAlertPrompt(ctx, fInputView);
+    }
+}

+ 9 - 16
app/src/main/java/info/knacki/pass/ui/alertPrompt/views/ConflictView.java

@@ -1,7 +1,6 @@
 package info.knacki.pass.ui.alertPrompt.views;
 
 import android.content.Context;
-import android.widget.CompoundButton;
 import android.widget.HorizontalScrollView;
 import android.widget.LinearLayout;
 import android.widget.RadioButton;
@@ -51,28 +50,22 @@ public class ConflictView extends HorizontalScrollView {
 
     private void createListeners(RadioButton mine, RadioButton their, final String name) {
         fConflicts.put(name, new ConflictState());
-        mine.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            @Override
-            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-                if (isChecked) {
-                    fConflicts.get(name).UseMine();
-                    UpdateButtonState();
-                }
+        mine.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            if (isChecked) {
+                fConflicts.get(name).UseMine();
+                UpdateButtonState();
             }
         });
-        their.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            @Override
-            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-                if (isChecked) {
-                    fConflicts.get(name).UseTheir();
-                    UpdateButtonState();
-                }
+        their.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            if (isChecked) {
+                fConflicts.get(name).UseTheir();
+                UpdateButtonState();
             }
         });
     }
 
     public ConflictViewResult GetResult() {
-        ConflictViewResult res = new ConflictViewResult(new HashSet<String>(), new HashSet<String>());
+        ConflictViewResult res = new ConflictViewResult(new HashSet<>(), new HashSet<>());
         for (Map.Entry<String, ConflictState> i: fConflicts.entrySet()) {
             if (i.getValue().fUseMine)
                 res.fUseMine.add(i.getKey());

+ 25 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/views/FingerprintView.java

@@ -0,0 +1,25 @@
+package info.knacki.pass.ui.alertPrompt.views;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.support.v7.widget.AppCompatImageView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import info.knacki.pass.R;
+
+public class FingerprintView extends LinearLayout {
+    private final AppCompatImageView image;
+
+    public FingerprintView(Context context) {
+        super(context);
+        @SuppressLint("InflateParams") View v = LayoutInflater.from(context).inflate(R.layout.fingerprint_picker, null);
+        addView(v);
+        image = v.findViewById(R.id.fingerprint_icon);
+    }
+
+    public void onAuthFailed() {
+        image.setBackgroundResource(R.drawable.ic_fingerprint_fail);
+    }
+}

+ 2 - 3
app/src/main/java/info/knacki/pass/ui/alertPrompt/views/PasswordTextEdit.java

@@ -11,7 +11,6 @@ import info.knacki.pass.ui.alertPrompt.integratedKeyboard.KeyboardWidget;
 
 public class PasswordTextEdit extends LinearLayout implements KeyboardWidget.OnKeyListener {
     private final AppCompatEditText fTextEdit;
-    private final KeyboardView fKeyboard;
 
     public PasswordTextEdit(Context c) {
         super(c);
@@ -19,8 +18,8 @@ public class PasswordTextEdit extends LinearLayout implements KeyboardWidget.OnK
         fTextEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
         fTextEdit.setFocusable(false);
         addView(fTextEdit);
-        fKeyboard = new KeyboardWidget(c).SetListener(this);
-        addView(fKeyboard);
+        KeyboardView keyboard = new KeyboardWidget(c).SetListener(this);
+        addView(keyboard);
         setOrientation(VERTICAL);
     }
 

+ 7 - 11
app/src/main/java/info/knacki/pass/ui/passwordList/EditablePasswordListView.java

@@ -2,7 +2,6 @@ package info.knacki.pass.ui.passwordList;
 
 import android.app.Activity;
 import android.view.MenuItem;
-import android.view.View;
 import android.widget.PopupMenu;
 
 import java.io.File;
@@ -19,16 +18,13 @@ public class EditablePasswordListView<T extends Activity & PasswordEditListener>
     protected void AddContextMenu(final FileIdent file, final PasswordView pv) {
         if (file.isParent())
             return;
-        pv.setOnLongClickListener(new OnLongClickListener() {
-            @Override
-            public boolean onLongClick(View v) {
-                fSelectedPassword = pv;
-                PopupMenu menu = new PopupMenu(fClickListener, pv);
-                menu.getMenuInflater().inflate(file.fIsDir ? R.menu.context_dir_menu : R.menu.context_file_menu, menu.getMenu());
-                menu.setOnMenuItemClickListener(EditablePasswordListView.this);
-                menu.show();
-                return true;
-            }
+        pv.setOnLongClickListener(v -> {
+            fSelectedPassword = pv;
+            PopupMenu menu = new PopupMenu(fClickListener, pv);
+            menu.getMenuInflater().inflate(file.fIsDir ? R.menu.context_dir_menu : R.menu.context_file_menu, menu.getMenu());
+            menu.setOnMenuItemClickListener(EditablePasswordListView.this);
+            menu.show();
+            return true;
         });
     }
 

+ 9 - 13
app/src/main/java/info/knacki/pass/ui/passwordList/PasswordListView.java

@@ -2,7 +2,6 @@ package info.knacki.pass.ui.passwordList;
 
 import android.content.Context;
 import android.support.annotation.NonNull;
-import android.view.View;
 import android.widget.LinearLayout;
 
 import java.io.File;
@@ -50,19 +49,16 @@ public class PasswordListView<T extends Context & PasswordClickListener> extends
 
     protected PasswordView CreateView(final FileIdent file) {
         PasswordView pv = new PasswordView(getContext(), file.fIsDir ? ((file.isParent() ? PasswordView.TYPE_PARENT : 0) | PasswordView.TYPE_DIR) : PasswordView.TYPE_PASSWORD, file.fName);
-        pv.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                File f = new File(fCurrentDir +"/" +file.fName);
-                if (file.isParent()) {
-                    f = new File(fCurrentDir).getParentFile();
-                    DisplayDir(f);
-                }
-                if (f.isDirectory())
-                    DisplayDir(f);
-                else
-                    fClickListener.OnPasswordClicked(f);
+        pv.setOnClickListener(view -> {
+            File f = new File(fCurrentDir +"/" +file.fName);
+            if (file.isParent()) {
+                f = new File(fCurrentDir).getParentFile();
+                DisplayDir(f);
             }
+            if (f.isDirectory())
+                DisplayDir(f);
+            else
+                fClickListener.OnPasswordClicked(f);
         });
         return pv;
     }

+ 353 - 0
app/src/main/java/info/knacki/pass/ui/passwordPicker/FingerprintPicker.java

@@ -0,0 +1,353 @@
+package info.knacki.pass.ui.passwordPicker;
+
+import android.content.Context;
+import android.os.Build;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.support.annotation.RequiresApi;
+import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
+import android.support.v4.os.CancellationSignal;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+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.ArrayDeque;
+import java.util.Queue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+
+import info.knacki.pass.R;
+import info.knacki.pass.io.FileInterfaceFactory;
+import info.knacki.pass.io.FileUtils;
+import info.knacki.pass.io.OnErrorListener;
+import info.knacki.pass.io.OnResponseListener;
+import info.knacki.pass.io.PathUtils;
+import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
+import info.knacki.pass.ui.alertPrompt.views.FingerprintView;
+
+@RequiresApi(api = Build.VERSION_CODES.M)
+class FingerprintPicker extends PasswordPicker {
+    private static final Logger log = Logger.getLogger(FingerprintPicker.class.getName());
+    private static final String KEY_STORE_PROVIDER = "AndroidKeyStore";
+    private static final String KEY_NAME = "AndroidPassFingerprintKey";
+    private static final String KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+    private static final String KEY_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;
+    private static final String KEY_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;
+
+    private final File fFile;
+    private IvAndPayload fEncrypted;
+    private Queue<String> fPassword;
+    private Queue<String> fPasswordIterator;
+    private AlertPrompt fDisplayingPrompt;
+
+    private static final class IvAndPayload {
+        static final char SEPARATOR = '-';
+        final byte[] fIV;
+        final byte[] fPayload;
+
+        IvAndPayload(Cipher c, byte[] payload) {
+            fIV = c.getIV();
+            fPayload = payload;
+        }
+
+        IvAndPayload(String raw) throws Base64DataException {
+            int sep = raw.indexOf(SEPARATOR);
+            if (sep < 0)
+                throw new Base64DataException("Cannot find separator");
+            fIV = Base64.decode(raw.substring(0, sep), Base64.NO_WRAP);
+            fPayload = Base64.decode(raw.substring(sep + 1), Base64.NO_WRAP);
+        }
+
+        public String toString() {
+            return Base64.encodeToString(fIV, Base64.NO_WRAP) + SEPARATOR + Base64.encodeToString(fPayload, Base64.NO_WRAP);
+        }
+    }
+
+    FingerprintPicker(Context ctx, AlertPromptGenerator alertFactory) {
+        super(ctx, alertFactory);
+        fFile = new File(PathUtils.GetFingerprintFile(ctx));
+        fPassword = fPasswordIterator = null;
+    }
+
+    private FingerprintManagerCompat GetFingerprintManager() {
+        return FingerprintManagerCompat.from(fContext);
+    }
+
+    private Key GetKey(KeyStore keyStore) {
+        try {
+            return keyStore.getKey(KEY_NAME, null);
+        }
+        catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
+            log.log(Level.SEVERE, e.getMessage(), e);
+            return null;
+        }
+    }
+
+    private Key TryGetKey(OnErrorListener resp) {
+        KeyStore keyStore;
+        try {
+            keyStore = KeyStore.getInstance(KEY_STORE_PROVIDER);
+            keyStore.load(null);
+        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
+            final String msg = "Cannot get android keystore";
+            resp.onError(msg, e);
+            log.log(Level.SEVERE, msg, e);
+            return null;
+        }
+        return GetKey(keyStore);
+    }
+
+    private boolean GenerateKey(OnErrorListener resp) {
+        log.info("Generating new key");
+        try {
+            KeyGenerator keyGenerator;
+            keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, KEY_STORE_PROVIDER);
+            keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+                    .setBlockModes(KEY_BLOCK_MODE)
+                    .setEncryptionPaddings(KEY_PADDING)
+                    .build());
+            keyGenerator.generateKey();
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
+            final String msg = "Cannot create Key generator";
+            resp.onError(msg, e);
+            log.log(Level.SEVERE, msg, e);
+            return false;
+        }
+        return true;
+    }
+
+    private FingerprintManagerCompat.CryptoObject InitCryptoObject(IvAndPayload ivForDecoding, OnErrorListener resp) {
+        Key key = TryGetKey(resp);
+        if (key == null) {
+            if (!GenerateKey(resp)) {
+                // Unrecoverable failure, error already handled
+                return null;
+            }
+            key = TryGetKey(resp);
+            if (key == null) {
+                // Unrecoverable failure
+                final String msg = "Cannot get android keystore";
+                log.log(Level.SEVERE, msg);
+                resp.onError(msg, null);
+                return null;
+            }
+        }
+
+        Cipher c;
+        try {
+            c = InitCipher(key, ivForDecoding);
+        }
+        catch (InvalidKeyException e) {
+            return null;
+        }
+        catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
+            final String msg = "Cannot create Cipher";
+            resp.onError(msg, e);
+            log.log(Level.SEVERE, msg, e);
+            return null;
+        }
+        return new FingerprintManagerCompat.CryptoObject(c);
+    }
+
+    private static Cipher InitCipher(Key key, IvAndPayload ivForDecoding) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
+        Cipher c = Cipher.getInstance(KEY_ALGORITHM +"/" +KEY_BLOCK_MODE +"/" +KEY_PADDING);
+        if (ivForDecoding == null)
+            c.init(Cipher.ENCRYPT_MODE, key);
+        else
+            c.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivForDecoding.fIV));
+        return c;
+    }
+
+    private void DoWriteFile(FingerprintManagerCompat.CryptoObject cryptoObject, String content) {
+        try {
+            Cipher cipher = cryptoObject.getCipher();
+            if (cipher == null) {
+                log.severe("Cannot write content: cipher is null");
+                return;
+            }
+            byte[] encrypted = cipher.doFinal(content.getBytes());
+            Writer writer = new FileWriter(fFile);
+            writer.write((new IvAndPayload(cipher, encrypted)).toString());
+            writer.close();
+        }
+        catch (IOException | BadPaddingException | IllegalBlockSizeException | NullPointerException e) {
+            log.log(Level.SEVERE, e.getMessage(), e);
+        }
+    }
+
+    private void DisplayPrompt(IvAndPayload ivForDecoding, OnResponseListener<FingerprintManagerCompat.CryptoObject> resp) {
+        final CancellationSignal signal = new CancellationSignal();
+        final FingerprintView fingerprintView = new FingerprintView(fContext);
+        fDisplayingPrompt = fAlertFactory.Generate(fContext)
+                .setView(fingerprintView)
+                .setTitle(R.string.pushfinger)
+                .setNegativeButton(R.string.cancel, (dialogInterface, view) -> {
+                    fDisplayingPrompt = null;
+                    signal.cancel();
+                    resp.onError(fContext.getString(R.string.cancelled), null);
+                })
+                .show();
+        final FingerprintManagerCompat.CryptoObject cryptoObject = InitCryptoObject(ivForDecoding, resp);
+        if (cryptoObject == null)
+            return; // Error already handled
+
+        GetFingerprintManager().authenticate(cryptoObject, 0, signal, new FingerprintManagerCompat.AuthenticationCallback() {
+            @Override
+            public void onAuthenticationError(int errMsgId, CharSequence errString) {
+                ClosePrompt();
+                if (errMsgId != 5) // cancelled
+                    resp.onError(errString.toString(), null);
+            }
+
+            @Override
+            public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
+                log.severe("Authentification help " +helpString);
+                super.onAuthenticationHelp(helpMsgId, helpString);
+            }
+
+            @Override
+            public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
+                ClosePrompt();
+                resp.onResponse(cryptoObject);
+            }
+
+            @Override
+            public void onAuthenticationFailed() {
+                ((FingerprintView)(fDisplayingPrompt.getView())).onAuthFailed();
+            }
+        }, null);
+    }
+
+    private void ClosePrompt() {
+        if (fDisplayingPrompt != null) {
+            fDisplayingPrompt.close();
+            fDisplayingPrompt = null;
+        }
+    }
+
+    private void ReadAllPasswords(final FileInterfaceFactory.OnPasswordEnteredListener onResp) {
+        try {
+            String raw = FileUtils.ReadAllFile(fFile);
+            fEncrypted = new IvAndPayload(raw);
+        } catch (IOException e) {
+            // Fallback password
+            SuperGetPassword(true, onResp);
+            return;
+        }
+
+        DisplayPrompt(fEncrypted, new OnResponseListener<FingerprintManagerCompat.CryptoObject>() {
+            @Override
+            public void onResponse(FingerprintManagerCompat.CryptoObject cryptoObject) {
+                try {
+                    Cipher cipher = cryptoObject.getCipher();
+                    if (cipher == null) {
+                        log.severe("Cannot read content: cipher is null");
+                        Toast.makeText(fContext, R.string.fingerprint_init_error, Toast.LENGTH_LONG).show();
+                        SuperGetPassword(false, onResp);
+                        return;
+                    }
+                    byte[] decoded = cipher.doFinal(fEncrypted.fPayload);
+                    String[] passwords = (new String(decoded, "UTF-8")).split("\n");
+                    fPassword = new ArrayDeque<>();
+                    fPasswordIterator = new ArrayDeque<>();
+                    // Try all password in keystore
+                    for (String i: passwords)
+                        if (i.length() > 0)
+                            fPassword.add(i);
+                    fPasswordIterator.addAll(fPassword);
+                    if (fPasswordIterator.isEmpty())
+                        SuperGetPassword(true, onResp);
+                    else
+                        onResp.onResponse(fPasswordIterator.remove());
+                }
+                catch (BadPaddingException | IllegalBlockSizeException | UnsupportedEncodingException e) {
+                    onError(e.getMessage(), e);
+                }
+            }
+
+            @Override
+            public void onError(String msg, Throwable e) {
+                // FIXME Toast error
+                // Fallback password
+                fPassword = new ArrayDeque<>();
+                fPasswordIterator = new ArrayDeque<>();
+                SuperGetPassword(false, onResp);
+            }
+        });
+    }
+
+    private void SuperGetPassword(boolean savePassword, FileInterfaceFactory.OnPasswordEnteredListener onResp) {
+        ClosePrompt();
+        FingerprintPicker.super.GetPassword(savePassword ? new FileInterfaceFactory.OnPasswordEnteredListener() {
+            @Override
+            public boolean onResponse(String result) {
+                // TODO checkbox save password ?
+                if (result != null) {
+                    AddPassword(result);
+                }
+                return onResp.onResponse(result);
+            }
+
+            @Override
+            public void onError(String msg, Throwable e) {
+                onResp.onError(msg, e);
+            }
+        } : onResp);
+    }
+
+    @Override
+    public void GetPassword(FileInterfaceFactory.OnPasswordEnteredListener onResp) {
+        if (fPassword == null)
+            ReadAllPasswords(onResp);
+        else if (fPasswordIterator.isEmpty())
+            SuperGetPassword(true, onResp);
+        else
+            onResp.onResponse(fPasswordIterator.remove());
+    }
+
+    @Override
+    public void WrongPassword() {
+
+    }
+
+    private void AddPassword(String newPass) {
+        fEncrypted = null;
+        DisplayPrompt(null, new OnResponseListener<FingerprintManagerCompat.CryptoObject>() {
+            @Override
+            public void onResponse(FingerprintManagerCompat.CryptoObject cryptoObject) {
+                StringBuilder prev = new StringBuilder();
+                if (fPassword != null)
+                    for (String i: fPassword)
+                        prev.append(i).append('\n');
+                prev.append(newPass);
+                DoWriteFile(cryptoObject, prev.toString());
+            }
+
+            @Override
+            public void onError(String msg, Throwable e) {
+            }
+        });
+    }
+}

+ 32 - 39
app/src/main/java/info/knacki/pass/ui/passwordPicker/PasswordPicker.java

@@ -1,25 +1,20 @@
 package info.knacki.pass.ui.passwordPicker;
 
 import android.content.Context;
-import android.content.DialogInterface;
-import android.view.View;
 import android.widget.Toast;
 
 import info.knacki.pass.R;
 import info.knacki.pass.io.FileInterfaceFactory;
-import info.knacki.pass.io.OnResponseListener;
-import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
 import info.knacki.pass.ui.alertPrompt.views.PasswordTextEdit;
 
-public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
+class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
     protected final Context fContext;
+    protected final AlertPromptGenerator fAlertFactory;
 
-    public PasswordPicker(Context context) {
+    public PasswordPicker(Context context, AlertPromptGenerator alertFactory) {
         fContext = context;
-    }
-
-    protected AlertPrompt AlertFactory() {
-        return new AlertPrompt(fContext);
+        fAlertFactory = alertFactory;
     }
 
     public int GetTitle() {
@@ -27,27 +22,22 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
     }
 
     @Override
-    public void GetPassword(final OnResponseListener<String> onPassword) {
-        AlertFactory()
+    public void GetPassword(final FileInterfaceFactory.OnPasswordEnteredListener onPassword) {
+        fAlertFactory.Generate(fContext)
             .setCancelable(true)
                 .setView(new PasswordTextEdit(fContext))
-            .setPositiveButton(R.string.ok, new AlertPrompt.OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialogInterface, View view) {
-                    onPassword.onResponse(((PasswordTextEdit) view).getStr());
-                }
+            .setPositiveButton(R.string.ok, (dialogInterface, view) -> {
+                if (!onPassword.onResponse(((PasswordTextEdit) view).getStr()))
+                    WrongPassword();
             })
-            .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialogInterface, View s) {
-                    onPassword.onResponse(null);
-                }
+            .setNegativeButton(R.string.cancel, (dialogInterface, s) -> {
+                if (!onPassword.onResponse(null))
+                    WrongPassword();
             })
-                .setTitle(GetTitle())
+            .setTitle(GetTitle())
             .show();
     }
 
-    @Override
     public void WrongPassword() {
         Toast.makeText(fContext, fContext.getResources().getString(R.string.wrongPassword), Toast.LENGTH_LONG).show();
     }
@@ -55,10 +45,10 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
     public static class ChangePasswordPicker extends PasswordPicker implements FileInterfaceFactory.ChangePasswordGetter {
         protected FileInterfaceFactory.ChangePasswordGetter.eStep fCurrentStep = FileInterfaceFactory.ChangePasswordGetter.eStep.eOldPassword;
         private String fFirstPassword = "";
-        private OnResponseListener<String> fListener;
+        private FileInterfaceFactory.OnPasswordEnteredListener fListener;
 
-        public ChangePasswordPicker(Context context) {
-            super(context);
+        public ChangePasswordPicker(Context context, AlertPromptGenerator alertFactory) {
+            super(context, alertFactory);
         }
 
         @Override
@@ -74,15 +64,16 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
             return R.string.enter_password;
         }
 
-        private OnResponseListener<String> fOnFirstPassEntered = new OnResponseListener<String>() {
+        private final FileInterfaceFactory.OnPasswordEnteredListener fOnFirstPassEntered = new FileInterfaceFactory.OnPasswordEnteredListener() {
             @Override
-            public void onResponse(String result) {
-                if (result != null) {
-                    fFirstPassword = result;
-                    SetStep(eStep.eRetypePassword).GetPassword(fListener);
-                } else {
+            public boolean onResponse(String result) {
+                if (result == null) {
                     fListener.onResponse(null);
+                    return false;
                 }
+                fFirstPassword = result;
+                SetStep(eStep.eRetypePassword).GetPassword(fListener);
+                return true;
             }
 
             @Override
@@ -91,17 +82,19 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
             }
         };
 
-        private OnResponseListener<String> fOnRetypePassEntered = new OnResponseListener<String>() {
+        private final FileInterfaceFactory.OnPasswordEnteredListener fOnRetypePassEntered = new FileInterfaceFactory.OnPasswordEnteredListener() {
             @Override
-            public void onResponse(String result) {
+            public boolean onResponse(String result) {
                 if (result == null) {
                     SetStep(eStep.eNewPassword).GetPassword(fListener);
-                } else if (fFirstPassword.equals(result)) {
-                    fListener.onResponse(result);
-                } else {
+                    return false;
+                } else if (!fFirstPassword.equals(result)) {
                     Toast.makeText(fContext, fContext.getResources().getString(R.string.password_missmatch), Toast.LENGTH_LONG).show();
                     SetStep(eStep.eNewPassword).GetPassword(fListener);
+                    return false;
                 }
+                fListener.onResponse(result);
+                return true;
             }
 
             @Override
@@ -111,7 +104,7 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
         };
 
         @Override
-        public void GetPassword(final OnResponseListener<String> onPassword) {
+        public void GetPassword(final FileInterfaceFactory.OnPasswordEnteredListener onPassword) {
             fListener = onPassword;
             switch (fCurrentStep) {
                 case eOldPassword:

+ 33 - 0
app/src/main/java/info/knacki/pass/ui/passwordPicker/PasswordPickerFactory.java

@@ -0,0 +1,33 @@
+package info.knacki.pass.ui.passwordPicker;
+
+import android.app.Service;
+import android.content.Context;
+import android.os.Build;
+import android.view.View;
+
+import info.knacki.pass.io.FileInterfaceFactory;
+import info.knacki.pass.settings.SettingsManager;
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
+import info.knacki.pass.ui.alertPrompt.ServiceAlertPromptGenerator;
+
+public class PasswordPickerFactory {
+    public static FileInterfaceFactory.PasswordGetter GetPasswordPicker(Context ctx) {
+        final AlertPromptGenerator generator = new AlertPromptGenerator();
+
+        if (SettingsManager.IsFingerprintEnabled(ctx) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+            return new FingerprintPicker(ctx, generator);
+        return new PasswordPicker(ctx, generator);
+    }
+
+    public static FileInterfaceFactory.PasswordGetter GetPasswordPicker(Service ctx, View v) {
+        final AlertPromptGenerator generator = new ServiceAlertPromptGenerator(v);
+
+        if (SettingsManager.IsFingerprintEnabled(ctx) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+            return new FingerprintPicker(ctx, generator);
+        return new PasswordPicker(ctx, generator);
+    }
+
+    public static FileInterfaceFactory.ChangePasswordGetter GetPasswordEditor(Context ctx) {
+        return new PasswordPicker.ChangePasswordPicker(ctx, new AlertPromptGenerator());
+    }
+}

+ 0 - 21
app/src/main/java/info/knacki/pass/ui/passwordPicker/ServicePasswordPicker.java

@@ -1,21 +0,0 @@
-package info.knacki.pass.ui.passwordPicker;
-
-import android.content.Context;
-import android.view.View;
-
-import info.knacki.pass.ui.alertPrompt.AlertPrompt;
-import info.knacki.pass.ui.alertPrompt.ServiceAlertPrompt;
-
-public class ServicePasswordPicker extends PasswordPicker {
-    protected final View fInputView;
-
-    public ServicePasswordPicker(Context context, View inputView) {
-        super(context);
-        fInputView = inputView;
-    }
-
-    @Override
-    protected AlertPrompt AlertFactory() {
-        return new ServiceAlertPrompt(fContext, fInputView);
-    }
-}

File diff suppressed because it is too large
+ 6 - 0
app/src/main/res/drawable/ic_fingerprint.xml


File diff suppressed because it is too large
+ 6 - 0
app/src/main/res/drawable/ic_fingerprint_fail.xml


+ 16 - 0
app/src/main/res/layout/fingerprint_picker.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal">
+    <android.support.v7.widget.AppCompatImageView
+        android:layout_width="@dimen/big_icon_height"
+        android:layout_height="@dimen/big_icon_height"
+        android:background="@drawable/ic_fingerprint"
+        android:id="@+id/fingerprint_icon"/>
+    <android.support.v7.widget.AppCompatTextView
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/big_icon_height"
+        android:gravity="center_vertical"
+        android:text="@string/pushfinger_desc"/>
+</LinearLayout>

+ 1 - 1
app/src/main/res/menu/context_dir_menu.xml

@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@+id/remove" android:title="@string/remove"></item>
+    <item android:id="@+id/remove" android:title="@string/remove"/>
 </menu>

+ 2 - 2
app/src/main/res/menu/context_file_menu.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
-<item android:id="@+id/remove" android:title="@string/remove"></item>
-<item android:id="@+id/clipboard" android:title="@string/copy_clipboard"></item>
+<item android:id="@+id/remove" android:title="@string/remove"/>
+<item android:id="@+id/clipboard" android:title="@string/copy_clipboard"/>
 </menu>

+ 4 - 1
app/src/main/res/values-fr/lang.xml

@@ -16,6 +16,7 @@
     <string name="edit_ShowPassword">Montrer le mot de passe</string>
     <string name="edit_Save">Sauvegarder</string>
     <string name="cancel">Annuler</string>
+    <string name="cancelled">Annulé</string>
     <string name="ChangeKeyboard">Revenir au clavier</string>
     <string name="settings">Configuration</string>
     <string name="pref_title_system_keyboard_settings">Paramètres système du clavier</string>
@@ -35,7 +36,6 @@
     <string name="pref_header_GPG">Configuration de GPG</string>
     <array name="pref_enctype_title">
         <item>Pas de chiffrement</item>
-        <item>Empreinte digitale</item>
         <item>GPG</item>
         <item>Mot de passe</item>
     </array>
@@ -80,4 +80,7 @@
     <string name="pref_header_gpg_export">Export GPG key</string>
     <string name="sync">Synchroniser</string>
     <string name="close">Fermer</string>
+    <string name="pushfinger">Lecture d\'empreinte</string>
+    <string name="pushfinger_desc">Touchez le capteur avec le bout de votre doigt</string>
+    <string name="fingerprint_init_error">Erreur lors de l\'initialisation du lecteur d\'empreintes.</string>
 </resources>

+ 1 - 0
app/src/main/res/values/dimens.xml

@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <dimen name="keyboard_key_height">32dp</dimen>
+    <dimen name="big_icon_height">64dp</dimen>
 </resources>

+ 4 - 1
app/src/main/res/values/lang.xml

@@ -16,6 +16,7 @@
     <string name="edit_ShowPassword">Show password</string>
     <string name="edit_Save">Save</string>
     <string name="cancel">Cancel</string>
+    <string name="cancelled">Cancelled</string>
     <string name="ChangeKeyboard">Change keyboard</string>
     <string name="settings">Settings</string>
     <string name="pref_title_system_keyboard_settings">Keyboard system settings</string>
@@ -25,7 +26,6 @@
     <string name="pref_title_enctype">Encryption type</string>
     <array name="pref_enctype_title">
         <item>No encryption</item>
-        <item>Fingerprint</item>
         <item>GPG</item>
         <item>Password</item>
     </array>
@@ -80,4 +80,7 @@
     <string name="pref_header_gpg_export">Export GPG key</string>
     <string name="sync">Synchronize</string>
     <string name="close">Close</string>
+    <string name="pushfinger">Fingerprint reader</string>
+    <string name="pushfinger_desc">Touch the fingerprint reader with the tip or your finger</string>
+    <string name="fingerprint_init_error">Error during fingerprint initialization</string>
 </resources>

+ 1 - 1
app/src/main/res/values/strings.xml

@@ -4,13 +4,13 @@
         <item>0</item>
         <item>1</item>
         <item>2</item>
-        <item>3</item>
     </string-array>
     <string-array name="pref_vcs_list_values">
         <item>0</item>
     </string-array>
     <string name="id_softSettings">id_softSettings</string>
     <string name="id_enctype">id_enctype</string>
+    <string name="pref_enctype_fingerprint">pref_enctype_fingerprint</string>
     <string name="id_vcs_enable">id_vcs_enable</string>
     <string name="id_vcs_list">id_vcs_list</string>
     <string name="id_vcs_git_url">id_vcs_git_url</string>

+ 4 - 0
app/src/main/res/xml/pref_encryption.xml

@@ -15,4 +15,8 @@
         android:fragment="info.knacki.pass.settings.ui.SettingsActivity$GPGPreferenceFragment"
         android:icon="@drawable/ic_info_black_24dp"
         android:title="@string/pref_header_GPG" />
+    <SwitchPreference
+        android:key="@string/pref_enctype_fingerprint"
+        android:title="@string/pushfinger"
+        android:icon="@drawable/ic_fingerprint" />
 </PreferenceScreen>

+ 1 - 3
app/src/main/res/xml/provider_paths.xml

@@ -1,6 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <paths>
-    <cache-path
-        name="cache"
-        path="."></cache-path>
+    <cache-path name="cache" path="."/>
 </paths>

+ 1 - 1
build.gradle

@@ -7,7 +7,7 @@ buildscript {
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.2.0'
+        classpath 'com.android.tools.build:gradle:3.2.1'
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }

BIN
gradle/wrapper/gradle-wrapper.jar


+ 1 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,5 @@
-#Mon Apr 02 19:42:02 CEST 2018
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip

Some files were not shown because too many files changed in this diff