Browse Source

[add][wip] manage conflicts

isundil 7 years ago
parent
commit
6c2f08f97b
34 changed files with 596 additions and 253 deletions
  1. 5 5
      app/build.gradle
  2. 1 1
      app/src/main/AndroidManifest.xml
  3. 0 3
      app/src/main/java/info/knacki/pass/git/GitInterface.java
  4. 7 2
      app/src/main/java/info/knacki/pass/git/GitLocal.java
  5. 4 1
      app/src/main/java/info/knacki/pass/git/GitSha1.java
  6. 0 1
      app/src/main/java/info/knacki/pass/git/HttpGitProtocol.java
  7. 29 23
      app/src/main/java/info/knacki/pass/git/entities/GitCommit.java
  8. 8 6
      app/src/main/java/info/knacki/pass/git/entities/GitObject.java
  9. 2 2
      app/src/main/java/info/knacki/pass/git/entities/GitPackableUtil.java
  10. 2 2
      app/src/main/java/info/knacki/pass/input/InputService.java
  11. 0 2
      app/src/main/java/info/knacki/pass/io/IFileInterface.java
  12. 0 3
      app/src/main/java/info/knacki/pass/io/PathUtils.java
  13. 2 0
      app/src/main/java/info/knacki/pass/settings/ui/AppCompatPreferenceActivity.java
  14. 6 5
      app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java
  15. 0 139
      app/src/main/java/info/knacki/pass/ui/AlertText.java
  16. 5 2
      app/src/main/java/info/knacki/pass/ui/EditPasswordActivity.java
  17. 98 24
      app/src/main/java/info/knacki/pass/ui/GitPullActivity.java
  18. 50 20
      app/src/main/java/info/knacki/pass/ui/MainActivity.java
  19. 7 5
      app/src/main/java/info/knacki/pass/ui/PasswordPicker.java
  20. 67 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/AlertPrompt.java
  21. 124 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/views/ConflictView.java
  22. 29 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/views/SimpleTextEdit.java
  23. 49 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/views/TextEditAndCheckbox.java
  24. 15 0
      app/src/main/java/info/knacki/pass/ui/alertPrompt/views/TextView.java
  25. 54 0
      app/src/main/java/info/knacki/pass/ui/passwordList/EditablePasswordListView.java
  26. 1 1
      app/src/main/java/info/knacki/pass/ui/passwordList/PasswordClickListener.java
  27. 8 0
      app/src/main/java/info/knacki/pass/ui/passwordList/PasswordEditListener.java
  28. 3 3
      app/src/main/java/info/knacki/pass/ui/passwordList/PasswordListView.java
  29. 1 1
      app/src/main/java/info/knacki/pass/ui/passwordList/PasswordView.java
  30. 5 0
      app/src/main/res/menu/context_menu.xml
  31. 7 0
      app/src/main/res/values-fr/lang.xml
  32. 5 0
      app/src/main/res/values/lang.xml
  33. 1 1
      build.gradle
  34. 1 1
      gradle/wrapper/gradle-wrapper.properties

+ 5 - 5
app/build.gradle

@@ -1,11 +1,11 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 27
+    compileSdkVersion 28
     defaultConfig {
         applicationId "info.knacki.pass"
         minSdkVersion 15
-        targetSdkVersion 27
+        targetSdkVersion 28
         versionCode 1
         versionName "1.0"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -21,10 +21,10 @@ android {
 
 dependencies {
     implementation fileTree(include: ['*.jar'], dir: 'libs')
-    implementation 'com.android.support:appcompat-v7:27.1.1'
+    implementation 'com.android.support:appcompat-v7:28.0.0'
     implementation 'com.android.support.constraint:constraint-layout:1.1.3'
-    implementation 'com.android.support:support-v4:27.1.1'
-    implementation 'com.android.support:support-vector-drawable:27.1.1'
+    implementation 'com.android.support:support-v4:28.0.0'
+    implementation 'com.android.support:support-vector-drawable:28.0.0'
     testImplementation 'junit:junit:4.12'
     androidTestImplementation 'com.android.support.test:runner:1.0.2'
     androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

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

@@ -3,7 +3,7 @@
     package="info.knacki.pass">
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
+    <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 

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

@@ -1,8 +1,5 @@
 package info.knacki.pass.git;
 
-import java.io.File;
-import java.util.Map;
-
 import info.knacki.pass.git.entities.GitCommit;
 import info.knacki.pass.git.entities.GitObject;
 import info.knacki.pass.git.entities.GitRef;

+ 7 - 2
app/src/main/java/info/knacki/pass/git/GitLocal.java

@@ -41,8 +41,9 @@ public class GitLocal {
         }
     }
 
-    public String GetHash(String key) {
-        return cache.get(key);
+    public String GetHash(String key, String defaultValue) {
+        String res = cache.get(key);
+        return res == null ? defaultValue : res;
     }
 
     public void SetHash(String filename, String hash) {
@@ -69,4 +70,8 @@ public class GitLocal {
         catch (IOException e)
         {}
     }
+
+    public boolean HasHash(String path) {
+        return cache.containsKey(path);
+    }
 }

+ 4 - 1
app/src/main/java/info/knacki/pass/git/GitSha1.java

@@ -46,7 +46,7 @@ public class GitSha1 {
                 catch (IOException e) {
 
                 }
-                return new byte[]{};
+                return null;
             }
         });
     }
