Browse Source

File import/export

isundil 4 years ago
parent
commit
20b649975a

+ 5 - 5
app/build.gradle

@@ -5,17 +5,17 @@ android {
         release {
             keyAlias 'key0'
             keyPassword 'android'
-            storeFile file('Z:/keys/androidKey.gpg')
+            storeFile file('Z:/isundil/keys/androidKey.gpg')
             storePassword 'android'
         }
     }
-    compileSdkVersion 28
+    compileSdkVersion 29
     defaultConfig {
         applicationId "info.knacki.pass"
         minSdkVersion 15
-        targetSdkVersion 28
-        versionCode 1
-        versionName "1.0.1"
+        targetSdkVersion 29
+        versionCode 2
+        versionName "1.0.2"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
         vectorDrawables.useSupportLibrary = true
         signingConfig signingConfigs.release

+ 1 - 0
app/src/main/java/info/knacki/pass/io/FileInterfaceFactory.java

@@ -31,6 +31,7 @@ public class FileInterfaceFactory {
 
     public interface PasswordGetter {
         PasswordGetter GetPassword(OnPasswordEnteredListener onPassword);
+        PasswordGetter SetTitle(String title);
         PasswordGetter SetFile(File file);
     }
 

+ 180 - 30
app/src/main/java/info/knacki/pass/io/FileMigratoryUtils.java

@@ -5,13 +5,19 @@ import android.content.Context;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayDeque;
+import java.util.logging.Logger;
 import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
 import java.util.zip.ZipOutputStream;
 
 import info.knacki.gitdroid.callback.OnResponseListener;
+import info.knacki.pass.R;
+import info.knacki.pass.ui.alertPrompt.views.ConflictFileView;
 
 public class FileMigratoryUtils {
+    public final static Logger log = Logger.getLogger(FileMigratoryUtils.class.getName());
     public interface MigratePasswordCondition {
         boolean isRelevantForMigration(File f);
     }
@@ -19,7 +25,7 @@ public class FileMigratoryUtils {
     private static void ListPasswords(File dir, MigratePasswordCondition condition, ArrayDeque<File> arr) {
         if (!dir.exists() || !dir.isDirectory())
             return;
-        for (File f: dir.listFiles()) {
+        for (File f : dir.listFiles()) {
             if (f.isHidden())
                 continue;
             if (f.isDirectory())
@@ -41,8 +47,7 @@ public class FileMigratoryUtils {
             public void OnResponse(String result) {
                 try {
                     to.WriteFile(result, onDone);
-                }
-                catch (IFileInterface.InvalidPasswordException e) {
+                } catch (IFileInterface.InvalidPasswordException e) {
                     OnError(e.getMessage(), e);
                 }
             }
@@ -77,8 +82,57 @@ public class FileMigratoryUtils {
         IFileInterface GetOutput(File input);
     }
 
-    private static void MigratePasswords(final Context ctx, final FileInterfaceFactory.PasswordGetter passwordGetter, final OutputGetter outputGetter, ArrayDeque<File> inputFiles, OnResponseListener<Void> onDone) {
-        final ArrayDeque<String> oldPasswords = new ArrayDeque<>();
+    private static class PasswordGetterWithCache implements FileInterfaceFactory.PasswordGetter {
+        public final ArrayDeque<String> fCache = new ArrayDeque<>();
+        public final ArrayDeque<String> fAllCache = new ArrayDeque<>();
+        public final FileInterfaceFactory.PasswordGetter fOther;
+
+        public PasswordGetterWithCache(FileInterfaceFactory.PasswordGetter other) {
+            fOther = other;
+        }
+
+        public PasswordGetterWithCache reset() {
+            fCache.clear();
+            fCache.addAll(fAllCache);
+            return this;
+        }
+
+        @Override
+        public FileInterfaceFactory.PasswordGetter GetPassword(FileInterfaceFactory.OnPasswordEnteredListener onPassword) {
+            if (fCache.isEmpty()) {
+                fOther.GetPassword(new FileInterfaceFactory.OnPasswordEnteredListener() {
+                    @Override
+                    public boolean OnResponse(String password) {
+                        fAllCache.push(password);
+                        return onPassword.OnResponse(password);
+                    }
+
+                    @Override
+                    public void OnError(String msg, Throwable e) {
+                        onPassword.OnError(msg, e);
+                    }
+                });
+            } else {
+                onPassword.OnResponse(fCache.pop());
+            }
+            return this;
+        }
+
+        @Override
+        public FileInterfaceFactory.PasswordGetter SetFile(File file) {
+            fOther.SetFile(file);
+            return this;
+        }
+
+        @Override
+        public FileInterfaceFactory.PasswordGetter SetTitle(String title) {
+            fOther.SetTitle(title);
+            return this;
+        }
+    }
+
+    private static void MigratePasswords(final Context ctx, final FileInterfaceFactory.PasswordGetter _passwordGetter, final OutputGetter outputGetter, ArrayDeque<File> inputFiles, OnResponseListener<Void> onDone) {
+        PasswordGetterWithCache passwordGetter = new PasswordGetterWithCache(_passwordGetter);
 
         final Runnable nextFile = new Runnable() {
             private void NextFile() {
@@ -88,30 +142,8 @@ public class FileMigratoryUtils {
                     return;
                 }
                 final File currentFile = inputFiles.poll();
-                final ArrayDeque<String> currentPasswordToTry = new ArrayDeque<>(oldPasswords);
-                MigratePassword(ctx, currentFile, outputGetter.GetOutput(currentFile), new FileInterfaceFactory.PasswordGetter() {
-                    @Override
-                    public FileInterfaceFactory.PasswordGetter SetFile(File file) { return this; }
-
-                    @Override
-                    public FileInterfaceFactory.PasswordGetter GetPassword(FileInterfaceFactory.OnPasswordEnteredListener listener) {
-                        if (!currentPasswordToTry.isEmpty())
-                            listener.OnResponse(currentPasswordToTry.poll());
-                        else
-                            passwordGetter.SetFile(currentFile).GetPassword(new FileInterfaceFactory.OnPasswordEnteredListener() {
-                                @Override
-                                public boolean OnResponse(String pass) {
-                                    oldPasswords.add(pass);
-                                    return listener.OnResponse(pass);
-                                }
-
-                                @Override
-                                public void OnError(String what, Throwable e) {
-                                }
-                            });
-                        return this;
-                    }
-                }, new OnResponseListener<Void>() {
+                passwordGetter.reset().SetFile(currentFile);
+                MigratePassword(ctx, currentFile, outputGetter.GetOutput(currentFile), passwordGetter, new OnResponseListener<Void>() {
                     @Override
                     public void OnResponse(Void result) {
                         NextFile();
@@ -138,11 +170,129 @@ public class FileMigratoryUtils {
         return absPath.substring(rootDir.length());
     }
 
+    private static class ImportResult {
+        public final ConflictFileView.eDefaultBehaviour fOnFileExists;
+        public final boolean fWritten;
+
+        public ImportResult(ConflictFileView.eDefaultBehaviour onFileExists, boolean written) {
+            fOnFileExists = onFileExists;
+            fWritten = written;
+        }
+    }
+
+    private static void ImportFromStream(final Context ctx, byte[] in, final String fileName, PasswordGetterWithCache passwordGetter, OnResponseListener<ImportResult> onDone, ConflictFileView.eDefaultBehaviour onFileExists) {
+        class DoWrite {
+            public void DoWrite(File output, String result, ConflictFileView.eDefaultBehaviour finalOnFileExists) {
+                if (output.exists())
+                    output.delete();
+                try {
+                    FileInterfaceFactory.GetFileInterface(ctx, null, output).WriteFile(result, new OnResponseListener<Void>() {
+                        @Override
+                        public void OnResponse(Void result) {
+                            onDone.OnResponse(new ImportResult(finalOnFileExists, true));
+                        }
+
+                        @Override
+                        public void OnError(String msg, Throwable e) {
+                            onDone.OnError(msg, e);
+                        }
+                    });
+                }
+                catch (IFileInterface.InvalidPasswordException e) {
+                    onDone.OnError("Cannot write file " +fileName, e);
+                }
+            }
+        };
+        final DoWrite _do = new DoWrite();
+        (new PasswordFileInterface(ctx, passwordGetter, in)).ReadFile(new OnResponseListener<String>() {
+            @Override
+            public void OnResponse(String result) {
+                File output = new File(PathUtils.GetPassDir(ctx), fileName);
+                if (!output.exists() || ConflictFileView.eDefaultBehaviour.eOverride.equals(onFileExists)) {
+                    _do.DoWrite(output, result, onFileExists);
+                } else if (ConflictFileView.eDefaultBehaviour.eUnknown.equals(onFileExists)) {
+                    ConflictFileView.Show(ctx, fileName, new OnResponseListener<ConflictFileView.Result>() {
+                        @Override
+                        public void OnResponse(ConflictFileView.Result conflictResult) {
+                            if (ConflictFileView.eDefaultBehaviour.eOverride.equals(conflictResult.fResponse))
+                                _do.DoWrite(output, result, conflictResult.fSave ? conflictResult.fResponse : ConflictFileView.eDefaultBehaviour.eUnknown);
+                            else
+                                onDone.OnResponse(new ImportResult(conflictResult.fSave ? conflictResult.fResponse : ConflictFileView.eDefaultBehaviour.eUnknown, false));
+                        }
+
+                        @Override
+                        public void OnError(String msg, Throwable e) {
+
+                        }
+                    });
+                } else {
+                    onDone.OnResponse(new ImportResult(onFileExists, false));
+                }
+            }
+
+            @Override
+            public void OnError(String msg, Throwable e) {
+                onDone.OnError(msg, e);
+            }
+        });
+    }
+
+    public static void ImportAllPasswords(final Context ctx, InputStream in, FileInterfaceFactory.PasswordGetter _passwordGetter, OnResponseListener<Integer> onDone) {
+        final ZipInputStream stream = new ZipInputStream(in);
+        final PasswordGetterWithCache passwordGetter = new PasswordGetterWithCache(_passwordGetter);
+
+        final Runnable nextFile = new Runnable() {
+            private int importedFileCount = 0;
+            public void Next(ConflictFileView.eDefaultBehaviour onFileExists) {
+                ZipEntry currentEntry = null;
+                try {
+                    currentEntry = stream.getNextEntry();
+                    if (currentEntry != null) {
+                        passwordGetter.reset();
+                        byte[] bb = FileUtils.ReadAllStream(stream);
+                        ImportFromStream(ctx, bb, currentEntry.getName(), passwordGetter, new OnResponseListener<ImportResult>() {
+                            @Override
+                            public void OnResponse(ImportResult result) {
+                                if (result.fWritten)
+                                    importedFileCount++;
+                                Next(result.fOnFileExists);
+                            }
+
+                            @Override
+                            public void OnError(String msg, Throwable e) {
+                                onDone.OnError(msg, e);
+                            }
+                        }, onFileExists);
+                    } else {
+                        onDone.OnResponse(importedFileCount);
+                    }
+                }
+                catch (IOException e) {
+                    if (currentEntry != null)
+                        onDone.OnError("Cannot read from file " +currentEntry.getName(), e);
+                    else
+                        onDone.OnError("Cannot read from ZIP file", e);
+                }
+            }
+
+            @Override
+            public void run() {
+                Next(ConflictFileView.eDefaultBehaviour.eUnknown);
+            }
+        };
+
+        nextFile.run();
+    }
+
     public static void ExportAllPasswords(final Context ctx, FileInterfaceFactory.PasswordGetter passwordGetter, OnResponseListener<byte[]> onDone) {
         // Get output password
-        passwordGetter.GetPassword(new FileInterfaceFactory.OnPasswordEnteredListener() {
+        passwordGetter.SetTitle(ctx.getResources().getString(R.string.enter_output_password)).GetPassword(new FileInterfaceFactory.OnPasswordEnteredListener() {
             @Override
             public boolean OnResponse(String outputPassword) {
+                if (outputPassword == null) {
+                    onDone.OnResponse(null);
+                    return true;
+                }
                 ByteArrayOutputStream out = new ByteArrayOutputStream();
                 ZipOutputStream compressedStream = new ZipOutputStream(out);
 

+ 52 - 8
app/src/main/java/info/knacki/pass/io/PasswordFileInterface.java

@@ -23,6 +23,7 @@ import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerat
 import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
 import org.bouncycastle.util.io.Streams;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -40,23 +41,55 @@ import info.knacki.pass.settings.SettingsManager;
 
 class PasswordFileInterface implements IFileInterface {
     public static final String PASSWORD_SUFFIX = ".pwd";
-    protected final File fFile;
+    protected final FileOrContent fFile;
     protected final FileInterfaceFactory.PasswordGetter fPasswordGetter;
     private final char[] fPassword;
     private final String fMethodName;
 
+    private static class FileOrContent {
+        public final File fFile;
+        public final byte[] fContent;
+
+        public FileOrContent(File f) {
+            fFile = f;
+            fContent = null;
+        }
+        public FileOrContent(byte[] content) {
+            fFile = null;
+            fContent = content;
+        }
+        public InputStream ToInputStream() throws IOException {
+            if (fFile != null)
+                return new FileInputStream(fFile);
+            return new ByteArrayInputStream(fContent);
+        }
+        public boolean empty() {
+            return (fFile != null && fFile.length() == 0) ||
+                    (fContent != null && fContent.length == 0) ||
+                    (fFile == null && fContent == null);
+        }
+    }
+
     PasswordFileInterface(Context ctx, FileInterfaceFactory.PasswordGetter passwordGetter, File f) {
-        fFile = f;
+        fFile = new FileOrContent(f);
         fPasswordGetter = passwordGetter;
         fPassword = SettingsManager.GetPassword(ctx).toCharArray();
         Resources r = ctx.getResources();
         fMethodName = r.getString(R.string.pref_enc_type_title_password);
     }
 
-    private boolean TryPassword(String pass, OnResponseListener<InputStream> onResponse) {
+    public PasswordFileInterface(Context ctx, FileInterfaceFactory.PasswordGetter passwordGetter, byte[] encryptedContent) {
+        fPasswordGetter = passwordGetter;
+        fPassword = SettingsManager.GetPassword(ctx).toCharArray();
+        Resources r = ctx.getResources();
+        fMethodName = r.getString(R.string.pref_enc_type_title_password);
+        fFile = new FileOrContent(encryptedContent);
+    }
+
+    private static boolean TryDecryptStream(String pass, InputStream in, OnResponseListener<InputStream> onResponse) {
         try {
-            InputStream in = PGPUtil.getDecoderStream(new FileInputStream(fFile));
-            JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
+            InputStream pgpIn = PGPUtil.getDecoderStream(in);
+            JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(pgpIn);
             Object o = pgpF.nextObject();
             PGPEncryptedDataList enc = (o instanceof PGPEncryptedDataList) ? (PGPEncryptedDataList) o : (PGPEncryptedDataList) pgpF.nextObject();
             InputStream clear = ((PGPPBEEncryptedData) enc.get(0)).getDataStream(new JcePBEDataDecryptorFactoryBuilder(new JcaPGPDigestCalculatorProviderBuilder().setProvider("BC").build()).setProvider("BC").build(pass.toCharArray()));
@@ -74,6 +107,18 @@ class PasswordFileInterface implements IFileInterface {
         }
     }
 
+    private boolean TryPassword(String pass, OnResponseListener<InputStream> onResponse) {
+        InputStream in;
+        try {
+            in = fFile.ToInputStream();
+        }
+        catch (IOException e) {
+            onResponse.OnError(e.getMessage(), e);
+            return true;
+        }
+        return TryDecryptStream(pass, in, onResponse);
+    }
+
     private void FindPassword(final OnResponseListener<InputStream> onResponse) {
         FileInterfaceFactory.OnPasswordEnteredListener onResp = new FileInterfaceFactory.OnPasswordEnteredListener() {
             @Override
@@ -97,7 +142,6 @@ class PasswordFileInterface implements IFileInterface {
     }
 
     private void DecryptFile(final OnResponseListener<byte[]> resp) {
-
         FindPassword(new OnResponseListener<InputStream>() {
             @Override
             public void OnResponse(InputStream clear) {
@@ -155,7 +199,7 @@ class PasswordFileInterface implements IFileInterface {
         Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
         Security.addProvider(new BouncyCastleProvider());
 
-        if (fFile.length() == 0) {
+        if (fFile.empty()) {
             resp.OnResponse("");
             return;
         }
@@ -186,7 +230,7 @@ class PasswordFileInterface implements IFileInterface {
         Security.addProvider(new BouncyCastleProvider());
 
         try {
-            CryptData(new FileOutputStream(fFile), CharsetHelper.StringToByteArray(content), fPassword);
+            CryptData(new FileOutputStream(fFile.fFile), CharsetHelper.StringToByteArray(content), fPassword);
         }
         catch (Throwable e) {
             resp.OnError(e.getMessage(), e);

+ 33 - 0
app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java

@@ -272,6 +272,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 
     public static class VCSPreferenceFragment extends PreferenceFragment {
         private static final int ACTIVITY_REQUEST_CODE_BROWSE_PRIVATE_KEY = 2;
+        private static final int ACTIVITY_REQUEST_CODE_BROWSE_IMPORT_FILE = 3;
 
         @Override
         public void onCreate(Bundle savedInstanceState) {
@@ -393,6 +394,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 FileMigratoryUtils.ExportAllPasswords(getActivity().getApplicationContext(), PasswordPickerFactory.GetPasswordPicker(getActivity()), new OnResponseListener<byte[]>() {
                     @Override
                     public void OnResponse(final byte[] result) {
+                        if (result == null) {
+                            // cancelled
+                            return;
+                        }
                         final File outFile = new File(getActivity().getCacheDir().getAbsolutePath() + "/passwords.zip");
                         try {
                             FileUtils.Touch(outFile);
@@ -424,6 +429,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 return true;
             });
             findPreference(getResources().getString(R.string.id_vcs_import)).setOnPreferenceClickListener(preference -> {
+                Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+                i.setType("application/zip");
+                i.addCategory(Intent.CATEGORY_OPENABLE);
+                VCSPreferenceFragment.this.startActivityForResult(i, ACTIVITY_REQUEST_CODE_BROWSE_IMPORT_FILE);
                 return true;
             });
         }
@@ -636,6 +645,30 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                     Toast.makeText(getActivity(), getResources().getString(R.string.file_not_found), Toast.LENGTH_LONG).show();
                     GPGStorageEngine.GetDefaultEngine(getActivity()).RemoveGpgKey();
                 }
+            } else if (requestCode == ACTIVITY_REQUEST_CODE_BROWSE_IMPORT_FILE && resultCode == RESULT_OK) {
+                final Uri intentData = data.getData();
+                InputStream in = null;
+                try {
+                    in = intentData == null ? null : getActivity().getContentResolver().openInputStream(intentData);
+                }
+                catch (FileNotFoundException e) {
+                    log.log(Level.SEVERE, "Cannot get Intent content", e);
+                }
+                if (in == null) {
+                    Toast.makeText(getActivity(), getResources().getString(R.string.file_not_found), Toast.LENGTH_LONG).show();
+                    return;
+                }
+                FileMigratoryUtils.ImportAllPasswords(getActivity(), in, PasswordPickerFactory.GetPasswordPicker(getActivity()), new OnResponseListener<Integer>() {
+                    @Override
+                    public void OnResponse(Integer result) {
+                        Toast.makeText(getActivity(), String.format(getResources().getString(R.string.pass_import_ok), result.intValue()), Toast.LENGTH_LONG).show();
+                    }
+
+                    @Override
+                    public void OnError(String msg, Throwable e) {
+                        Toast.makeText(getActivity(), getResources().getString(R.string.pass_import_ko), Toast.LENGTH_LONG).show();
+                    }
+                });
             }
         }
     }

+ 73 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/views/ConflictFileView.java

@@ -0,0 +1,73 @@
+package info.knacki.pass.ui.alertPrompt.views;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.CheckBox;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import info.knacki.gitdroid.callback.OnResponseListener;
+import info.knacki.pass.R;
+import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
+
+public class ConflictFileView extends HorizontalScrollView {
+    public enum eDefaultBehaviour {
+        eKeepPrevious,
+        eOverride,
+        eUnknown
+    }
+
+    public static class Result {
+        public final eDefaultBehaviour fResponse;
+        public final boolean fSave;
+
+        private Result(eDefaultBehaviour response, boolean save) {
+            fResponse = response;
+            fSave = save;
+        }
+    }
+
+    private boolean fChecked = false;
+
+    private ConflictFileView(Context context, String filename) {
+        super(context);
+        LinearLayout layout = new LinearLayout(context);
+        layout.setOrientation(LinearLayout.VERTICAL);
+        layout.setPadding(15, 0, 15, 0);
+        addView(layout);
+
+        TextView tv = new TextView(context);
+        tv.setText(String.format(context.getString(R.string.conflicting_file), filename));
+        layout.addView(tv);
+
+        CheckBox cb = new CheckBox(context);
+        cb.setText(R.string.save_for_all);
+        cb.setChecked(false);
+        cb.setOnCheckedChangeListener((compoundButton, b) -> fChecked = b);
+        layout.addView(cb);
+    }
+
+    private boolean IsSaveChecked() {
+        return fChecked;
+    }
+
+    public static void Show(final Context ctx, String filename, OnResponseListener<Result> onResponse) {
+        final ConflictFileView view = new ConflictFileView(ctx, filename);
+
+        new Handler(Looper.getMainLooper()).post(() -> {
+            AlertPrompt pt = AlertPromptGenerator.StaticMake(ctx)
+                    .setCancelable(true)
+                    .setNegativeButton(R.string.skip, ((dialogInterface, view1) -> {
+                        onResponse.OnResponse(new Result(eDefaultBehaviour.eKeepPrevious, view.IsSaveChecked()));
+                    }))
+                    .setPositiveButton(R.string.overwrite, (dialogInterface, v) -> {
+                        onResponse.OnResponse(new Result(eDefaultBehaviour.eOverride, view.IsSaveChecked()));
+                    })
+                    .setTitle(R.string.conflictingFiles);
+            pt.setView(view).show();
+        });
+    }
+}

+ 26 - 14
app/src/main/java/info/knacki/pass/ui/passwordPicker/PasswordPicker.java

@@ -2,6 +2,7 @@ package info.knacki.pass.ui.passwordPicker;
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.view.View;
 import android.widget.Toast;
 
 import java.io.File;
@@ -15,38 +16,49 @@ import info.knacki.pass.ui.alertPrompt.views.PasswordTextEdit;
 class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
     protected final Context fContext;
     protected final AlertPromptGenerator fAlertFactory;
-    protected String fName = null;
+    protected String fDisplayTitle = null;
 
     public PasswordPicker(Context context, AlertPromptGenerator alertFactory) {
         fContext = context;
         fAlertFactory = alertFactory;
-    }
-
-    public String GetTitle() {
-        return fName == null ? fContext.getResources().getString(R.string.enter_password) : String.format(fContext.getResources().getString(R.string.enter_password_for), fName);
+        fDisplayTitle = fContext.getResources().getString(R.string.enter_password);
     }
 
     @Override
     public FileInterfaceFactory.PasswordGetter SetFile(File file) {
-        if (file == null) {
-            fName = null;
-        } else {
+        String name = null;
+        if (file != null) {
             int rootLen = PathUtils.GetPassDir(fContext).length();
-            fName = file.getAbsolutePath();
-            if (fName.length() > rootLen) {
-                fName = fName.substring(rootLen);
-                if (fName.startsWith("/"))
-                    fName = fName.substring(1);
+            name = file.getAbsolutePath();
+            if (name.length() > rootLen) {
+                name = name.substring(rootLen);
+                if (name.startsWith("/"))
+                    name = name.substring(1);
             }
         }
+        fDisplayTitle = name == null ? fContext.getResources().getString(R.string.enter_password) : String.format(fContext.getResources().getString(R.string.enter_password_for), name);
+        return this;
+    }
+
+    protected String GetTitle() {
+        return fDisplayTitle;
+    }
+
+    @Override
+    public FileInterfaceFactory.PasswordGetter SetTitle(String title) {
+        fDisplayTitle = title;
         return this;
     }
 
+    protected View GetAlertView() {
+        return new PasswordTextEdit(fContext);
+    }
+
     @Override
     public PasswordPicker GetPassword(final FileInterfaceFactory.OnPasswordEnteredListener onPassword) {
         fAlertFactory.Generate(fContext)
             .setCancelable(true)
-            .setView(new PasswordTextEdit(fContext))
+            .setView(GetAlertView())
             .setPositiveButton(R.string.ok, (dialogInterface, view) -> {
                 if (!onPassword.OnResponse(((PasswordTextEdit) view).getStr()))
                     WrongPassword();

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


+ 8 - 0
app/src/main/res/values-fr/lang.xml

@@ -15,6 +15,8 @@
     <string name="generate_difficulty">Force du mot de passe</string>
     <string name="edit_ShowPassword">Montrer le mot de passe</string>
     <string name="edit_Save">Sauvegarder</string>
+    <string name="save_for_all">Appliquer à tous</string>
+    <string name="conflicting_file">%s existe déjà, voulez-vous l\'écraser ?</string>
     <string name="cancel">Annuler</string>
     <string name="cancelled">Annulé</string>
     <string name="ChangeKeyboard">Revenir au clavier</string>
@@ -34,6 +36,7 @@
     <string name="pref_vcs_git_private_key_change">Changer sa clée privée</string>
     <string name="pref_vcs_branch_title">Branche GIT</string>
     <string name="pref_vcs_git_pull">Git pull</string>
+    <string name="pref_vcs_importexport">Import / Export to file</string>
     <string name="pref_vcs_export">Export</string>
     <string name="pref_vcs_import">Import</string>
     <string name="pref_vcs_git_auth_category">Authentication</string>
@@ -90,11 +93,16 @@
     <string name="copied_clipboard">Mot de passe dans le presse-papier</string>
     <string name="gpg_import_ok">Clé GPG importée avec succès</string>
     <string name="gpg_import_ko">Erreur lors de l\'import de la clé GPG</string>
+    <string name="pass_import_ok">%d mots de passes importés avec succès</string>
+    <string name="pass_import_ko">Erreur lors de l\'import des mots de passe</string>
     <string name="file_not_found">Fichier introuvable</string>
     <string name="password_mismatch">Les mots de passes ne correspondent pas. Veuillez rééssayer</string>
     <string name="enter_old_password">Saisissez l\'ancien mot de passe</string>
+    <string name="enter_output_password">Saisissez mot de passe pour l\'export</string>
     <string name="type_new_password">Saisissez le nouveau mot de passe</string>
     <string name="retype_new_password">Saisissez de nouveau le nouveau mot de passe</string>
+    <string name="skip">Passer</string>
+    <string name="overwrite">Écraser</string>
     <string name="yes">Oui</string>
     <string name="no">Non</string>
     <string name="unprotected_key">Cette clé n\'est pas protégée par un mot de passe, Voulez-vous continuer ?</string>

+ 8 - 0
app/src/main/res/values/lang.xml

@@ -15,6 +15,8 @@
     <string name="generate_difficulty">Password strength</string>
     <string name="edit_ShowPassword">Show password</string>
     <string name="edit_Save">Save</string>
+    <string name="save_for_all">Apply to all</string>
+    <string name="conflicting_file">%s already exists. Do you want to overwrite it ?</string>
     <string name="cancel">Cancel</string>
     <string name="cancelled">Cancelled</string>
     <string name="ChangeKeyboard">Change keyboard</string>
@@ -40,6 +42,7 @@
     <string name="pref_vcs_git_private_key_change">Change RSA private key</string>
     <string name="pref_vcs_branch_title">Git branch</string>
     <string name="pref_vcs_git_pull">Git pull</string>
+    <string name="pref_vcs_importexport">Importer / Exporter a partir d\'un fichier</string>
     <string name="pref_vcs_export">Export</string>
     <string name="pref_vcs_import">Import</string>
     <string name="pref_vcs_git_auth_category">Authentication</string>
@@ -90,11 +93,16 @@
     <string name="copied_clipboard">Password copied to Clipboard</string>
     <string name="gpg_import_ok">Done importing GPG Key</string>
     <string name="gpg_import_ko">Error while importing GPG Key</string>
+    <string name="pass_import_ok">Done importing %d passwords File</string>
+    <string name="pass_import_ko">Error while importing Passwords File</string>
     <string name="file_not_found">Error: file not found</string>
     <string name="password_mismatch">Password mismatch. Please try again</string>
     <string name="enter_old_password">Enter old password</string>
+    <string name="enter_output_password">Enter output password</string>
     <string name="type_new_password">Enter new password</string>
     <string name="retype_new_password">Retype new password</string>
+    <string name="skip">Skip</string>
+    <string name="overwrite">Overwrite</string>
     <string name="yes">Yes</string>
     <string name="no">No</string>
     <string name="unprotected_key">This key is not protected with a password, do you want to continue ?</string>

+ 2 - 1
app/src/main/res/xml/pref_vcs.xml

@@ -45,8 +45,9 @@
     <EditTextPreference
         android:key="@string/id_vcs_git_ci_user_email"
         android:title="@string/pref_vcs_git_user_email_title"/>
-
     <Preference android:title="@string/pref_vcs_git_pull" android:key="@string/id_vcs_git_pull" />
+
+    <PreferenceCategory android:title="@string/pref_vcs_importexport"/>
     <Preference android:title="@string/pref_vcs_export" android:key="@string/id_vcs_export" />
     <Preference android:title="@string/pref_vcs_import" android:key="@string/id_vcs_import" />
 </PreferenceScreen>

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

@@ -1,6 +1,6 @@
-#Thu Mar 25 21:20:09 CET 2021
+#Tue Apr 06 10:00:19 CEST 2021
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip