diff options
Diffstat (limited to 'app/src/main')
-rw-r--r-- | app/src/main/AndroidManifest.xml | 2 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordStore.java | 28 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/UserPreference.java | 9 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.java | 67 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java (renamed from app/src/main/java/com/zeapo/pwdstore/git/GitHandler.java) | 469 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java | 170 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/PullOperation.java | 41 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/PushOperation.java | 41 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/config/GitConfigSessionFactory.java | 24 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.java | 70 | ||||
-rw-r--r-- | app/src/main/res/layout/activity_git_clone.xml | 2 | ||||
-rw-r--r-- | app/src/main/res/menu/git_clone.xml | 2 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 5 |
13 files changed, 572 insertions, 358 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4a124c94..cc21dc54 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <activity android:name=".git.GitHandler" + <activity android:name=".git.GitActivity" android:parentActivityName=".PasswordStore"> <meta-data android:name="android.PARENT_ACTIVITY1" android:value="com.zeapo.pwdstore.PasswordStore" /> diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index f7107eff..2af74075 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -18,8 +18,8 @@ import android.view.MenuItem; import android.view.View; import com.zeapo.pwdstore.crypto.PgpHandler; +import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.git.GitAsyncTask; -import com.zeapo.pwdstore.git.GitHandler; import com.zeapo.pwdstore.utils.PasswordItem; import com.zeapo.pwdstore.utils.PasswordRecyclerAdapter; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -151,9 +151,9 @@ public class PasswordStore extends ActionBarActivity { break; } - intent = new Intent(this, GitHandler.class); - intent.putExtra("Operation", GitHandler.REQUEST_PUSH); - startActivityForResult(intent, GitHandler.REQUEST_PUSH); + intent = new Intent(this, GitActivity.class); + intent.putExtra("Operation", GitActivity.REQUEST_PUSH); + startActivityForResult(intent, GitActivity.REQUEST_PUSH); this.leftActivity = true; return true; @@ -163,9 +163,9 @@ public class PasswordStore extends ActionBarActivity { break; } - intent = new Intent(this, GitHandler.class); - intent.putExtra("Operation", GitHandler.REQUEST_PULL); - startActivityForResult(intent, GitHandler.REQUEST_PULL); + intent = new Intent(this, GitActivity.class); + intent.putExtra("Operation", GitActivity.REQUEST_PULL); + startActivityForResult(intent, GitActivity.REQUEST_PULL); this.leftActivity = true; return true; @@ -186,9 +186,9 @@ public class PasswordStore extends ActionBarActivity { } public void getClone(View view){ - Intent intent = new Intent(this, GitHandler.class); - intent.putExtra("Operation", GitHandler.REQUEST_CLONE); - startActivityForResult(intent, GitHandler.REQUEST_CLONE); + Intent intent = new Intent(this, GitActivity.class); + intent.putExtra("Operation", GitActivity.REQUEST_CLONE); + startActivityForResult(intent, GitActivity.REQUEST_CLONE); } private void createRepository() { @@ -228,7 +228,7 @@ public class PasswordStore extends ActionBarActivity { @Override public void onClick(DialogInterface dialogInterface, int i) { Intent intent = new Intent(activity, UserPreference.class); - startActivityForResult(intent, GitHandler.REQUEST_INIT); + startActivityForResult(intent, GitActivity.REQUEST_INIT); } }) .setNegativeButton(this.getResources().getString(R.string.dialog_negative), new DialogInterface.OnClickListener() { @@ -433,7 +433,7 @@ public class PasswordStore extends ActionBarActivity { protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { - if (requestCode == GitHandler.REQUEST_CLONE) + if (requestCode == GitActivity.REQUEST_CLONE) checkLocalRepository(); switch (requestCode) { @@ -446,10 +446,10 @@ public class PasswordStore extends ActionBarActivity { ); refreshListAdapter(); break; - case GitHandler.REQUEST_INIT: + case GitActivity.REQUEST_INIT: initRepository(getCurrentFocus()); break; - case GitHandler.REQUEST_PULL: + case GitActivity.REQUEST_PULL: updateListAdapter(); break; } diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java index 89c1cd36..43198738 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java @@ -10,9 +10,10 @@ import android.preference.PreferenceFragment; import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.MenuItem; +import android.widget.Toast; import com.zeapo.pwdstore.crypto.PgpHandler; -import com.zeapo.pwdstore.git.GitHandler; +import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.utils.PasswordRepository; import org.apache.commons.io.FileUtils; @@ -105,8 +106,8 @@ public class UserPreference extends ActionBarActivity implements Preference.OnPr break; case "git_server_info": { - Intent intent = new Intent(this, GitHandler.class); - intent.putExtra("Operation", GitHandler.EDIT_SERVER); + Intent intent = new Intent(this, GitActivity.class); + intent.putExtra("Operation", GitActivity.EDIT_SERVER); startActivityForResult(intent, EDIT_GIT_INFO); } break; @@ -153,7 +154,7 @@ public class UserPreference extends ActionBarActivity implements Preference.OnPr { try { copySshKey(data.getData()); - Log.i("PREF", "Got key"); + Toast.makeText(this, this.getResources().getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show(); setResult(RESULT_OK); finish(); } catch (IOException e) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.java b/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.java new file mode 100644 index 00000000..dc94ce3b --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.java @@ -0,0 +1,67 @@ +package com.zeapo.pwdstore.git; + +import android.app.Activity; + +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; + +import java.io.File; + +public class CloneOperation extends GitOperation { + private static final String TAG = "CLONEOPT"; + + /** + * Creates a new clone operation + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ + public CloneOperation(File fileDir, Activity callingActivity) { + super(fileDir, callingActivity); + } + + /** + * Sets the command using the repository uri + * @param uri the uri of the repository + * @return the current object + */ + public CloneOperation setCommand(String uri) { + this.command = Git.cloneRepository(). + setCloneAllBranches(true). + setDirectory(repository.getWorkTree()). + setURI(uri); + return this; + } + + /** + * sets the authentication for user/pwd scheme + * @param username the username + * @param password the password + * @return the current object + */ + @Override + public CloneOperation setAuthentication(String username, String password) { + super.setAuthentication(username, password); + return this; + } + + /** + * sets the authentication for the ssh-key scheme + * @param sshKey the ssh-key file + * @param username the username + * @param passphrase the passphrase + * @return the current object + */ + @Override + public CloneOperation setAuthentication(File sshKey, String username, String passphrase) { + super.setAuthentication(sshKey, username, passphrase); + return this; + } + + @Override + public void execute() throws Exception { + if (this.provider != null) { + ((CloneCommand) this.command).setCredentialsProvider(this.provider); + } + new GitAsyncTask(callingActivity, true, false, CloneCommand.class).execute(this.command); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitHandler.java b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java index 85872d65..637d2052 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitHandler.java +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java @@ -10,22 +10,17 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v7.app.ActionBarActivity; import android.text.Editable; -import android.text.InputType; import android.text.TextWatcher; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; -import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.UserInfo; import com.zeapo.pwdstore.R; import com.zeapo.pwdstore.UserPreference; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -34,29 +29,17 @@ import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; import org.apache.commons.io.FileUtils; -import org.eclipse.jgit.api.GitCommand; -import org.eclipse.jgit.api.PullCommand; -import org.eclipse.jgit.api.PushCommand; -import org.eclipse.jgit.errors.UnsupportedCredentialItem; -import org.eclipse.jgit.transport.CredentialItem; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.CredentialsProviderUserInfo; -import org.eclipse.jgit.transport.JschConfigSessionFactory; -import org.eclipse.jgit.transport.OpenSshConfig; -import org.eclipse.jgit.transport.SshSessionFactory; -import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; -import org.eclipse.jgit.util.FS; import java.io.File; import java.io.IOException; -import java.lang.reflect.Method; import java.util.regex.Matcher; import java.util.regex.Pattern; // TODO move the messages to strings.xml -public class GitHandler extends ActionBarActivity { +public class GitActivity extends ActionBarActivity { + private static final String TAG = "GitAct"; private Activity activity; private Context context; @@ -77,8 +60,6 @@ public class GitHandler extends ActionBarActivity { public static final int REQUEST_INIT = 104; public static final int EDIT_SERVER = 105; - private static final int GET_SSH_KEY_FROM_CLONE = 201; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -98,11 +79,11 @@ public class GitHandler extends ActionBarActivity { case REQUEST_CLONE: case EDIT_SERVER: setContentView(R.layout.activity_git_clone); + setTitle(R.string.title_activity_git_clone); final Spinner protcol_spinner = (Spinner) findViewById(R.id.clone_protocol); final Spinner connection_mode_spinner = (Spinner) findViewById(R.id.connection_mode); - // init the spinner for connection modes final ArrayAdapter<CharSequence> connection_mode_adapter = ArrayAdapter.createFromResource(this, R.array.connection_modes, android.R.layout.simple_spinner_item); @@ -131,9 +112,9 @@ public class GitHandler extends ActionBarActivity { new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { - protocol = ((Spinner)findViewById(R.id.clone_protocol)).getSelectedItem().toString(); + protocol = ((Spinner) findViewById(R.id.clone_protocol)).getSelectedItem().toString(); if (protocol.equals("ssh://")) { - ((EditText)findViewById(R.id.clone_uri)).setHint("user@hostname:path"); + ((EditText) findViewById(R.id.clone_uri)).setHint("user@hostname:path"); ((EditText) findViewById(R.id.server_port)).setHint(R.string.default_ssh_port); @@ -141,7 +122,7 @@ public class GitHandler extends ActionBarActivity { connection_mode_spinner.setSelection(0); connection_mode_spinner.setEnabled(true); } else { - ((EditText)findViewById(R.id.clone_uri)).setHint("hostname/path"); + ((EditText) findViewById(R.id.clone_uri)).setHint("hostname/path"); ((EditText) findViewById(R.id.server_port)).setHint(R.string.default_https_port); @@ -158,13 +139,24 @@ public class GitHandler extends ActionBarActivity { } ); + if (protocol.equals("ssh://")) { + protcol_spinner.setSelection(0); + } else { + protcol_spinner.setSelection(1); + } + + if (connectionMode.equals("ssh-key")) { + connection_mode_spinner.setSelection(0); + } else { + connection_mode_spinner.setSelection(1); + } // init the server information final EditText server_url = ((EditText) findViewById(R.id.server_url)); final EditText server_port = ((EditText) findViewById(R.id.server_port)); final EditText server_path = ((EditText) findViewById(R.id.server_path)); final EditText server_user = ((EditText) findViewById(R.id.server_user)); - final EditText server_uri = ((EditText)findViewById(R.id.clone_uri)); + final EditText server_uri = ((EditText) findViewById(R.id.clone_uri)); View.OnFocusChangeListener updateListener = new View.OnFocusChangeListener() { @Override @@ -180,7 +172,8 @@ public class GitHandler extends ActionBarActivity { server_url.addTextChangedListener(new TextWatcher() { @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { @@ -189,11 +182,13 @@ public class GitHandler extends ActionBarActivity { } @Override - public void afterTextChanged(Editable editable) { } + public void afterTextChanged(Editable editable) { + } }); server_port.addTextChangedListener(new TextWatcher() { @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { @@ -202,11 +197,13 @@ public class GitHandler extends ActionBarActivity { } @Override - public void afterTextChanged(Editable editable) { } + public void afterTextChanged(Editable editable) { + } }); server_user.addTextChangedListener(new TextWatcher() { @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { @@ -215,11 +212,13 @@ public class GitHandler extends ActionBarActivity { } @Override - public void afterTextChanged(Editable editable) { } + public void afterTextChanged(Editable editable) { + } }); server_path.addTextChangedListener(new TextWatcher() { @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { @@ -228,7 +227,8 @@ public class GitHandler extends ActionBarActivity { } @Override - public void afterTextChanged(Editable editable) { } + public void afterTextChanged(Editable editable) { + } }); server_uri.addTextChangedListener(new TextWatcher() { @@ -247,8 +247,7 @@ public class GitHandler extends ActionBarActivity { } }); - if (operationCode == EDIT_SERVER) - { + if (operationCode == EDIT_SERVER) { findViewById(R.id.clone_button).setVisibility(View.INVISIBLE); findViewById(R.id.save_button).setVisibility(View.VISIBLE); } else { @@ -256,21 +255,24 @@ public class GitHandler extends ActionBarActivity { findViewById(R.id.save_button).setVisibility(View.INVISIBLE); } + updateURI(); break; case REQUEST_PULL: - authenticateAndRun("pullOperation"); + pullFromRepository(); break; case REQUEST_PUSH: - authenticateAndRun("pushOperation"); + pushToRepository(); break; } } - /** Fills in the server_uri field with the information coming from other fields */ + /** + * Fills in the server_uri field with the information coming from other fields + */ private void updateURI() { EditText uri = (EditText) findViewById(R.id.clone_uri); EditText server_url = ((EditText) findViewById(R.id.server_url)); @@ -278,11 +280,9 @@ public class GitHandler extends ActionBarActivity { EditText server_path = ((EditText) findViewById(R.id.server_path)); EditText server_user = ((EditText) findViewById(R.id.server_user)); - if (uri != null) { - switch (protocol) - { - case "ssh://": - { + if (uri != null) { + switch (protocol) { + case "ssh://": { String hostname = server_user.getText() + "@" + @@ -306,8 +306,7 @@ public class GitHandler extends ActionBarActivity { if (!hostname.equals("@:")) uri.setText(hostname); } break; - case "https://": - { + case "https://": { StringBuilder hostname = new StringBuilder(); hostname.append(server_url.getText().toString().trim()); @@ -325,13 +324,15 @@ public class GitHandler extends ActionBarActivity { } break; default: - break; + break; } } } - /** Splits the information in server_uri into the other fields */ + /** + * Splits the information in server_uri into the other fields + */ private void splitURI() { EditText server_uri = (EditText) findViewById(R.id.clone_uri); EditText server_url = ((EditText) findViewById(R.id.server_url)); @@ -397,76 +398,35 @@ public class GitHandler extends ActionBarActivity { return super.onOptionsItemSelected(item); } - protected class GitConfigSessionFactory extends JschConfigSessionFactory { - - protected void configure(OpenSshConfig.Host hc, Session session) { - session.setConfig("StrictHostKeyChecking", "no"); - } + /** + * Saves the configuration found in the form + */ + private void saveConfiguration() { + // remember the settings + SharedPreferences.Editor editor = settings.edit(); - @Override - protected JSch - getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { - JSch jsch = super.getJSch(hc, fs); - jsch.removeAllIdentity(); - return jsch; - } + editor.putString("git_remote_server", ((EditText) findViewById(R.id.server_url)).getText().toString()); + editor.putString("git_remote_location", ((EditText) findViewById(R.id.server_path)).getText().toString()); + editor.putString("git_remote_username", ((EditText) findViewById(R.id.server_user)).getText().toString()); + editor.putString("git_remote_protocol", protocol); + editor.putString("git_remote_auth", connectionMode); + editor.putString("git_remote_port", port); + editor.commit(); } - protected class SshConfigSessionFactory extends GitConfigSessionFactory { - private String sshKey; - private String passphrase; - - public SshConfigSessionFactory(String sshKey, String passphrase) { - this.sshKey = sshKey; - this.passphrase = passphrase; - } - - @Override - protected JSch - getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { - JSch jsch = super.getJSch(hc, fs); - jsch.removeAllIdentity(); - jsch.addIdentity(sshKey); - return jsch; - } - - @Override - protected void configure(OpenSshConfig.Host hc, Session session) { - session.setConfig("StrictHostKeyChecking", "no"); - - CredentialsProvider provider = new CredentialsProvider() { - @Override - public boolean isInteractive() { - return false; - } - - @Override - public boolean supports(CredentialItem... items) { - return true; - } - - @Override - public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem { - for (CredentialItem item : items) { - if (item instanceof CredentialItem.Username) { - ((CredentialItem.Username) item).setValue(settings.getString("git_remote_username", "git")); - continue; - } - if (item instanceof CredentialItem.StringType) { - ((CredentialItem.StringType) item).setValue(passphrase); - } - } - return true; - } - }; - UserInfo userInfo = new CredentialsProviderUserInfo(session, provider); - session.setUserInfo(userInfo); - } + /** + * Save the repository information to the shared preferences settings + * + * @param view + */ + public void saveConfiguration(View view) { + saveConfiguration(); + PasswordRepository.addRemote("origin", ((EditText) findViewById(R.id.clone_uri)).getText().toString(), true); } - /** * Clones the repository, the directory exists, deletes it + * * @param view */ public void cloneRepository(View view) { @@ -505,8 +465,7 @@ public class GitHandler extends ActionBarActivity { username = hostname.split("@")[0]; } - - if (localDir.exists()) { + if (localDir.exists() && localDir.listFiles().length != 0) { new AlertDialog.Builder(this). setTitle(R.string.dialog_delete_title). setMessage(R.string.dialog_delete_msg). @@ -516,7 +475,15 @@ public class GitHandler extends ActionBarActivity { public void onClick(DialogInterface dialog, int id) { try { FileUtils.deleteDirectory(localDir); - authenticateAndRun("cloneOperation"); + try { + new CloneOperation(localDir, activity) + .setCommand(hostname) + .executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key")); + } catch (Exception e) { + //This is what happens when jgit fails :( + //TODO Handle the diffent cases of exceptions + e.printStackTrace(); + } } catch (IOException e) { //TODO Handle the exception correctly if we are unable to delete the directory... e.printStackTrace(); @@ -538,8 +505,12 @@ public class GitHandler extends ActionBarActivity { ). show(); } else { + saveConfiguration(); + try { - authenticateAndRun("cloneOperation"); + new CloneOperation(localDir, activity) + .setCommand(hostname) + .executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key")); } catch (Exception e) { //This is what happens when jgit fails :( //TODO Handle the diffent cases of exceptions @@ -548,53 +519,28 @@ public class GitHandler extends ActionBarActivity { } } - /** - * Save the repository information to the shared preferences settings - * @param view + * Pull the latest changes from the remote repository and merges them locally */ - public void saveConfiguration(View view) { - // remember the settings - SharedPreferences.Editor editor = settings.edit(); - - editor.putString("git_remote_server", ((EditText) findViewById(R.id.server_url)).getText().toString()); - editor.putString("git_remote_location", ((EditText) findViewById(R.id.server_path)).getText().toString()); - editor.putString("git_remote_username", ((EditText) findViewById(R.id.server_user)).getText().toString()); - editor.putString("git_remote_protocol", protocol); - editor.putString("git_remote_auth", connectionMode); - editor.putString("git_remote_port", port); - editor.commit(); - - PasswordRepository.addRemote("origin", ((EditText) findViewById(R.id.clone_uri)).getText().toString(), true); + public void pullFromRepository() { + syncRepository(REQUEST_PULL); } - public void cloneOperation(UsernamePasswordCredentialsProvider provider) { - - // remember the settings - SharedPreferences.Editor editor = settings.edit(); - - editor.putString("git_remote_server", ((EditText) findViewById(R.id.server_url)).getText().toString()); - editor.putString("git_remote_location", ((EditText) findViewById(R.id.server_path)).getText().toString()); - editor.putString("git_remote_username", ((EditText) findViewById(R.id.server_user)).getText().toString()); - editor.putString("git_remote_protocol", protocol); - editor.putString("git_remote_auth", connectionMode); - editor.putString("git_remote_port", port); - editor.commit(); - - CloneCommand cmd = Git.cloneRepository(). - setCredentialsProvider(provider). - setCloneAllBranches(true). - setDirectory(localDir). - setURI(hostname); - - new GitAsyncTask(activity, true, false, CloneCommand.class).execute(cmd); + /** + * Pushes the latest changes from the local repository to the remote one + */ + public void pushToRepository() { + syncRepository(REQUEST_PUSH); } - public void pullOperation(UsernamePasswordCredentialsProvider provider) { - + /** + * Syncs the local repository with the remote one (either pull or push) + * @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH + */ + private void syncRepository(int operation) { if (settings.getString("git_remote_username", "").isEmpty() || - settings.getString("git_remote_server", "").isEmpty() || - settings.getString("git_remote_location", "").isEmpty() ) + settings.getString("git_remote_server", "").isEmpty() || + settings.getString("git_remote_location", "").isEmpty()) new AlertDialog.Builder(this) .setMessage(activity.getResources().getString(R.string.set_information_dialog_text)) .setPositiveButton(activity.getResources().getString(R.string.dialog_positive), new DialogInterface.OnClickListener() { @@ -616,185 +562,23 @@ public class GitHandler extends ActionBarActivity { else { // check that the remote origin is here, else add it - PasswordRepository.addRemote("origin", settings.getString("git_remote_username", "user") - + "@" + - settings.getString("git_remote_server", "server.com").trim() - + ":" + - settings.getString("git_remote_location", "path/to/repository"), false); - - GitCommand cmd; - if (provider != null) - cmd = new Git(PasswordRepository.getRepository(new File(""))) - .pull() - .setRebase(true) - .setRemote("origin") - .setCredentialsProvider(provider); - else - cmd = new Git(PasswordRepository.getRepository(new File(""))) - .pull() - .setRebase(true) - .setRemote("origin"); - - new GitAsyncTask(activity, true, false, PullCommand.class).execute(cmd); - } - } - + PasswordRepository.addRemote("origin", hostname, false); + GitOperation op; - public void pushOperation(UsernamePasswordCredentialsProvider provider) { - if (settings.getString("git_remote_username", "user").isEmpty() || - settings.getString("git_remote_server", "server.com").trim().isEmpty() || - settings.getString("git_remote_location", "path/to/repository").isEmpty() ) - new AlertDialog.Builder(this) - .setMessage("You have to set the information about the server before synchronizing with the server") - .setPositiveButton("On my way!", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - Intent intent = new Intent(activity, UserPreference.class); - startActivityForResult(intent, REQUEST_PUSH); - } - }) - .setNegativeButton("Nah... later", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - // do nothing :( - setResult(RESULT_OK); - finish(); - } - }) - .show(); - - else { - // check that the remote origin is here, else add it - PasswordRepository.addRemote("origin", settings.getString("git_remote_username", "user") - + "@" + - settings.getString("git_remote_server", "server.com").trim() - + ":" + - settings.getString("git_remote_location", "path/to/repository"), false); - - GitCommand cmd; - if (provider != null) - cmd = new Git(PasswordRepository.getRepository(new File(""))) - .push() - .setPushAll() - .setRemote("origin") - .setCredentialsProvider(provider); - else - cmd = new Git(PasswordRepository.getRepository(new File(""))) - .push() - .setPushAll() - .setRemote("origin"); - - - new GitAsyncTask(activity, true, false, PushCommand.class).execute(cmd); - } - } - - /** Finds the method and provides it with authentication paramters via invokeWithAuthentication */ - private void authenticateAndRun(String operation) { - try { - invokeWithAuthentication(this, GitHandler.class.getMethod(operation, UsernamePasswordCredentialsProvider.class)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** Calls a method encapsulating a GitCommand and providing it with authentication parameters - * - * @param activity - * @param method - */ - private void invokeWithAuthentication(final GitHandler activity, final Method method) { - - if (connectionMode.equalsIgnoreCase("ssh-key")) { - final File sshKey = new File(getFilesDir() + "/.ssh_key"); - if (!sshKey.exists()) { - new AlertDialog.Builder(this) - .setMessage(activity.getResources().getString(R.string.ssh_preferences_dialog_text)) - .setTitle(activity.getResources().getString(R.string.ssh_preferences_dialog_title)) - .setPositiveButton(activity.getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - try { - Intent intent = new Intent(getApplicationContext(), UserPreference.class); - intent.putExtra("operation", "get_ssh_key"); - startActivityForResult(intent, GET_SSH_KEY_FROM_CLONE); - } catch (Exception e) { - System.out.println("Exception caught :("); - e.printStackTrace(); - } - } - }).setNegativeButton(activity.getResources().getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - // Do nothing... - } - }).show(); + if (operation == REQUEST_PULL) { + op = new PullOperation(localDir, activity).setCommand(); + } else if (operation == REQUEST_PUSH) { + op = new PushOperation(localDir, activity).setCommand(); } else { - final EditText passphrase = new EditText(activity); - passphrase.setHint("Passphrase"); - passphrase.setWidth(LinearLayout.LayoutParams.MATCH_PARENT); - passphrase.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - - new AlertDialog.Builder(activity) - .setTitle(activity.getResources().getString(R.string.passphrase_dialog_title)) - .setMessage(activity.getResources().getString(R.string.passphrase_dialog_text)) - .setView(passphrase) - .setPositiveButton(activity.getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - - SshSessionFactory.setInstance(new GitConfigSessionFactory()); - try { - - JschConfigSessionFactory sessionFactory = new SshConfigSessionFactory(sshKey.getAbsolutePath(), passphrase.getText().toString()); - SshSessionFactory.setInstance(sessionFactory); - - try { - method.invoke(activity, (UsernamePasswordCredentialsProvider) null); - } catch (Exception e){ - e.printStackTrace(); - } - - } catch (Exception e){ - e.printStackTrace(); - } - - } - }).setNegativeButton(activity.getResources().getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // Do nothing. - } - }).show(); + Log.e(TAG, "Sync operation not recognized : " + operation); + return; } - } else { - final EditText password = new EditText(activity); - password.setHint("Password"); - password.setWidth(LinearLayout.LayoutParams.MATCH_PARENT); - password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - - new AlertDialog.Builder(activity) - .setTitle(activity.getResources().getString(R.string.passphrase_dialog_title)) - .setMessage(activity.getResources().getString(R.string.password_dialog_text)) - .setView(password) - .setPositiveButton(activity.getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - - SshSessionFactory.setInstance(new GitConfigSessionFactory()); - try { - method.invoke(activity, - new UsernamePasswordCredentialsProvider( - settings.getString("git_remote_username", "git"), - password.getText().toString()) - ); - } catch (Exception e){ - e.printStackTrace(); - } - } - }).setNegativeButton(activity.getResources().getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // Do nothing. - } - }).show(); + try { + op.executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key")); + } catch (Exception e) { + e.printStackTrace(); + } } } @@ -807,16 +591,31 @@ public class GitHandler extends ActionBarActivity { } if (resultCode == RESULT_OK) { + GitOperation op; switch (requestCode) { case REQUEST_PULL: - authenticateAndRun("pullOperation"); + op = new PullOperation(localDir, activity).setCommand(); break; + case REQUEST_PUSH: - authenticateAndRun("pushOperation"); + op = new PushOperation(localDir, activity).setCommand(); break; - case GET_SSH_KEY_FROM_CLONE: - authenticateAndRun("cloneOperation"); + + case GitOperation.GET_SSH_KEY_FROM_CLONE: + op = new CloneOperation(localDir, activity).setCommand(hostname); + break; + default: + Log.e(TAG, "Operation not recognized : " + resultCode); + setResult(RESULT_CANCELED); + finish(); + return; + } + + try { + op.executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key")); + } catch (Exception e) { + e.printStackTrace(); } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java new file mode 100644 index 00000000..60112c90 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java @@ -0,0 +1,170 @@ +package com.zeapo.pwdstore.git; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.support.annotation.Nullable; +import android.text.InputType; +import android.util.Log; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.zeapo.pwdstore.R; +import com.zeapo.pwdstore.UserPreference; +import com.zeapo.pwdstore.git.config.GitConfigSessionFactory; +import com.zeapo.pwdstore.git.config.SshConfigSessionFactory; +import com.zeapo.pwdstore.utils.PasswordRepository; + +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.GitCommand; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; + +import java.io.File; + +public abstract class GitOperation { + private static final String TAG = "GitOpt"; + public static final int GET_SSH_KEY_FROM_CLONE = 201; + + protected final Repository repository; + protected final Activity callingActivity; + protected UsernamePasswordCredentialsProvider provider; + protected GitCommand command; + + /** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ + public GitOperation(File fileDir, Activity callingActivity) { + this.repository = PasswordRepository.getRepository(fileDir); + this.callingActivity = callingActivity; + } + + /** + * Sets the authentication using user/pwd scheme + * + * @param username the username + * @param password the password + * @return the current object + */ + public GitOperation setAuthentication(String username, String password) { + SshSessionFactory.setInstance(new GitConfigSessionFactory()); + this.provider = new UsernamePasswordCredentialsProvider(username, password); + return this; + } + + /** + * Sets the authentication using ssh-key scheme + * + * @param sshKey the ssh-key file + * @param username the username + * @param passphrase the passphrase + * @return the current object + */ + public GitOperation setAuthentication(File sshKey, String username, String passphrase) { + JschConfigSessionFactory sessionFactory = new SshConfigSessionFactory(sshKey.getAbsolutePath(), username, passphrase); + SshSessionFactory.setInstance(sessionFactory); + this.provider = null; + return this; + } + + /** + * Executes the GitCommand in an async task + * + * @throws Exception + */ + public abstract void execute() throws Exception; + + /** + * Executes the GitCommand in an async task after creating the authentication + * + * @param connectionMode the server-connection mode + * @param username the username + * @param sshKey the ssh-key file + * @throws Exception + */ + public void executeAfterAuthentication(String connectionMode, final String username, @Nullable final File sshKey) throws Exception { + if (connectionMode.equalsIgnoreCase("ssh-key")) { + if (sshKey == null || !sshKey.exists()) { + new AlertDialog.Builder(callingActivity) + .setMessage(callingActivity.getResources().getString(R.string.ssh_preferences_dialog_text)) + .setTitle(callingActivity.getResources().getString(R.string.ssh_preferences_dialog_title)) + .setPositiveButton(callingActivity.getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + try { + // Ask the UserPreference to provide us with the ssh-key + // onResult has to be handled by the callingActivity + Intent intent = new Intent(callingActivity.getApplicationContext(), UserPreference.class); + intent.putExtra("operation", "get_ssh_key"); + callingActivity.startActivityForResult(intent, GET_SSH_KEY_FROM_CLONE); + } catch (Exception e) { + System.out.println("Exception caught :("); + e.printStackTrace(); + } + } + }).setNegativeButton(callingActivity.getResources().getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + // Do nothing... + } + }).show(); + } else { + final EditText passphrase = new EditText(callingActivity); + passphrase.setHint("Passphrase"); + passphrase.setWidth(LinearLayout.LayoutParams.MATCH_PARENT); + passphrase.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + new AlertDialog.Builder(callingActivity) + .setTitle(callingActivity.getResources().getString(R.string.passphrase_dialog_title)) + .setMessage(callingActivity.getResources().getString(R.string.passphrase_dialog_text)) + .setView(passphrase) + .setPositiveButton(callingActivity.getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + try { + // Authenticate using the ssh-key and then execute the command + setAuthentication(sshKey, username, passphrase.getText().toString()).execute(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + }).setNegativeButton(callingActivity.getResources().getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // Do nothing. + } + }).show(); + } + } else { + final EditText password = new EditText(callingActivity); + password.setHint("Password"); + password.setWidth(LinearLayout.LayoutParams.MATCH_PARENT); + password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + new AlertDialog.Builder(callingActivity) + .setTitle(callingActivity.getResources().getString(R.string.passphrase_dialog_title)) + .setMessage(callingActivity.getResources().getString(R.string.password_dialog_text)) + .setView(password) + .setPositiveButton(callingActivity.getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // authenticate using the user/pwd and then execute the command + try { + setAuthentication(username, password.getText().toString()).execute(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + }).setNegativeButton(callingActivity.getResources().getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // Do nothing. + } + }).show(); + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.java b/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.java new file mode 100644 index 00000000..abf4bfe6 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.java @@ -0,0 +1,41 @@ +package com.zeapo.pwdstore.git; + +import android.app.Activity; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.PullCommand; + +import java.io.File; + +public class PullOperation extends GitOperation { + + /** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ + public PullOperation(File fileDir, Activity callingActivity) { + super(fileDir, callingActivity); + } + + /** + * Sets the command + * @return the current object + */ + public PullOperation setCommand() { + this.command = new Git(repository) + .pull() + .setRebase(true) + .setRemote("origin"); + return this; + } + + @Override + public void execute() throws Exception { + if (this.provider != null) { + ((PullCommand) this.command).setCredentialsProvider(this.provider); + } + new GitAsyncTask(callingActivity, true, false, PullCommand.class).execute(this.command); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.java b/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.java new file mode 100644 index 00000000..73ed05ff --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.java @@ -0,0 +1,41 @@ +package com.zeapo.pwdstore.git; + +import android.app.Activity; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.PushCommand; + +import java.io.File; + +public class PushOperation extends GitOperation { + + /** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ + public PushOperation(File fileDir, Activity callingActivity) { + super(fileDir, callingActivity); + } + + /** + * Sets the command + * @return the current object + */ + public PushOperation setCommand() { + this.command = new Git(repository) + .push() + .setPushAll() + .setRemote("origin"); + return this; + } + + @Override + public void execute() throws Exception { + if (this.provider != null) { + ((PushCommand) this.command).setCredentialsProvider(this.provider); + } + new GitAsyncTask(callingActivity, true, false, PushCommand.class).execute(this.command); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/GitConfigSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/GitConfigSessionFactory.java new file mode 100644 index 00000000..ab23361e --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/GitConfigSessionFactory.java @@ -0,0 +1,24 @@ +package com.zeapo.pwdstore.git.config; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; + +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.util.FS; + +public class GitConfigSessionFactory extends JschConfigSessionFactory { + + protected void configure(OpenSshConfig.Host hc, Session session) { + session.setConfig("StrictHostKeyChecking", "no"); + } + + @Override + protected JSch + getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { + JSch jsch = super.getJSch(hc, fs); + jsch.removeAllIdentity(); + return jsch; + } +}
\ No newline at end of file diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.java new file mode 100644 index 00000000..333a6325 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.java @@ -0,0 +1,70 @@ +package com.zeapo.pwdstore.git.config; + +import android.content.SharedPreferences; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UserInfo; + +import org.eclipse.jgit.errors.UnsupportedCredentialItem; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.CredentialsProviderUserInfo; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.util.FS; + +public class SshConfigSessionFactory extends GitConfigSessionFactory { + private String sshKey; + private String passphrase; + private String username; + + public SshConfigSessionFactory(String sshKey, String username, String passphrase) { + this.sshKey = sshKey; + this.passphrase = passphrase; + this.username = username; + } + + @Override + protected JSch + getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { + JSch jsch = super.getJSch(hc, fs); + jsch.removeAllIdentity(); + jsch.addIdentity(sshKey); + return jsch; + } + + @Override + protected void configure(OpenSshConfig.Host hc, Session session) { + session.setConfig("StrictHostKeyChecking", "no"); + + CredentialsProvider provider = new CredentialsProvider() { + @Override + public boolean isInteractive() { + return false; + } + + @Override + public boolean supports(CredentialItem... items) { + return true; + } + + @Override + public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem { + for (CredentialItem item : items) { + if (item instanceof CredentialItem.Username) { + ((CredentialItem.Username) item).setValue(username); + continue; + } + if (item instanceof CredentialItem.StringType) { + ((CredentialItem.StringType) item).setValue(passphrase); + } + } + return true; + } + }; + UserInfo userInfo = new CredentialsProviderUserInfo(session, provider); + session.setUserInfo(userInfo); + } +}
\ No newline at end of file diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml index 07d57934..d53a2201 100644 --- a/app/src/main/res/layout/activity_git_clone.xml +++ b/app/src/main/res/layout/activity_git_clone.xml @@ -6,7 +6,7 @@ android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" - tools:context="com.zeapo.pwdstore.git.GitHandler" + tools:context="com.zeapo.pwdstore.git.GitActivity" android:background="@android:color/white"> <LinearLayout android:layout_width="match_parent" diff --git a/app/src/main/res/menu/git_clone.xml b/app/src/main/res/menu/git_clone.xml index 4a240712..092a8b2b 100644 --- a/app/src/main/res/menu/git_clone.xml +++ b/app/src/main/res/menu/git_clone.xml @@ -1,7 +1,7 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:pwstore="http://schemas.android.com/apk/res-auto" - tools:context="com.zeapo.pwdstore.git.GitHandler" > + tools:context="com.zeapo.pwdstore.git.GitActivity" > <item android:id="@+id/user_pref" android:title="@string/action_settings" android:orderInCategory="100" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86885674..8cf8d140 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,7 +12,7 @@ <string name="dialog_delete_msg">Target directory already exist. Current version support only a single store. Do you want to delete the current password store directory?</string> <string name="dialog_delete">Delete directory</string> <string name="dialog_do_not_delete">Cancel</string> - <string name="title_activity_git_clone">Clone repository</string> + <string name="title_activity_git_clone">Repository information</string> <!-- Password Store --> <string name="creation_dialog_text">Please clone or create a new repository below before trying to add a password or any synchronization operation.</string> @@ -41,7 +41,7 @@ <!-- Git Async Task --> <string name="running_dialog_text">Running command...</string> <string name="jgit_error_dialog_title">Internal exception occurred</string> - <string name="jgit_error_dialog_text">Message from jgit: /n</string> + <string name="jgit_error_dialog_text">Message from jgit: \n</string> <!-- Git Handler --> <string name="read_only_dialog_text">You are about to use a read-only repository, you will not be able to push to it</string> @@ -108,6 +108,7 @@ <string name="pref_password_dialog_title">Set the time you want the password to be in clipboard</string> <string name="pref_copy_title">Automatically Copy Password</string> <string name="pref_copy_dialog_title">Automatically copy the password to the clipboard after decryption was successful.</string> + <string name="ssh_key_success_dialog_title" translatable="false">SSH-key imported</string> <string name="ssh_key_error_dialog_title">Error while trying to import the ssh-key</string> <string name="ssh_key_error_dialog_text">Message : \n</string> <string name="pref_recursive_filter">Recursive filtering</string> |