@@ -55,6 +55,9 @@ public class GitSha1 {
         try {
             MessageDigest sha1Builder = MessageDigest.getInstance("SHA1");
             byte[] packContent = obj.GetPack();
+            if (packContent == null) {
+                return new byte[]{};
+            }
             sha1Builder.update((GitPackableUtil.GetObjTypeString(obj.GetPackableType()) +" " +packContent.length).getBytes());
             sha1Builder.update(new byte[] { 0 });
             sha1Builder.update(packContent);

+ 0 - 1
app/src/main/java/info/knacki/pass/git/HttpGitProtocol.java

@@ -480,7 +480,6 @@ class HttpGitProtocol implements GitInterface {
                 if (myref != null) {
                     final GitRef finalRef = myref;
                     response.onMsg("Pushing over " +myref.GetBranch() +" revision " +myref.GetHash());
-                    log.severe("Pack: " +GitSha1.getSha1OfPackable(commit.Build()) +": " +new String(commit.Build().GetPack()));
                     try {
                         HashMap<String, String> headers = new HashMap<>();
                         headers.put("Content-Type", "application/x-git-receive-pack-request");

+ 29 - 23
app/src/main/java/info/knacki/pass/git/entities/GitCommit.java

@@ -104,7 +104,6 @@ public class GitCommit implements GitPackable {
 
     public static class Builder {
         private final GitCommit co;
-        private final GitCommit fParent;
         private final GitObject.GitTree fTree;
         private final ArrayDeque<File> fFilesToPush = new ArrayDeque<>();
         private final Set<String> fToPack = new TreeSet<>();
@@ -112,8 +111,7 @@ public class GitCommit implements GitPackable {
         public Builder(GitCommit parent, String author, String authorEmail, String message) {
             String aut = author +" <" +authorEmail +">";
             co = new GitCommit(parent, aut, aut, message);
-            fParent = parent;
-            fTree = new GitObject.GitTree(parent.GetTree());
+            fTree = new GitObject.GitTree(null, parent.GetTree());
         }
 
         public GitCommit Build() {
@@ -126,32 +124,40 @@ public class GitCommit implements GitPackable {
         }
 
         public GitCommit.Builder AddFile(String relativeFilename, File f) {
-            fFilesToPush.add(f);
-            GitObject obj = fTree.AddItem(relativeFilename, f);
-            do {
-                fToPack.add(obj.GetGitPath());
-                obj = obj.GetParent();
-            } while (obj != null && !"".equals(obj.fName));
-            return this;
-        }
-
-        public GitCommit.Builder RmFile(String relativeFilename) {
-            GitObject obj = fTree.GetObject(relativeFilename);
-            if (obj != null) {
-                GitObject.GitTree parent = obj.GetParent();
-                if (parent != null)
-                    parent.Remove(obj.fName);
+            if (f.exists()) {
+                fFilesToPush.add(f);
+                GitObject obj = fTree.AddItem(relativeFilename, f);
+                do {
+                    fToPack.add(obj.GetGitPath());
+                    obj = obj.GetParent();
+                } while (obj != null);
+            } else {
+                GitObject obj = fTree.GetObjectFullPath(relativeFilename);
+                do {
+                    if (obj != null && obj.GetParent() != null) {
+                        obj.GetParent().Remove(obj.fName);
+                        fToPack.remove(obj.GetGitPath());
+                        obj = obj.GetParent();
+                    }
+                } while (obj instanceof GitObject.GitTree && ((GitObject.GitTree) obj).fItems.size() == 0);
+                if (obj instanceof GitObject.GitTree)
+                    for (String i: fToPack)
+                        if (i.startsWith(relativeFilename +"/"))
+                            fToPack.remove(i);
+                do {
+                    fToPack.add(obj.GetGitPath());
+                    obj.fSha1 = null;
+                    obj = obj.GetParent();
+                } while (obj != null);
             }
             return this;
         }
 
         public SortedSet<GitPackable> PreparePack() {
-            TreeSet packs = new TreeSet<GitPackable>(new GitPackableUtil.Comparator());
-            packs.add(co);
+            TreeSet<GitPackable> packs = new TreeSet<>(new GitPackableUtil.Comparator());
             for (String i: fToPack)
-                packs.add(fTree.GetObjectFullPath(i));
-            if (!fToPack.isEmpty())
-                packs.add(fTree);
+                packs.add("".equals(i) ? fTree : fTree.GetObjectFullPath(i));
+            packs.add(co);
             return packs;
         }
     }

+ 8 - 6
app/src/main/java/info/knacki/pass/git/entities/GitObject.java

@@ -35,8 +35,8 @@ public abstract class GitObject implements Comparable<GitObject>, GitPackable {
             fFile = null;
         }
 
-        private GitBlob(GitBlob copy) {
-            this(copy.fParent, copy.fMode, copy.fName, copy.fSha1);
+        private GitBlob(GitTree parent, GitBlob copy) {
+            this(parent, copy.fMode, copy.fName, copy.fSha1);
         }
 
         GitBlob(GitTree parent, String filename, File f) {
@@ -88,12 +88,13 @@ public abstract class GitObject implements Comparable<GitObject>, GitPackable {
             super(parent, mode, name, sha1);
         }
 
-        GitTree(GitTree copy) {
-            this(copy.fParent, copy.fMode, copy.fName, copy.fSha1);
+        GitTree(GitTree parent, GitTree copy) {
+            this(parent, copy.fMode, copy.fName, copy.fSha1);
             Initialize();
             for (Map.Entry<String, GitObject> o: copy.fItems.entrySet()) {
                 GitObject src = o.getValue();
-                fItems.put(o.getKey(), src instanceof GitTree ? new GitTree((GitTree) src) : new GitBlob((GitBlob) src));
+                GitObject oCopy = src instanceof GitTree ? new GitTree(this, (GitTree) src) : new GitBlob(this, (GitBlob) src);
+                fItems.put(o.getKey(), oCopy);
             }
         }
 
@@ -203,8 +204,9 @@ public abstract class GitObject implements Comparable<GitObject>, GitPackable {
 
             if (obj == null) {
                 if (nextSlash != -1) {
-                    GitTree child = new GitTree(this, filename);
+                    GitTree child = new GitTree(this, filename).Initialize();
                     newItem = child.AddItem(path.substring(nextSlash + 1), f);
+                    fItems.put(filename, child);
                 } else {
                     newItem = new GitBlob(this, filename, f);
                     fItems.put(filename, newItem);

+ 2 - 2
app/src/main/java/info/knacki/pass/git/entities/GitPackableUtil.java

@@ -83,9 +83,9 @@ public class GitPackableUtil {
         public int compare(GitPackable t1, GitPackable t2) {
             if (t1 instanceof GitObject && t2 instanceof GitObject)
                 return ((GitObject) t2).GetDepth() - ((GitObject) t1).GetDepth();
-            if (t1 instanceof GitObject && !(t2 instanceof GitObject))
+            if (t1 instanceof GitObject /* then !(t2 instanceof GitObject) */)
                 return -1;
-            if (!(t1 instanceof GitObject) && t2 instanceof GitObject)
+            if (t2 instanceof GitObject /* then !(t1 instanceof GitObject) */)
                 return 1;
             return (int)(((GitCommit)t1).GetTime().getTime() - ((GitCommit) t2).GetTime().getTime());
         }

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

@@ -19,8 +19,8 @@ import info.knacki.pass.R;
 import info.knacki.pass.git.GitInterface;
 import info.knacki.pass.io.FileInterfaceFactory;
 import info.knacki.pass.io.PathUtils;
-import info.knacki.pass.ui.PasswordClickListener;
-import info.knacki.pass.ui.PasswordListView;
+import info.knacki.pass.ui.passwordList.PasswordClickListener;
+import info.knacki.pass.ui.passwordList.PasswordListView;
 import info.knacki.pass.ui.MainActivity;
 import info.knacki.pass.ui.PasswordPicker;
 

+ 0 - 2
app/src/main/java/info/knacki/pass/io/IFileInterface.java

@@ -1,7 +1,5 @@
 package info.knacki.pass.io;
 
-import java.io.IOException;
-
 import info.knacki.pass.git.GitInterface;
 
 public interface IFileInterface {

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

@@ -1,9 +1,6 @@
 package info.knacki.pass.io;
 
 import android.content.Context;
-import android.os.Environment;
-
-import java.io.File;
 
 public class PathUtils {
     private static final String DATA_DIR_NAME = "pass";

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

@@ -4,6 +4,7 @@ import android.content.res.Configuration;
 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;
@@ -41,6 +42,7 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
         getDelegate().setSupportActionBar(toolbar);
     }
 
+    @NonNull
     @Override
     public MenuInflater getMenuInflater() {
         return getDelegate().getMenuInflater();

+ 6 - 5
app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java

@@ -31,7 +31,8 @@ import info.knacki.pass.git.GitInterfaceFactory;
 import info.knacki.pass.git.entities.GitRef;
 import info.knacki.pass.io.PathUtils;
 import info.knacki.pass.settings.SettingsManager;
-import info.knacki.pass.ui.AlertText;
+import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.views.TextView;
 import info.knacki.pass.ui.GitPullActivity;
 
 /**
@@ -145,11 +146,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             findPreference(getResources().getString(R.string.id_removeall)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                 @Override
                 public boolean onPreferenceClick(Preference preference) {
-                    new AlertText(getActivity())
+                    new AlertPrompt(getActivity())
                             .setTitle(R.string.are_you_sure)
-                            .setView(new AlertText.TextView(getActivity())
+                            .setView(new TextView(getActivity())
                                 .SetText(R.string.about_to_rm_root_star))
-                            .setPositiveButton(R.string.ok, new AlertText.OnClickListener() {
+                            .setPositiveButton(R.string.ok, new AlertPrompt.OnClickListener() {
                                 @Override
                                 public void onClick(DialogInterface dialogInterface, View view) {
                                     new File(PathUtils.GetGitFile(getActivity())).delete();
@@ -158,7 +159,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                                     f.mkdir();
                                 }
                             })
-                            .setNegativeButton(R.string.cancel, new AlertText.OnClickListener() {
+                            .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
                                 @Override
                                 public void onClick(DialogInterface dialogInterface, View view) {
                                 }

+ 0 - 139
app/src/main/java/info/knacki/pass/ui/AlertText.java

@@ -1,139 +0,0 @@
-package info.knacki.pass.ui;
-
-import android.content.Context;
-import android.content.DialogInterface;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.AppCompatCheckBox;
-import android.support.v7.widget.AppCompatEditText;
-import android.support.v7.widget.AppCompatTextView;
-import android.text.InputType;
-import android.view.View;
-import android.widget.LinearLayout;
-
-public class AlertText {
-    protected final AlertDialog.Builder fAlertBuilder;
-    protected View fView = null;
-
-    public interface OnClickListener {
-        void onClick(DialogInterface dialogInterface, View view);
-    }
-
-    public static class SimpleTextEdit extends AppCompatEditText {
-        SimpleTextEdit(Context c) {
-            super(c);
-        }
-
-        public SimpleTextEdit setText(String str) {
-            super.setText(str);
-            super.setSelection(str.length());
-            super.requestFocus();
-            return this;
-        }
-
-        public String getStr() {
-            return super.getText().toString().trim();
-        }
-
-        public SimpleTextEdit SetPassword() {
-            setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
-            return this;
-        }
-    }
-
-    public static class TextEditAndCheckbox extends LinearLayout {
-        private final AppCompatEditText fTextEdit;
-        private final AppCompatCheckBox fCheckbox;
-
-        TextEditAndCheckbox(Context c) {
-            super(c);
-            setOrientation(VERTICAL);
-
-            fTextEdit = new AppCompatEditText(c);
-            addView(fTextEdit);
-
-            fCheckbox = new AppCompatCheckBox(c);
-            addView(fCheckbox);
-        }
-
-        public TextEditAndCheckbox setText(String str) {
-            fTextEdit.setText(str);
-            fTextEdit.setSelection(str.length());
-            fTextEdit.requestFocus();
-            return this;
-        }
-
-        public TextEditAndCheckbox setChecked(boolean value) {
-            fCheckbox.setChecked(value);
-            return this;
-        }
-
-        public TextEditAndCheckbox setCheckboxCaption(int textId) {
-            fCheckbox.setText(textId);
-            return this;
-        }
-
-        public String getStr() {
-            return fTextEdit.getText().toString().trim();
-        }
-
-        public boolean isChecked() {
-            return fCheckbox.isChecked();
-        }
-    }
-
-    public static class TextView extends AppCompatTextView {
-        public TextView(Context c) {
-            super(c);
-        }
-
-        public TextView SetText(int str) {
-            setText(str);
-            return this;
-        }
-    }
-
-    public AlertText(Context c) {
-        fAlertBuilder = new AlertDialog.Builder(c);
-    }
-
-    public AlertText setView(View v) {
-        fView = v;
-        fAlertBuilder.setView(v);
-        return this;
-    }
-
-    public AlertText setCancelable(boolean value) {
-        fAlertBuilder.setCancelable(value);
-        return this;
-    }
-
-    public AlertText setPositiveButton(int textId, final OnClickListener listener) {
-        fAlertBuilder.setPositiveButton(textId, new DialogInterface.OnClickListener() {
-            @Override
-            public void onClick(DialogInterface dialogInterface, int i) {
-                listener.onClick(dialogInterface, fView);
-            }
-        });
-        return this;
-    }
-
-    public AlertText setNegativeButton(int textId, final OnClickListener listener) {
-        fAlertBuilder.setNegativeButton(textId, new DialogInterface.OnClickListener() {
-            @Override
-            public void onClick(DialogInterface dialogInterface, int i) {
-                listener.onClick(dialogInterface, fView);
-            }
-        });
-        return this;
-    }
-
-    public AlertText setTitle(int textId) {
-        fAlertBuilder.setTitle(textId);
-        return this;
-    }
-
-    public AlertText show() {
-        fAlertBuilder.show();
-        return this;
-    }
-}

+ 5 - 2
app/src/main/java/info/knacki/pass/ui/EditPasswordActivity.java

@@ -4,13 +4,13 @@ import android.os.Bundle;
 import android.support.v7.app.AppCompatActivity;
 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;
-import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -47,7 +47,10 @@ public class EditPasswordActivity extends AppCompatActivity {
         findViewById(R.id.saveButton).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                FileInterfaceFactory.GetFileInterface(EditPasswordActivity.this, new PasswordPicker(EditPasswordActivity.this), fOutputFile).WriteFile(fTextEdit.getText().toString(), new GitInterface.OnResponseListener<Void>() {
+                final Editable text = fTextEdit.getText();
+                final String textStr = text == null ? "" : text.toString();
+
+                FileInterfaceFactory.GetFileInterface(EditPasswordActivity.this, new PasswordPicker(EditPasswordActivity.this), fOutputFile).WriteFile(textStr, new GitInterface.OnResponseListener<Void>() {
                     @Override
                     public void onResponse(Void result) {
                         EditPasswordActivity.this.finish();

+ 98 - 24
app/src/main/java/info/knacki/pass/ui/GitPullActivity.java

@@ -1,7 +1,9 @@
 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;
 
@@ -26,11 +28,15 @@ import info.knacki.pass.git.entities.GitCommit;
 import info.knacki.pass.git.entities.GitObject;
 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.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;
 
@@ -103,17 +109,19 @@ public class GitPullActivity extends AppCompatActivity {
         HashMap<String, GitObject.GitBlob> conflictingFiles = new HashMap<>();
 
         for (Map.Entry<String, GitObject.GitBlob>i: fHeadCommit.GetTree().FindAllBlobs().entrySet()) {
-            String remoteLocalHash = localVersion.GetHash(i.getKey());
-            String currentHash = GitSha1.BytesToString(GitSha1.getRawSha1OfFile(new File(PathUtils.GetPassDir(this) +i.getKey())));
-
-            if (remoteLocalHash == null) remoteLocalHash = "";
+            final String remoteKnownHash = localVersion.GetHash(i.getKey(), "");
+            final String remoteHash = GitSha1.BytesToString(i.getValue().GetHash());
+            final String currentHash = GitSha1.BytesToString(GitSha1.getRawSha1OfFile(new File(PathUtils.GetPassDir(this) +i.getKey())));
 
-            final boolean remoteChanged = !remoteLocalHash.equals(GitSha1.BytesToString(i.getValue().GetHash()));
-            final boolean localChanged = !remoteLocalHash.equals(currentHash);
+            final boolean remoteChanged = !remoteKnownHash.equals(remoteHash);
+            final boolean localChanged = !remoteKnownHash.equals(currentHash);
 
             if (remoteChanged && localChanged) {
-                // Conflict
-                conflictingFiles.put(i.getKey(), i.getValue());
+                // Conflict (but file can still has the same content)
+                if (currentHash.equals(remoteHash))
+                    filesToPull.put(i.getKey(), i.getValue());
+                else
+                    conflictingFiles.put(i.getKey(), i.getValue());
             } else if (remoteChanged) {
                 // remote changed
                 filesToPull.put(i.getKey(), i.getValue());
@@ -126,7 +134,7 @@ public class GitPullActivity extends AppCompatActivity {
             if (fHeadCommit.GetTree().GetObjectFullPath(i) == null) {
                 log.finer("removed from remote " +i);
                 final String currentHash = GitSha1.BytesToString(GitSha1.getRawSha1OfFile(new File(PathUtils.GetPassDir(this) +i)));
-                final boolean localChanged = !currentHash.equals(localVersion.GetHash(i));
+                final boolean localChanged = !currentHash.equals(localVersion.GetHash(i, ""));
 
                 if (localChanged)
                     conflictingFiles.put(i, null);
@@ -134,15 +142,59 @@ public class GitPullActivity extends AppCompatActivity {
                     filesToPull.put(i, null);
             }
         }
-        // FIXME new file on local
+        CheckNewFiles(localVersion, new File(PathUtils.GetPassDir(this)), "", filesToPull.keySet(), filesToPush, conflictingFiles.keySet());
+        if (conflictingFiles.isEmpty())
+            SyncFiles(localVersion, filesToPull, filesToPush);
+        else
+            AskForConflicts(localVersion, conflictingFiles, filesToPull, filesToPush);
+    }
 
-        AskForConflicts(localVersion, conflictingFiles, filesToPull, filesToPush);
+    int CheckNewFiles(GitLocal localVersion, File root, String rootPath, Set<String> filesToPull, Set<String> filesToPush, Set<String> conflicts) {
+        int newFiles = 0;
+        for (final File i: root.listFiles()) {
+            if (i.isDirectory()) {
+                newFiles += CheckNewFiles(localVersion, i, rootPath +"/" +i.getName(), filesToPull, filesToPush, conflicts);
+            } else if (i.isFile()){
+                String path = rootPath +"/" +i.getName();
+                if (!localVersion.HasHash(path) && !filesToPull.contains(path) && !conflicts.contains(path)) {
+                    // New file
+                    filesToPush.add(path);
+                    newFiles++;
+                }
+            }
+        }
+        return newFiles;
     }
 
-    void AskForConflicts(GitLocal localVersion, HashMap<String, GitObject.GitBlob> conflicts, HashMap<String, GitObject.GitBlob> filesToPull, Set<String> filesToPush) {
-        // FIXME
-        filesToPush.addAll(conflicts.keySet());
-        SyncFiles(localVersion, filesToPull, filesToPush);
+    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();
+            }
+        });
     }
 
     void RmEmptyDirs(File dir, boolean isRoot) {
@@ -159,6 +211,13 @@ public class GitPullActivity extends AppCompatActivity {
     }
 
     void SyncFiles(final GitLocal localVersion, final HashMap<String, GitObject.GitBlob> filesToPull, final Set<String> filesToPush) {
+        log.severe("Sync : ");
+        for (String i: filesToPush)
+            log.severe("PUSH > " +i);
+        for (String i: filesToPull.keySet())
+            log.severe("PULL > " +i);
+        log.severe("End listing files");
+        //*
         final GitInterface.OnStreamResponseListener<Void> allDone = new GitInterface.OnStreamResponseListener<Void>() {
             @Override
             public void onResponse(Void result) {
@@ -194,20 +253,32 @@ public class GitPullActivity extends AppCompatActivity {
             }
         };
 
-        DownloadBlobs(filesToPull, localVersion, new GitInterface.OnResponseListener<Void>(){
+        final Runnable afterFetching = new Runnable() {
             @Override
-            public void onResponse(Void result) {
+            public void run() {
                 if (filesToPush.size() > 0)
                     PushBlobs(filesToPush, localVersion, allDone);
                 else
-                    allDone.onResponse(result);
+                    allDone.onResponse(null);
             }
+        };
 
-            @Override
-            public void onError(String msg, Throwable e) {
-                allDone.onError(msg, e);
-            }
-        });
+        if (filesToPull.isEmpty()) {
+            afterFetching.run();
+        } else {
+            DownloadBlobs(filesToPull, localVersion, new GitInterface.OnResponseListener<Void>() {
+                @Override
+                public void onResponse(Void result) {
+                    afterFetching.run();
+                }
+
+                @Override
+                public void onError(String msg, Throwable e) {
+                    allDone.onError(msg, e);
+                }
+            });
+        }
+        //*/
     }
 
     void DownloadBlobs(Map<String, GitObject.GitBlob> blobs, final GitLocal localVersion, final GitInterface.OnResponseListener<Void> resp) {
@@ -317,7 +388,10 @@ public class GitPullActivity extends AppCompatActivity {
                 public void onResponse(Void result) {
                     final GitObject.GitTree tree = commit.Build().GetTree();
                     for (String i: files) {
-                        localVersion.SetHash(i, GitSha1.BytesToString(tree.GetObjectFullPath(i).GetHash()));
+                        if (tree.GetObjectFullPath(i) == null)
+                            localVersion.remove(i);
+                        else
+                            localVersion.SetHash(i, GitSha1.BytesToString(tree.GetObjectFullPath(i).GetHash()));
                     }
                     onMsg("Done");
                     resp.onResponse(result);

+ 50 - 20
app/src/main/java/info/knacki/pass/ui/MainActivity.java

@@ -1,6 +1,8 @@
 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;
@@ -26,21 +28,26 @@ import info.knacki.pass.io.IFileInterface;
 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.views.SimpleTextEdit;
+import info.knacki.pass.ui.alertPrompt.views.TextEditAndCheckbox;
+import info.knacki.pass.ui.passwordList.EditablePasswordListView;
+import info.knacki.pass.ui.passwordList.PasswordEditListener;
 
-public class MainActivity extends AppCompatActivity implements PasswordClickListener {
+public class MainActivity extends AppCompatActivity implements PasswordEditListener {
     private final Logger log = Logger.getLogger(MainActivity.class.getName());
-    private PasswordListView vPasswordListView;
+    private EditablePasswordListView vPasswordListView;
 
-    protected AlertText.TextEditAndCheckbox lastFileName = null;
+    protected TextEditAndCheckbox lastFileName = null;
 
     protected void CreateNewFolder() {
-        new AlertText(this)
+        new AlertPrompt(this)
                 .setCancelable(true)
-                .setView(new AlertText.SimpleTextEdit(this))
-                .setPositiveButton(R.string.add, new AlertText.OnClickListener() {
+                .setView(new SimpleTextEdit(this))
+                .setPositiveButton(R.string.add, new AlertPrompt.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialogInterface, View view) {
-                        final String filename = ((AlertText.SimpleTextEdit) view).getStr();
+                        final String filename = ((SimpleTextEdit) view).getStr();
 
                         if (filename.length() == 0) {
                             Toast.makeText(MainActivity.this, "Error: Empty file name", Toast.LENGTH_LONG).show();
@@ -53,7 +60,7 @@ public class MainActivity extends AppCompatActivity implements PasswordClickList
                         vPasswordListView.refresh();
                     }
                 })
-                .setNegativeButton(R.string.cancel, new AlertText.OnClickListener() {
+                .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialogInterface, View s) {
                         dialogInterface.cancel();
@@ -65,18 +72,18 @@ public class MainActivity extends AppCompatActivity implements PasswordClickList
 
     protected void CreateNewPassword() {
         if (lastFileName == null) {
-            lastFileName = new AlertText.TextEditAndCheckbox(this);
+            lastFileName = new TextEditAndCheckbox(this);
         }
-        new AlertText(this)
+        new AlertPrompt(this)
                 .setCancelable(true)
-                .setView(new AlertText.TextEditAndCheckbox(this)
+                .setView(new TextEditAndCheckbox(this)
                     .setText(lastFileName.getStr())
                     .setCheckboxCaption(R.string.add_generate)
                     .setChecked(lastFileName.isChecked()))
-                .setPositiveButton(R.string.add, new AlertText.OnClickListener() {
+                .setPositiveButton(R.string.add, new AlertPrompt.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialogInterface, View view) {
-                        lastFileName = (AlertText.TextEditAndCheckbox) view;
+                        lastFileName = (TextEditAndCheckbox) view;
                         String filename = lastFileName.getStr();
                         if (filename.length() == 0) {
                             Toast.makeText(MainActivity.this, "Error: Empty file name", Toast.LENGTH_LONG).show();
@@ -100,7 +107,7 @@ public class MainActivity extends AppCompatActivity implements PasswordClickList
                         lastFileName = null;
                     }
                 })
-                .setNegativeButton(R.string.cancel, new AlertText.OnClickListener() {
+                .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialogInterface, View s) {
                         dialogInterface.cancel();
@@ -128,12 +135,10 @@ public class MainActivity extends AppCompatActivity implements PasswordClickList
         setContentView(R.layout.activity_pass_list);
 
         setTitle(R.string.title);
-        vPasswordListView = new PasswordListView<>(this, PathUtils.GetPassDir(this));
+        vPasswordListView = new EditablePasswordListView<>(this, PathUtils.GetPassDir(this));
         ((ScrollView)findViewById(R.id.passwordListContainer)).addView(vPasswordListView);
 
         requestPermissions();
-
-        //startActivity(new Intent(this, GitPullActivity.class));
     }
 
     @Override
@@ -166,10 +171,10 @@ public class MainActivity extends AppCompatActivity implements PasswordClickList
     protected void GeneratePassword(final File f) {
         final PasswordGeneratorWizard wiz = new PasswordGeneratorWizard(this);
 
-        new AlertText(this)
+        new AlertPrompt(this)
             .setCancelable(true)
             .setView(wiz)
-            .setPositiveButton(R.string.add, new AlertText.OnClickListener() {
+            .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);
@@ -191,7 +196,7 @@ public class MainActivity extends AppCompatActivity implements PasswordClickList
                     });
                 }
             })
-            .setNegativeButton(R.string.cancel, new AlertText.OnClickListener() {
+            .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
                 @Override
                 public void onClick(DialogInterface dialogInterface, View s) {
                     dialogInterface.cancel();
@@ -216,4 +221,29 @@ public class MainActivity extends AppCompatActivity implements PasswordClickList
     public void OnPasswordClicked(File f) {
         EditFile(f, false);
     }
+
+    @Override
+    public void OnRemovePassword(File f) {
+        f.delete();
+        vPasswordListView.refresh();
+    }
+
+    @Override
+    public void OnCopyToClipboard(File f) {
+        FileInterfaceFactory.GetFileInterface(this, new PasswordPicker(this), f).ReadFile(new GitInterface.OnResponseListener<String>() {
+            @Override
+            public void onResponse(String result) {
+                ClipboardManager clip = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+                if (clip != null) {
+                    clip.setPrimaryClip(ClipData.newPlainText("password", result));
+                }
+            }
+
+            @Override
+            public void onError(String msg, Throwable e) {
+                Toast.makeText(MainActivity.this, "Error: " +msg, Toast.LENGTH_LONG).show();
+                log.log(Level.WARNING, msg, e);
+            }
+        });
+    }
 }

+ 7 - 5
app/src/main/java/info/knacki/pass/ui/PasswordPicker.java

@@ -8,6 +8,8 @@ import android.widget.Toast;
 import info.knacki.pass.R;
 import info.knacki.pass.git.GitInterface;
 import info.knacki.pass.io.FileInterfaceFactory;
+import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+import info.knacki.pass.ui.alertPrompt.views.SimpleTextEdit;
 
 public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
     private final Context fContext;
@@ -18,16 +20,16 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
 
     @Override
     public void GetPassword(final GitInterface.OnResponseListener<String> onPassword) {
-        new AlertText(fContext)
+        new AlertPrompt(fContext)
             .setCancelable(true)
-            .setView(new AlertText.SimpleTextEdit(fContext).SetPassword())
-            .setPositiveButton(R.string.ok, new AlertText.OnClickListener() {
+            .setView(new SimpleTextEdit(fContext).SetPassword())
+            .setPositiveButton(R.string.ok, new AlertPrompt.OnClickListener() {
                 @Override
                 public void onClick(DialogInterface dialogInterface, View view) {
-                    onPassword.onResponse(((AlertText.SimpleTextEdit) view).getStr());
+                    onPassword.onResponse(((SimpleTextEdit) view).getStr());
                 }
             })
-            .setNegativeButton(R.string.cancel, new AlertText.OnClickListener() {
+            .setNegativeButton(R.string.cancel, new AlertPrompt.OnClickListener() {
                 @Override
                 public void onClick(DialogInterface dialogInterface, View s) {
                     onPassword.onResponse(null);

+ 67 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/AlertPrompt.java

@@ -0,0 +1,67 @@
+package info.knacki.pass.ui.alertPrompt;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.View;
+
+public class AlertPrompt {
+    protected final AlertDialog.Builder fAlertBuilder;
+    protected android.app.AlertDialog fDialog;
+    protected View fView = null;
+
+    public interface OnClickListener {
+        void onClick(DialogInterface dialogInterface, View view);
+    }
+
+    public AlertPrompt(Context c) {
+        fAlertBuilder = new AlertDialog.Builder(c);
+    }
+
+    public AlertPrompt setView(View v) {
+        fView = v;
+        fAlertBuilder.setView(v);
+        return this;
+    }
+
+    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);
+            }
+        });
+        return this;
+    }
+
+    public AlertPrompt setNegativeButton(int textId, final OnClickListener listener) {
+        fAlertBuilder.setNegativeButton(textId, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialogInterface, int i) {
+                listener.onClick(dialogInterface, fView);
+            }
+        });
+        return this;
+    }
+
+    public AlertPrompt setTitle(int textId) {
+        fAlertBuilder.setTitle(textId);
+        return this;
+    }
+
+    public AlertPrompt show() {
+        fDialog = fAlertBuilder.create();
+        fDialog.show();
+        return this;
+    }
+
+    public AlertPrompt setPositiveButtonEnabled(boolean enabled) {
+        fDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
+        return this;
+    }
+}

+ 124 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/views/ConflictView.java

@@ -0,0 +1,124 @@
+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;
+import android.widget.RadioGroup;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import info.knacki.pass.R;
+import info.knacki.pass.ui.alertPrompt.AlertPrompt;
+
+public class ConflictView extends HorizontalScrollView {
+    protected final HashMap<String, ConflictState> fConflicts;
+    protected final AlertPrompt fDialog;
+
+    public ConflictView(Context context, AlertPrompt parentDialog, Set<String> conflicts) {
+        super(context);
+        fConflicts = new HashMap<>();
+        fDialog = parentDialog;
+        LinearLayout rootLayout = new LinearLayout(context);
+        rootLayout.setOrientation(LinearLayout.VERTICAL);
+        rootLayout.setPadding(15, 0, 15, 0);
+        addView(rootLayout);
+        for (String i: conflicts) {
+            TextView label = new TextView(context);
+            label.setText(i);
+            rootLayout.addView(label);
+
+            LinearLayout controls = new LinearLayout(context);
+            controls.setOrientation(LinearLayout.HORIZONTAL);
+            RadioGroup input = new RadioGroup(context);
+            input.setOrientation(LinearLayout.HORIZONTAL);
+            RadioButton useMineButton = new RadioButton(context);
+            RadioButton useTheirButton = new RadioButton(context);
+            input.addView(useMineButton);
+            input.addView(useTheirButton);
+            useMineButton.setText(R.string.useMine);
+            useTheirButton.setText(R.string.useTheir);
+            createListeners(useMineButton, useTheirButton, i);
+
+            controls.addView(input);
+            rootLayout.addView(controls);
+        }
+    }
+
+    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();
+                }
+            }
+        });
+        their.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                if (isChecked) {
+                    fConflicts.get(name).UseTheir();
+                    UpdateButtonState();
+                }
+            }
+        });
+    }
+
+    public ConflictViewResult GetResult() {
+        ConflictViewResult res = new ConflictViewResult(new HashSet<String>(), new HashSet<String>());
+        for (Map.Entry<String, ConflictState> i: fConflicts.entrySet()) {
+            if (i.getValue().fUseMine)
+                res.fUseMine.add(i.getKey());
+            if (i.getValue().fUseTheir)
+                res.fUseTheir.add(i.getKey());
+        }
+        return res;
+    }
+
+    public boolean UpdateButtonState() {
+        for (ConflictState i : fConflicts.values()) {
+            if (!i.IsValid()) {
+                fDialog.setPositiveButtonEnabled(false);
+                return false;
+            }
+        }
+        fDialog.setPositiveButtonEnabled(true);
+        return true;
+    }
+
+    private class ConflictState {
+        public boolean fUseTheir = false;
+        public boolean fUseMine = false;
+
+        void UseTheir() {
+            fUseTheir = true;
+            fUseMine = false;
+        }
+
+        void UseMine() {
+            fUseTheir = false;
+            fUseMine = true;
+        }
+
+        public boolean IsValid() {
+            return fUseTheir || fUseMine;
+        }
+    }
+
+    public class ConflictViewResult {
+        public final Set<String> fUseMine;
+        public final Set<String> fUseTheir;
+
+        ConflictViewResult(Set<String> useMine, Set<String> useTheir) {
+            fUseMine = useMine;
+            fUseTheir = useTheir;
+        }
+    }
+}

+ 29 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/views/SimpleTextEdit.java

@@ -0,0 +1,29 @@
+package info.knacki.pass.ui.alertPrompt.views;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatEditText;
+import android.text.Editable;
+import android.text.InputType;
+
+public class SimpleTextEdit extends AppCompatEditText {
+    public SimpleTextEdit(Context c) {
+        super(c);
+    }
+
+    public SimpleTextEdit setText(String str) {
+        super.setText(str);
+        super.setSelection(str.length());
+        super.requestFocus();
+        return this;
+    }
+
+    public String getStr() {
+        Editable text = super.getText();
+        return null == text ? "" : text.toString().trim();
+    }
+
+    public SimpleTextEdit SetPassword() {
+        setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+        return this;
+    }
+}

+ 49 - 0
app/src/main/java/info/knacki/pass/ui/alertPrompt/views/TextEditAndCheckbox.java

@@ -0,0 +1,49 @@
+package info.knacki.pass.ui.alertPrompt.views;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatCheckBox;
+import android.support.v7.widget.AppCompatEditText;
+import android.text.Editable;
+import android.widget.LinearLayout;
+
+public class TextEditAndCheckbox extends LinearLayout {
+    private final AppCompatEditText fTextEdit;
+    private final AppCompatCheckBox fCheckbox;
+
+    public TextEditAndCheckbox(Context c) {
+        super(c);
+        setOrientation(VERTICAL);
+
+        fTextEdit = new AppCompatEditText(c);
+        addView(fTextEdit);
+
+        fCheckbox = new AppCompatCheckBox(c);
+        addView(fCheckbox);
+    }
+
+    public TextEditAndCheckbox setText(String str) {
+        fTextEdit.setText(str);
+        fTextEdit.setSelection(str.length());
+        fTextEdit.requestFocus();
+        return this;
+    }
+
+    public TextEditAndCheckbox setChecked(boolean value) {
+        fCheckbox.setChecked(value);
+        return this;
+    }
+
+    public TextEditAndCheckbox setCheckboxCaption(int textId) {
+        fCheckbox.setText(textId);
+        return this;
+    }
+
+    public String getStr() {
+        Editable text = fTextEdit.getText();
+        return null == text ? "" : text.toString().trim();
+    }
+
+    public boolean isChecked() {
+        return fCheckbox.isChecked();
+    }
+}

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

@@ -0,0 +1,15 @@
+package info.knacki.pass.ui.alertPrompt.views;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatTextView;
+
+public class TextView extends AppCompatTextView {
+    public TextView(Context c) {
+        super(c);
+    }
+
+    public TextView SetText(int str) {
+        setText(str);
+        return this;
+    }
+}

+ 54 - 0
app/src/main/java/info/knacki/pass/ui/passwordList/EditablePasswordListView.java

@@ -0,0 +1,54 @@
+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;
+
+import info.knacki.pass.R;
+
+public class EditablePasswordListView<T extends Activity & PasswordEditListener> extends PasswordListView<T> implements PopupMenu.OnMenuItemClickListener  {
+    private PasswordView fSelectedPassword;
+
+    public EditablePasswordListView(T ctx, String rootPath) {
+        super(ctx, rootPath);
+    }
+
+    @Override
+    protected PasswordView CreateView(PasswordListView<T>.FileIdent file) {
+        final PasswordView pv = super.CreateView(file);
+        pv.setOnLongClickListener(new OnLongClickListener() {
+            @Override
+            public boolean onLongClick(View v) {
+                fSelectedPassword = pv;
+                PopupMenu menu = new PopupMenu(fClickListener, pv);
+                menu.getMenuInflater().inflate(R.menu.context_menu, menu.getMenu());
+                menu.setOnMenuItemClickListener(EditablePasswordListView.this);
+                menu.show();
+                return true;
+            }
+        });
+        return pv;
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        boolean found = false;
+
+        switch (item.getItemId()) {
+            case R.id.remove:
+                fClickListener.OnRemovePassword(new File(fCurrentDir +"/" +fSelectedPassword.vName.getText()));
+                found = true;
+                break;
+
+            case R.id.clipboard:
+                fClickListener.OnCopyToClipboard(new File(fCurrentDir +"/" +fSelectedPassword.vName.getText()));
+                found = true;
+                break;
+        }
+        fSelectedPassword = null;
+        return found;
+    }
+}

+ 1 - 1
app/src/main/java/info/knacki/pass/ui/PasswordClickListener.java → app/src/main/java/info/knacki/pass/ui/passwordList/PasswordClickListener.java

@@ -1,4 +1,4 @@
-package info.knacki.pass.ui;
+package info.knacki.pass.ui.passwordList;
 
 import java.io.File;
 

+ 8 - 0
app/src/main/java/info/knacki/pass/ui/passwordList/PasswordEditListener.java

@@ -0,0 +1,8 @@
+package info.knacki.pass.ui.passwordList;
+
+import java.io.File;
+
+public interface PasswordEditListener extends PasswordClickListener {
+    void OnRemovePassword(File f);
+    void OnCopyToClipboard(File f);
+}

+ 3 - 3
app/src/main/java/info/knacki/pass/ui/PasswordListView.java → app/src/main/java/info/knacki/pass/ui/passwordList/PasswordListView.java

@@ -1,4 +1,4 @@
-package info.knacki.pass.ui;
+package info.knacki.pass.ui.passwordList;
 
 import android.content.Context;
 import android.support.annotation.NonNull;
@@ -12,9 +12,9 @@ import java.util.TreeSet;
 public class PasswordListView<T extends Context & PasswordClickListener> extends LinearLayout {
     public final String fRootPath;
     public String fCurrentDir;
-    protected final PasswordClickListener fClickListener;
+    protected final T fClickListener;
 
-    private class FileIdent implements Comparable<FileIdent> {
+    class FileIdent implements Comparable<FileIdent> {
         final boolean fIsDir;
         final String fName;
         private boolean fParent = false;

+ 1 - 1
app/src/main/java/info/knacki/pass/ui/PasswordView.java → app/src/main/java/info/knacki/pass/ui/passwordList/PasswordView.java

@@ -1,4 +1,4 @@
-package info.knacki.pass.ui;
+package info.knacki.pass.ui.passwordList;
 
 import android.content.Context;
 import android.view.Gravity;

+ 5 - 0
app/src/main/res/menu/context_menu.xml

@@ -0,0 +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>
+</menu>

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

@@ -58,4 +58,11 @@
     <string name="wrongPassword">Mot de passe erroné</string>
     <string name="ok">OK</string>
     <string name="pref_title_remove_all">Supprimer toutes les données</string>
+    <string name="are_you_sure">Êtes-vous sur.e ?</string>
+    <string name="about_to_rm_root_star">Vous allez supprimer tout les mots de passes sauvegardés</string>
+    <string name="conflictingFiles">Résolution des conflits</string>
+    <string name="useMine">Utiliser ma version</string>
+    <string name="useTheir">Utiliser leur version</string>
+    <string name="remove">Supprimer</string>
+    <string name="copy_clipboard">Copier dans le presse-papier</string>
 </resources>

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

@@ -60,4 +60,9 @@
     <string name="pref_title_remove_all">Remove all data</string>
     <string name="are_you_sure">Are you sure ?</string>
     <string name="about_to_rm_root_star">You are about to remove all password from the password store</string>
+    <string name="conflictingFiles">Solve conflicts</string>
+    <string name="useMine">Use mine</string>
+    <string name="useTheir">Use their</string>
+    <string name="remove">Remove</string>
+    <string name="copy_clipboard">Copy to clipboard</string>
 </resources>

+ 1 - 1
build.gradle

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

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

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip