diff options
| author | Harsh Shandilya <msfjarvis@gmail.com> | 2019-10-01 22:14:28 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-10-01 22:14:28 +0530 |
| commit | 073346c157ec34b750d175c8543f346e6eae94de (patch) | |
| tree | a55d24ef43563f3a2d03d7938a1e96c1f5cd1caf /app/src/main/java/com/zeapo/pwdstore/git | |
| parent | c9dc4034f12b62f4470f92fb13ecd72871f4a5b7 (diff) | |
| download | HardenedPass-073346c157ec34b750d175c8543f346e6eae94de.tar.gz HardenedPass-073346c157ec34b750d175c8543f346e6eae94de.zip | |
Redesign UI and introduce dark theme (#519)
Caveats:
- The openpgp preference had to be removed because the open-intents developers are too lazy to update their libraries. Over the coming weeks I will be reimplementing a local solution for this instead.
- The autofill dialog is broken but I since it is being worked on in #410 already I'm not going to bother fixing it.
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git')
10 files changed, 752 insertions, 772 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt b/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt index 31c65704..8b23c38e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt @@ -1,7 +1,7 @@ package com.zeapo.pwdstore.git import android.app.Activity -import android.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand @@ -37,7 +37,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: Activity) : GitOperatio override fun execute() { val git = Git(repository) if (!git.repository.repositoryState.isRebasing) { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) .setMessage("The repository is not rebasing, no need to push to another branch") .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> @@ -59,7 +59,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: Activity) : GitOperatio } override fun onError(errorMessage: String) { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) .setMessage("Error occurred when checking out another branch operation $errorMessage") .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> @@ -68,7 +68,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: Activity) : GitOperatio } override fun onSuccess() { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) .setMessage("There was a conflict when trying to rebase. " + "Your local master branch was pushed to another branch named conflicting-master-....\n" + diff --git a/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt index bf407838..8489eb1d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt @@ -1,11 +1,10 @@ package com.zeapo.pwdstore.git import android.app.Activity -import android.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R import org.eclipse.jgit.api.CloneCommand import org.eclipse.jgit.api.Git - import java.io.File /** @@ -23,7 +22,10 @@ class CloneOperation(fileDir: File, callingActivity: Activity) : GitOperation(fi * @return the current object */ fun setCommand(uri: String): CloneOperation { - this.command = Git.cloneRepository().setCloneAllBranches(true).setDirectory(repository.workTree).setURI(uri) + this.command = Git.cloneRepository() + .setCloneAllBranches(true) + .setDirectory(repository?.workTree) + .setURI(uri) return this } @@ -53,14 +55,12 @@ class CloneOperation(fileDir: File, callingActivity: Activity) : GitOperation(fi } override fun execute() { - if (this.provider != null) { - (this.command as CloneCommand).setCredentialsProvider(this.provider) - } + (this.command as? CloneCommand)?.setCredentialsProvider(this.provider) GitAsyncTask(callingActivity, true, false, this).execute(this.command) } override fun onError(errorMessage: String) { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) .setMessage("Error occured during the clone operation, " + callingActivity.resources.getString(R.string.jgit_error_dialog_text) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java deleted file mode 100644 index 9ef05f60..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java +++ /dev/null @@ -1,726 +0,0 @@ -package com.zeapo.pwdstore.git; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Editable; -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.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; - -import com.zeapo.pwdstore.R; -import com.zeapo.pwdstore.UserPreference; -import com.zeapo.pwdstore.git.config.SshApiSessionFactory; -import com.zeapo.pwdstore.utils.PasswordRepository; - -import org.apache.commons.io.FileUtils; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.Repository; - -import java.io.File; -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class GitActivity extends AppCompatActivity { - public static final int REQUEST_PULL = 101; - public static final int REQUEST_PUSH = 102; - public static final int REQUEST_CLONE = 103; - public static final int REQUEST_INIT = 104; - public static final int EDIT_SERVER = 105; - public static final int REQUEST_SYNC = 106; - public static final int REQUEST_CREATE = 107; - public static final int EDIT_GIT_CONFIG = 108; - public static final int BREAK_OUT_OF_DETACHED = 109; - private static final String TAG = "GitAct"; - private static final String emailPattern = "^[^@]+@[^@]+$"; - private Activity activity; - private Context context; - private String protocol; - private String connectionMode; - private String hostname; - private SharedPreferences settings; - private SshApiSessionFactory.IdentityBuilder identityBuilder; - private SshApiSessionFactory.ApiIdentity identity; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - context = getApplicationContext(); - activity = this; - - settings = PreferenceManager.getDefaultSharedPreferences(this.context); - - protocol = settings.getString("git_remote_protocol", "ssh://"); - connectionMode = settings.getString("git_remote_auth", "ssh-key"); - int operationCode = getIntent().getExtras().getInt("Operation"); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - switch (operationCode) { - case REQUEST_CLONE: - case EDIT_SERVER: - setContentView(R.layout.activity_git_clone); - setTitle(R.string.title_activity_git_clone); - - final Spinner protcol_spinner = findViewById(R.id.clone_protocol); - final Spinner connection_mode_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); - connection_mode_adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - connection_mode_spinner.setAdapter(connection_mode_adapter); - connection_mode_spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { - String selection = ((Spinner) findViewById(R.id.connection_mode)).getSelectedItem().toString(); - connectionMode = selection; - settings.edit().putString("git_remote_auth", selection).apply(); - } - - @Override - public void onNothingSelected(AdapterView<?> adapterView) { - - } - }); - - // init the spinner for protocols - ArrayAdapter<CharSequence> protocol_adapter = ArrayAdapter.createFromResource(this, - R.array.clone_protocols, android.R.layout.simple_spinner_item); - protocol_adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - protcol_spinner.setAdapter(protocol_adapter); - protcol_spinner.setOnItemSelectedListener( - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { - 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.server_port)).setHint(R.string.default_ssh_port); - - // select ssh-key auth mode as default and enable the spinner in case it was disabled - connection_mode_spinner.setSelection(0); - connection_mode_spinner.setEnabled(true); - - // however, if we have some saved that, that's more important! - if (connectionMode.equalsIgnoreCase("ssh-key")) { - connection_mode_spinner.setSelection(0); - } else if (connectionMode.equalsIgnoreCase("OpenKeychain")) { - connection_mode_spinner.setSelection(2); - } else { - connection_mode_spinner.setSelection(1); - } - } else { - ((EditText) findViewById(R.id.clone_uri)).setHint("hostname/path"); - - ((EditText) findViewById(R.id.server_port)).setHint(R.string.default_https_port); - - // select user/pwd auth-mode and disable the spinner - connection_mode_spinner.setSelection(1); - connection_mode_spinner.setEnabled(false); - } - - updateURI(); - } - - @Override - public void onNothingSelected(AdapterView<?> adapterView) { - - } - } - ); - - if (protocol.equals("ssh://")) { - protcol_spinner.setSelection(0); - } else { - protcol_spinner.setSelection(1); - } - - // init the server information - final EditText server_url = findViewById(R.id.server_url); - final EditText server_port = findViewById(R.id.server_port); - final EditText server_path = findViewById(R.id.server_path); - final EditText server_user = findViewById(R.id.server_user); - final EditText server_uri = findViewById(R.id.clone_uri); - - server_url.setText(settings.getString("git_remote_server", "")); - server_port.setText(settings.getString("git_remote_port", "")); - server_user.setText(settings.getString("git_remote_username", "")); - server_path.setText(settings.getString("git_remote_location", "")); - - server_url.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - if (server_url.isFocused()) - updateURI(); - } - - @Override - public void afterTextChanged(Editable editable) { - } - }); - server_port.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - if (server_port.isFocused()) - updateURI(); - } - - @Override - public void afterTextChanged(Editable editable) { - } - }); - server_user.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - if (server_user.isFocused()) - updateURI(); - } - - @Override - public void afterTextChanged(Editable editable) { - } - }); - server_path.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - if (server_path.isFocused()) - updateURI(); - } - - @Override - public void afterTextChanged(Editable editable) { - } - }); - - server_uri.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - if (server_uri.isFocused()) - splitURI(); - } - - @Override - public void afterTextChanged(Editable editable) { - } - }); - - if (operationCode == EDIT_SERVER) { - findViewById(R.id.clone_button).setVisibility(View.INVISIBLE); - findViewById(R.id.save_button).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.clone_button).setVisibility(View.VISIBLE); - findViewById(R.id.save_button).setVisibility(View.INVISIBLE); - } - - updateURI(); - - break; - case EDIT_GIT_CONFIG: - setContentView(R.layout.activity_git_config); - setTitle(R.string.title_activity_git_config); - - showGitConfig(); - break; - case REQUEST_PULL: - syncRepository(REQUEST_PULL); - break; - - case REQUEST_PUSH: - syncRepository(REQUEST_PUSH); - break; - - case REQUEST_SYNC: - syncRepository(REQUEST_SYNC); - break; - } - - - } - - /** - * Fills in the server_uri field with the information coming from other fields - */ - private void updateURI() { - EditText uri = findViewById(R.id.clone_uri); - EditText server_url = findViewById(R.id.server_url); - EditText server_port = findViewById(R.id.server_port); - EditText server_path = findViewById(R.id.server_path); - EditText server_user = findViewById(R.id.server_user); - - if (uri != null) { - switch (protocol) { - case "ssh://": { - String hostname = - server_user.getText() - + "@" + - server_url.getText().toString().trim() - + ":"; - if (server_port.getText().toString().equals("22")) { - hostname += server_path.getText().toString(); - - findViewById(R.id.warn_url).setVisibility(View.GONE); - } else { - TextView warn_url = findViewById(R.id.warn_url); - if (!server_path.getText().toString().matches("/.*") && !server_port.getText().toString().isEmpty()) { - warn_url.setText(R.string.warn_malformed_url_port); - warn_url.setVisibility(View.VISIBLE); - } else { - warn_url.setVisibility(View.GONE); - } - hostname += server_port.getText().toString() + server_path.getText().toString(); - } - - if (!hostname.equals("@:")) uri.setText(hostname); - } - break; - case "https://": { - StringBuilder hostname = new StringBuilder(); - hostname.append(server_url.getText().toString().trim()); - - if (server_port.getText().toString().equals("443")) { - hostname.append(server_path.getText().toString()); - - findViewById(R.id.warn_url).setVisibility(View.GONE); - } else { - hostname.append("/"); - hostname.append(server_port.getText().toString()) - .append(server_path.getText().toString()); - } - - if (!hostname.toString().equals("@/")) uri.setText(hostname); - } - break; - default: - break; - } - - } - } - - /** - * Splits the information in server_uri into the other fields - */ - private void splitURI() { - EditText server_uri = findViewById(R.id.clone_uri); - EditText server_url = findViewById(R.id.server_url); - EditText server_port = findViewById(R.id.server_port); - EditText server_path = findViewById(R.id.server_path); - EditText server_user = findViewById(R.id.server_user); - - String uri = server_uri.getText().toString(); - Pattern pattern = Pattern.compile("(.+)@([\\w\\d.]+):([\\d]+)*(.*)"); - Matcher matcher = pattern.matcher(uri); - if (matcher.find()) { - int count = matcher.groupCount(); - if (count > 1) { - server_user.setText(matcher.group(1)); - server_url.setText(matcher.group(2)); - } - if (count == 4) { - server_port.setText(matcher.group(3)); - server_path.setText(matcher.group(4)); - - TextView warn_url = findViewById(R.id.warn_url); - if (!server_path.getText().toString().matches("/.*") && !server_port.getText().toString().isEmpty()) { - warn_url.setText(R.string.warn_malformed_url_port); - warn_url.setVisibility(View.VISIBLE); - } else { - warn_url.setVisibility(View.GONE); - } - } - } - } - - @Override - public void onResume() { - super.onResume(); - updateURI(); - } - - @Override - protected void onDestroy() { - // Do not leak the service connection - if (identityBuilder != null) { - identityBuilder.close(); - identityBuilder = null; - } - super.onDestroy(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.git_clone, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - if (id == R.id.user_pref) { - try { - Intent intent = new Intent(this, UserPreference.class); - startActivity(intent); - } catch (Exception e) { - System.out.println("Exception caught :("); - e.printStackTrace(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * Saves the configuration found in the form - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean saveConfiguration() { - // 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", ((EditText) findViewById(R.id.server_port)).getText().toString()); - editor.putString("git_remote_uri", ((EditText) findViewById(R.id.clone_uri)).getText().toString()); - - // 'save' hostname variable for use by addRemote() either here or later - // in syncRepository() - hostname = ((EditText) findViewById(R.id.clone_uri)).getText().toString(); - String port = ((EditText) findViewById(R.id.server_port)).getText().toString(); - // don't ask the user, take off the protocol that he puts in - hostname = hostname.replaceFirst("^.+://", ""); - ((TextView) findViewById(R.id.clone_uri)).setText(hostname); - - if (!protocol.equals("ssh://")) { - hostname = protocol + hostname; - } else { - // if the port is explicitly given, jgit requires the ssh:// - if (!port.isEmpty() && !port.equals("22")) - hostname = protocol + hostname; - - // did he forget the username? - if (!hostname.matches("^.+@.+")) { - new AlertDialog.Builder(this). - setMessage(activity.getResources().getString(R.string.forget_username_dialog_text)). - setPositiveButton(activity.getResources().getString(R.string.dialog_oops), null). - show(); - return false; - } - } - if (PasswordRepository.isInitialized() && settings.getBoolean("repository_initialized", false)) { - // don't just use the clone_uri text, need to use hostname which has - // had the proper protocol prepended - PasswordRepository.addRemote("origin", hostname, true); - } - - editor.apply(); - return true; - } - - /** - * Save the repository information to the shared preferences settings - */ - public void saveConfiguration(View view) { - if (!saveConfiguration()) - return; - finish(); - } - - private void showGitConfig() { - // init the server information - final EditText git_user_name = findViewById(R.id.git_user_name); - final EditText git_user_email = findViewById(R.id.git_user_email); - final Button abort = findViewById(R.id.git_abort_rebase); - - git_user_name.setText(settings.getString("git_config_user_name", "")); - git_user_email.setText(settings.getString("git_config_user_email", "")); - - // git status - Repository repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(activity.getApplicationContext())); - if (repo != null) { - final TextView git_commit_hash = findViewById(R.id.git_commit_hash); - try { - ObjectId objectId = repo.resolve(Constants.HEAD); - Ref ref = repo.getRef("refs/heads/master"); - String head = ref.getObjectId().equals(objectId) ? ref.getName() : "DETACHED"; - git_commit_hash.setText(String.format("%s (%s)", objectId.abbreviate(8).name(), head)); - - // enable the abort button only if we're rebasing - abort.setEnabled(repo.getRepositoryState().isRebasing()); - } catch (Exception e) { - // ignore - } - } - } - - private boolean saveGitConfigs() { - // remember the settings - SharedPreferences.Editor editor = settings.edit(); - - String email = ((EditText) findViewById(R.id.git_user_email)).getText().toString(); - editor.putString("git_config_user_email", email); - editor.putString("git_config_user_name", ((EditText) findViewById(R.id.git_user_name)).getText().toString()); - - if (!email.matches(emailPattern)) { - new AlertDialog.Builder(this). - setMessage(activity.getResources().getString(R.string.invalid_email_dialog_text)). - setPositiveButton(activity.getResources().getString(R.string.dialog_oops), null). - show(); - return false; - } - - editor.apply(); - return true; - } - - public void applyGitConfigs(View view) { - if (!saveGitConfigs()) - return; - - String git_user_name = settings.getString("git_config_user_name", ""); - String git_user_email = settings.getString("git_config_user_email", ""); - - PasswordRepository.setUserName(git_user_name); - PasswordRepository.setUserEmail(git_user_email); - - finish(); - } - - public void abortRebase(View view) { - launchGitOperation(BREAK_OUT_OF_DETACHED); - } - - /** - * Clones the repository, the directory exists, deletes it - */ - public void cloneRepository(View view) { - if (PasswordRepository.getRepository(null) == null) { - PasswordRepository.initialize(this); - } - File localDir = PasswordRepository.getRepositoryDirectory(context); - - if (!saveConfiguration()) - return; - - // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder - if (localDir.exists() && localDir.listFiles().length != 0 - && !(localDir.listFiles().length == 1 && localDir.listFiles()[0].getName().equals(".git"))) { - new AlertDialog.Builder(this). - setTitle(R.string.dialog_delete_title). - setMessage(getResources().getString(R.string.dialog_delete_msg) + " " + localDir.toString()). - setCancelable(false). - setPositiveButton(R.string.dialog_delete, - (dialog, id) -> { - try { - FileUtils.deleteDirectory(localDir); - launchGitOperation(REQUEST_CLONE); - } catch (IOException e) { - //TODO Handle the exception correctly if we are unable to delete the directory... - e.printStackTrace(); - new AlertDialog.Builder(GitActivity.this).setMessage(e.getMessage()).show(); - } - - dialog.cancel(); - } - ). - setNegativeButton(R.string.dialog_do_not_delete, - (dialog, id) -> dialog.cancel() - ). - show(); - } else { - try { - // Silently delete & replace the lone .git folder if it exists - if (localDir.exists() && localDir.listFiles().length == 1 && localDir.listFiles()[0].getName().equals(".git")) { - try { - FileUtils.deleteDirectory(localDir); - } catch (IOException e) { - e.printStackTrace(); - new AlertDialog.Builder(GitActivity.this).setMessage(e.getMessage()).show(); - } - } - } catch (Exception e) { - //This is what happens when jgit fails :( - //TODO Handle the diffent cases of exceptions - e.printStackTrace(); - new AlertDialog.Builder(this).setMessage(e.getMessage()).show(); - } - launchGitOperation(REQUEST_CLONE); - } - } - - /** - * 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()) - new AlertDialog.Builder(this) - .setMessage(activity.getResources().getString(R.string.set_information_dialog_text)) - .setPositiveButton(activity.getResources().getString(R.string.dialog_positive), (dialogInterface, i) -> { - Intent intent = new Intent(activity, UserPreference.class); - startActivityForResult(intent, REQUEST_PULL); - }) - .setNegativeButton(activity.getResources().getString(R.string.dialog_negative), (dialogInterface, i) -> { - // do nothing :( - setResult(RESULT_OK); - finish(); - }) - .show(); - - else { - // check that the remote origin is here, else add it - PasswordRepository.addRemote("origin", hostname, false); - launchGitOperation(operation); - } - } - - /** - * Attempt to launch the requested GIT operation. Depending on the configured auth, it may not - * be possible to launch the operation immediately. In that case, this function may launch an - * intermediate activity instead, which will gather necessary information and post it back via - * onActivityResult, which will then re-call this function. This may happen multiple times, - * until either an error is encountered or the operation is successfully launched. - * - * @param operation The type of GIT operation to launch - */ - protected void launchGitOperation(int operation) { - GitOperation op; - File localDir = PasswordRepository.getRepositoryDirectory(context); - - try { - - // Before launching the operation with OpenKeychain auth, we need to issue several requests - // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents, - // we just need to keep calling it until it returns a completed ApiIdentity. - if (connectionMode.equalsIgnoreCase("OpenKeychain") && identity == null) { - // Lazy initialization of the IdentityBuilder - if (identityBuilder == null) { - identityBuilder = new SshApiSessionFactory.IdentityBuilder(this); - } - - // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure - // that onActivityResult is called with operation again, which will re-invoke us here - identity = identityBuilder.tryBuild(operation); - if (identity == null) - return; - } - - switch (operation) { - case REQUEST_CLONE: - case GitOperation.GET_SSH_KEY_FROM_CLONE: - op = new CloneOperation(localDir, activity).setCommand(hostname); - break; - - case REQUEST_PULL: - op = new PullOperation(localDir, activity).setCommand(); - break; - - case REQUEST_PUSH: - op = new PushOperation(localDir, activity).setCommand(); - break; - - case REQUEST_SYNC: - op = new SyncOperation(localDir, activity).setCommands(); - break; - - case BREAK_OUT_OF_DETACHED: - op = new BreakOutOfDetached(localDir, activity).setCommands(); - break; - - case SshApiSessionFactory.POST_SIGNATURE: - return; - - default: - Log.e(TAG, "Operation not recognized : " + operation); - setResult(RESULT_CANCELED); - finish(); - return; - } - - op.executeAfterAuthentication(connectionMode, - settings.getString("git_remote_username", "git"), - new File(getFilesDir() + "/.ssh_key"), - identity); - } catch (Exception e) { - e.printStackTrace(); - new AlertDialog.Builder(this).setMessage(e.getMessage()).show(); - } - } - - protected void onActivityResult(int requestCode, int resultCode, - Intent data) { - - // In addition to the pre-operation-launch series of intents for OpenKeychain auth - // that will pass through here and back to launchGitOperation, there is one - // synchronous operation that happens /after/ the operation has been launched in the - // background thread - the actual signing of the SSH challenge. We pass through the - // completed signature to the ApiIdentity, which will be blocked in the other thread - // waiting for it. - if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null) - identity.postSignature(data); - - if (resultCode == RESULT_CANCELED) { - setResult(RESULT_CANCELED); - finish(); - } else if (resultCode == RESULT_OK) { - // If an operation has been re-queued via this mechanism, let the - // IdentityBuilder attempt to extract some updated state from the intent before - // trying to re-launch the operation. - if (identityBuilder != null) { - identityBuilder.consume(data); - } - launchGitOperation(requestCode); - } - super.onActivityResult(requestCode, resultCode, data); - } - -} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt new file mode 100644 index 00000000..11cc4c1b --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt @@ -0,0 +1,660 @@ +package com.zeapo.pwdstore.git + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.text.Editable +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.Spinner +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatTextView +import androidx.preference.PreferenceManager +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.git.config.SshApiSessionFactory +import com.zeapo.pwdstore.utils.PasswordRepository +import org.apache.commons.io.FileUtils +import org.eclipse.jgit.lib.Constants +import java.io.File +import java.io.IOException +import java.util.regex.Pattern + +open class GitActivity : AppCompatActivity() { + private lateinit var context: Context + private lateinit var settings: SharedPreferences + private lateinit var protocol: String + private lateinit var connectionMode: String + private lateinit var hostname: String + private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null + private var identity: SshApiSessionFactory.ApiIdentity? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + context = requireNotNull(this) + + settings = PreferenceManager.getDefaultSharedPreferences(this) + + protocol = settings.getString("git_remote_protocol", null) ?: "ssh://" + connectionMode = settings.getString("git_remote_auth", null) ?: "ssh-key" + hostname = settings.getString("git_remote_location", null) ?: "" + val operationCode = intent.extras!!.getInt("Operation") + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + when (operationCode) { + REQUEST_CLONE, EDIT_SERVER -> { + setContentView(R.layout.activity_git_clone) + setTitle(R.string.title_activity_git_clone) + + val protcolSpinner = findViewById<Spinner>(R.id.clone_protocol) + val connectionModeSpinner = findViewById<Spinner>(R.id.connection_mode) + + // init the spinner for connection modes + val connectionModeAdapter = ArrayAdapter.createFromResource(this, + R.array.connection_modes, android.R.layout.simple_spinner_item) + connectionModeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + connectionModeSpinner.adapter = connectionModeAdapter + connectionModeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) { + val selection = (findViewById<View>(R.id.connection_mode) as Spinner).selectedItem.toString() + connectionMode = selection + settings.edit().putString("git_remote_auth", selection).apply() + } + + override fun onNothingSelected(adapterView: AdapterView<*>) { + + } + } + + // init the spinner for protocols + val protocolAdapter = ArrayAdapter.createFromResource(this, + R.array.clone_protocols, android.R.layout.simple_spinner_item) + protocolAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + protcolSpinner.adapter = protocolAdapter + protcolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) { + protocol = (findViewById<View>(R.id.clone_protocol) as Spinner).selectedItem.toString() + if (protocol == "ssh://") { + + // select ssh-key auth mode as default and enable the spinner in case it was disabled + connectionModeSpinner.setSelection(0) + connectionModeSpinner.isEnabled = true + + // however, if we have some saved that, that's more important! + when { + connectionMode.equals("ssh-key", ignoreCase = true) -> connectionModeSpinner.setSelection(0) + connectionMode.equals("OpenKeychain", ignoreCase = true) -> connectionModeSpinner.setSelection(2) + else -> connectionModeSpinner.setSelection(1) + } + } else { + // select user/pwd auth-mode and disable the spinner + connectionModeSpinner.setSelection(1) + connectionModeSpinner.isEnabled = false + } + + updateURI() + } + + override fun onNothingSelected(adapterView: AdapterView<*>) { + + } + } + + if (protocol == "ssh://") { + protcolSpinner.setSelection(0) + } else { + protcolSpinner.setSelection(1) + } + + // init the server information + val serverUrl = findViewById<TextInputEditText>(R.id.server_url) + val serverPort = findViewById<TextInputEditText>(R.id.server_port) + val serverPath = findViewById<TextInputEditText>(R.id.server_path) + val serverUser = findViewById<TextInputEditText>(R.id.server_user) + val serverUri = findViewById<TextInputEditText>(R.id.clone_uri) + + serverUrl.setText(settings.getString("git_remote_server", "")) + serverPort.setText(settings.getString("git_remote_port", "")) + serverUser.setText(settings.getString("git_remote_username", "")) + serverPath.setText(settings.getString("git_remote_location", "")) + + serverUrl.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { + if (serverUrl.isFocused) + updateURI() + } + + override fun afterTextChanged(editable: Editable) {} + }) + serverPort.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { + if (serverPort.isFocused) + updateURI() + } + + override fun afterTextChanged(editable: Editable) {} + }) + serverUser.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { + if (serverUser.isFocused) + updateURI() + } + + override fun afterTextChanged(editable: Editable) {} + }) + serverPath.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { + if (serverPath.isFocused) + updateURI() + } + + override fun afterTextChanged(editable: Editable) {} + }) + + serverUri.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { + if (serverUri.isFocused) + splitURI() + } + + override fun afterTextChanged(editable: Editable) {} + }) + + if (operationCode == EDIT_SERVER) { + findViewById<View>(R.id.clone_button).visibility = View.INVISIBLE + findViewById<View>(R.id.save_button).visibility = View.VISIBLE + } else { + findViewById<View>(R.id.clone_button).visibility = View.VISIBLE + findViewById<View>(R.id.save_button).visibility = View.INVISIBLE + } + + updateURI() + } + EDIT_GIT_CONFIG -> { + setContentView(R.layout.activity_git_config) + setTitle(R.string.title_activity_git_config) + + showGitConfig() + } + REQUEST_PULL -> syncRepository(REQUEST_PULL) + + REQUEST_PUSH -> syncRepository(REQUEST_PUSH) + + REQUEST_SYNC -> syncRepository(REQUEST_SYNC) + } + } + + /** + * Fills in the server_uri field with the information coming from other fields + */ + private fun updateURI() { + val uri = findViewById<TextInputEditText>(R.id.clone_uri) + val serverUrl = findViewById<TextInputEditText>(R.id.server_url) + val serverPort = findViewById<TextInputEditText>(R.id.server_port) + val serverPath = findViewById<TextInputEditText>(R.id.server_path) + val serverUser = findViewById<TextInputEditText>(R.id.server_user) + + if (uri != null) { + when (protocol) { + "ssh://" -> { + var hostname = (serverUser.text.toString() + + "@" + + serverUrl.text.toString().trim { it <= ' ' } + + ":") + if (serverPort.text.toString() == "22") { + hostname += serverPath.text.toString() + + findViewById<View>(R.id.warn_url).visibility = View.GONE + } else { + val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url) + if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) { + warnUrl.setText(R.string.warn_malformed_url_port) + warnUrl.visibility = View.VISIBLE + } else { + warnUrl.visibility = View.GONE + } + hostname += serverPort.text.toString() + serverPath.text.toString() + } + + if (hostname != "@:") uri.setText(hostname) + } + "https://" -> { + val hostname = StringBuilder() + hostname.append(serverUrl.text.toString().trim { it <= ' ' }) + + if (serverPort.text.toString() == "443") { + hostname.append(serverPath.text.toString()) + + findViewById<View>(R.id.warn_url).visibility = View.GONE + } else { + hostname.append("/") + hostname.append(serverPort.text.toString()) + .append(serverPath.text.toString()) + } + + if (hostname.toString() != "@/") uri.setText(hostname) + } + else -> { + } + } + + } + } + + /** + * Splits the information in server_uri into the other fields + */ + private fun splitURI() { + val serverUri = findViewById<TextInputEditText>(R.id.clone_uri) + val serverUrl = findViewById<TextInputEditText>(R.id.server_url) + val serverPort = findViewById<TextInputEditText>(R.id.server_port) + val serverPath = findViewById<TextInputEditText>(R.id.server_path) + val serverUser = findViewById<TextInputEditText>(R.id.server_user) + + val uri = serverUri.text.toString() + val pattern = Pattern.compile("(.+)@([\\w\\d.]+):([\\d]+)*(.*)") + val matcher = pattern.matcher(uri) + if (matcher.find()) { + val count = matcher.groupCount() + if (count > 1) { + serverUser.setText(matcher.group(1)) + serverUrl.setText(matcher.group(2)) + } + if (count == 4) { + serverPort.setText(matcher.group(3)) + serverPath.setText(matcher.group(4)) + + val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url) + if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) { + warnUrl.setText(R.string.warn_malformed_url_port) + warnUrl.visibility = View.VISIBLE + } else { + warnUrl.visibility = View.GONE + } + } + } + } + + public override fun onResume() { + super.onResume() + updateURI() + } + + override fun onDestroy() { + // Do not leak the service connection + if (identityBuilder != null) { + identityBuilder!!.close() + identityBuilder = null + } + super.onDestroy() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.git_clone, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.user_pref -> try { + val intent = Intent(this, UserPreference::class.java) + startActivity(intent) + return true + } catch (e: Exception) { + println("Exception caught :(") + e.printStackTrace() + } + + android.R.id.home -> { + finish() + return true + } + } + return super.onOptionsItemSelected(item) + } + + /** + * Saves the configuration found in the form + */ + private fun saveConfiguration(): Boolean { + // remember the settings + val editor = settings.edit() + + editor.putString("git_remote_server", (findViewById<View>(R.id.server_url) as TextInputEditText).text.toString()) + editor.putString("git_remote_location", (findViewById<View>(R.id.server_path) as TextInputEditText).text.toString()) + editor.putString("git_remote_username", (findViewById<View>(R.id.server_user) as TextInputEditText).text.toString()) + editor.putString("git_remote_protocol", protocol) + editor.putString("git_remote_auth", connectionMode) + editor.putString("git_remote_port", (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString()) + editor.putString("git_remote_uri", (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString()) + + // 'save' hostname variable for use by addRemote() either here or later + // in syncRepository() + hostname = (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString() + val port = (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString() + // don't ask the user, take off the protocol that he puts in + hostname = hostname.replaceFirst("^.+://".toRegex(), "") + (findViewById<View>(R.id.clone_uri) as TextInputEditText).setText(hostname) + + if (protocol != "ssh://") { + hostname = protocol + hostname + } else { + // if the port is explicitly given, jgit requires the ssh:// + if (port.isNotEmpty() && port != "22") + hostname = protocol + hostname + + // did he forget the username? + if (!hostname.matches("^.+@.+".toRegex())) { + MaterialAlertDialogBuilder(this) + .setMessage(context.getString(R.string.forget_username_dialog_text)) + .setPositiveButton(context.getString(R.string.dialog_oops), null) + .show() + return false + } + } + if (PasswordRepository.isInitialized && settings.getBoolean("repository_initialized", false)) { + // don't just use the clone_uri text, need to use hostname which has + // had the proper protocol prepended + PasswordRepository.addRemote("origin", hostname, true) + } + + editor.apply() + return true + } + + /** + * Save the repository information to the shared preferences settings + */ + @Suppress("UNUSED_PARAMETER") + fun saveConfiguration(view: View) { + if (!saveConfiguration()) + return + finish() + } + + private fun showGitConfig() { + // init the server information + val username = findViewById<TextInputEditText>(R.id.git_user_name) + val email = findViewById<TextInputEditText>(R.id.git_user_email) + val abort = findViewById<MaterialButton>(R.id.git_abort_rebase) + + username.setText(settings.getString("git_config_user_name", "")) + email.setText(settings.getString("git_config_user_email", "")) + + // git status + val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(context)) + if (repo != null) { + val commitHash = findViewById<AppCompatTextView>(R.id.git_commit_hash) + try { + val objectId = repo.resolve(Constants.HEAD) + val ref = repo.getRef("refs/heads/master") + val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED" + commitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head) + + // enable the abort button only if we're rebasing + val isRebasing = repo.repositoryState.isRebasing + abort.isEnabled = isRebasing + abort.alpha = if (isRebasing) 1.0f else 0.5f + } catch (e: Exception) { + // ignore + } + + } + } + + private fun saveGitConfigs(): Boolean { + // remember the settings + val editor = settings.edit() + + val email = (findViewById<View>(R.id.git_user_email) as TextInputEditText).text!!.toString() + editor.putString("git_config_user_email", email) + editor.putString("git_config_user_name", (findViewById<View>(R.id.git_user_name) as TextInputEditText).text.toString()) + + if (!email.matches(emailPattern.toRegex())) { + MaterialAlertDialogBuilder(this) + .setMessage(context.getString(R.string.invalid_email_dialog_text)) + .setPositiveButton(context.getString(R.string.dialog_oops), null) + .show() + return false + } + + editor.apply() + return true + } + + @Suppress("UNUSED_PARAMETER") + fun applyGitConfigs(view: View) { + if (!saveGitConfigs()) + return + PasswordRepository.setUserName(settings.getString("git_config_user_name", null) ?: "") + PasswordRepository.setUserEmail(settings.getString("git_config_user_email", null) ?: "") + finish() + } + + @Suppress("UNUSED_PARAMETER") + fun abortRebase(view: View) { + launchGitOperation(BREAK_OUT_OF_DETACHED) + } + + @Suppress("UNUSED_PARAMETER") + fun resetToRemote(view: View) { + launchGitOperation(REQUEST_RESET) + } + + /** + * Clones the repository, the directory exists, deletes it + */ + @Suppress("UNUSED_PARAMETER") + fun cloneRepository(view: View) { + if (PasswordRepository.getRepository(null) == null) { + PasswordRepository.initialize(this) + } + val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context)) + + if (!saveConfiguration()) + return + + // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder + if (localDir.exists() && localDir.listFiles()!!.isNotEmpty() + && !(localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git")) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.dialog_delete_title) + .setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString()) + .setCancelable(false) + .setPositiveButton(R.string.dialog_delete + ) { dialog, _ -> + try { + FileUtils.deleteDirectory(localDir) + launchGitOperation(REQUEST_CLONE) + } catch (e: IOException) { + //TODO Handle the exception correctly if we are unable to delete the directory... + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + + dialog.cancel() + } + .setNegativeButton(R.string.dialog_do_not_delete + ) { dialog, _ -> dialog.cancel() } + .show() + } else { + try { + // Silently delete & replace the lone .git folder if it exists + if (localDir.exists() && localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git") { + try { + FileUtils.deleteDirectory(localDir) + } catch (e: IOException) { + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + + } + } catch (e: Exception) { + //This is what happens when jgit fails :( + //TODO Handle the diffent cases of exceptions + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + + launchGitOperation(REQUEST_CLONE) + } + } + + /** + * 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 fun syncRepository(operation: Int) { + if (settings.getString("git_remote_username", "")!!.isEmpty() || + settings.getString("git_remote_server", "")!!.isEmpty() || + settings.getString("git_remote_location", "")!!.isEmpty()) + MaterialAlertDialogBuilder(this) + .setMessage(context.getString(R.string.set_information_dialog_text)) + .setPositiveButton(context.getString(R.string.dialog_positive)) { _, _ -> + val intent = Intent(context, UserPreference::class.java) + startActivityForResult(intent, REQUEST_PULL) + } + .setNegativeButton(context.getString(R.string.dialog_negative)) { _, _ -> + // do nothing :( + setResult(AppCompatActivity.RESULT_OK) + finish() + } + .show() + else { + // check that the remote origin is here, else add it + PasswordRepository.addRemote("origin", hostname, false) + launchGitOperation(operation) + } + } + + /** + * Attempt to launch the requested GIT operation. Depending on the configured auth, it may not + * be possible to launch the operation immediately. In that case, this function may launch an + * intermediate activity instead, which will gather necessary information and post it back via + * onActivityResult, which will then re-call this function. This may happen multiple times, + * until either an error is encountered or the operation is successfully launched. + * + * @param operation The type of GIT operation to launch + */ + private fun launchGitOperation(operation: Int) { + val op: GitOperation + val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context)) + + try { + + // Before launching the operation with OpenKeychain auth, we need to issue several requests + // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents, + // we just need to keep calling it until it returns a completed ApiIdentity. + if (connectionMode.equals("OpenKeychain", ignoreCase = true) && identity == null) { + // Lazy initialization of the IdentityBuilder + if (identityBuilder == null) { + identityBuilder = SshApiSessionFactory.IdentityBuilder(this) + } + + // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure + // that onActivityResult is called with operation again, which will re-invoke us here + identity = identityBuilder!!.tryBuild(operation) + if (identity == null) + return + } + + when (operation) { + REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> op = CloneOperation(localDir, this).setCommand(hostname) + + REQUEST_PULL -> op = PullOperation(localDir, this).setCommand() + + REQUEST_PUSH -> op = PushOperation(localDir, this).setCommand() + + REQUEST_SYNC -> op = SyncOperation(localDir, this).setCommands() + + BREAK_OUT_OF_DETACHED -> op = BreakOutOfDetached(localDir, this).setCommands() + + REQUEST_RESET -> op = ResetToRemoteOperation(localDir, this).setCommands() + + SshApiSessionFactory.POST_SIGNATURE -> return + + else -> { + Log.e(TAG, "Operation not recognized : $operation") + setResult(AppCompatActivity.RESULT_CANCELED) + finish() + return + } + } + + op.executeAfterAuthentication(connectionMode, + settings.getString("git_remote_username", "git")!!, + File("$filesDir/.ssh_key"), + identity) + } catch (e: Exception) { + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, + data: Intent?) { + + // In addition to the pre-operation-launch series of intents for OpenKeychain auth + // that will pass through here and back to launchGitOperation, there is one + // synchronous operation that happens /after/ the operation has been launched in the + // background thread - the actual signing of the SSH challenge. We pass through the + // completed signature to the ApiIdentity, which will be blocked in the other thread + // waiting for it. + if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null) + identity!!.postSignature(data) + + if (resultCode == AppCompatActivity.RESULT_CANCELED) { + setResult(AppCompatActivity.RESULT_CANCELED) + finish() + } else if (resultCode == AppCompatActivity.RESULT_OK) { + // If an operation has been re-queued via this mechanism, let the + // IdentityBuilder attempt to extract some updated state from the intent before + // trying to re-launch the operation. + if (identityBuilder != null) { + identityBuilder!!.consume(data) + } + launchGitOperation(requestCode) + } + super.onActivityResult(requestCode, resultCode, data) + } + + companion object { + const val REQUEST_PULL = 101 + const val REQUEST_PUSH = 102 + const val REQUEST_CLONE = 103 + const val REQUEST_INIT = 104 + const val EDIT_SERVER = 105 + const val REQUEST_SYNC = 106 + @Suppress("Unused") + const val REQUEST_CREATE = 107 + const val EDIT_GIT_CONFIG = 108 + const val BREAK_OUT_OF_DETACHED = 109 + const val REQUEST_RESET = 110 + private const val TAG = "GitAct" + private const val emailPattern = "^[^@]+@[^@]+$" + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt index 5c945c30..4ab90e96 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt @@ -3,15 +3,14 @@ package com.zeapo.pwdstore.git import android.annotation.SuppressLint import android.app.Activity import android.content.Intent -import android.preference.PreferenceManager import android.text.InputType import android.view.LayoutInflater import android.view.View import android.widget.CheckBox import android.widget.EditText import android.widget.LinearLayout -import androidx.appcompat.app.AlertDialog - +import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jcraft.jsch.JSch import com.jcraft.jsch.JSchException import com.jcraft.jsch.KeyPair @@ -21,12 +20,10 @@ import com.zeapo.pwdstore.git.config.GitConfigSessionFactory import com.zeapo.pwdstore.git.config.SshApiSessionFactory import com.zeapo.pwdstore.git.config.SshConfigSessionFactory import com.zeapo.pwdstore.utils.PasswordRepository - import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider - import java.io.File /** @@ -37,7 +34,7 @@ import java.io.File */ abstract class GitOperation(fileDir: File, internal val callingActivity: Activity) { - protected val repository: Repository = PasswordRepository.getRepository(fileDir) + protected val repository: Repository? = PasswordRepository.getRepository(fileDir) internal var provider: UsernamePasswordCredentialsProvider? = null internal var command: GitCommand<*>? = null @@ -117,7 +114,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit showError: Boolean) { if (connectionMode.equals("ssh-key", ignoreCase = true)) { if (sshKey == null || !sshKey.exists()) { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text)) .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title)) .setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ -> @@ -170,7 +167,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit executeAfterAuthentication(connectionMode, username, sshKey, identity, true) } } else { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) .setMessage(callingActivity.resources.getString(R.string.passphrase_dialog_text)) .setView(dialogView) @@ -195,10 +192,11 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit setAuthentication(sshKey, username, "").execute() } } catch (e: JSchException) { - AlertDialog.Builder(callingActivity) + e.printStackTrace() + MaterialAlertDialogBuilder(callingActivity) .setTitle("Unable to open the ssh-key") .setMessage("Please check that it was imported.") - .setPositiveButton("Ok") { _, _ -> } + .setPositiveButton("Ok") { _, _ -> callingActivity.finish() } .show() } @@ -211,7 +209,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit password.width = LinearLayout.LayoutParams.MATCH_PARENT password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) .setMessage(callingActivity.resources.getString(R.string.password_dialog_text)) .setView(password) @@ -231,7 +229,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit * Action to execute on error */ open fun onError(errorMessage: String) { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) .setMessage(callingActivity.resources.getString(R.string.jgit_error_dialog_text) + errorMessage) .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> diff --git a/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt index fa6c5445..90c46878 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt @@ -1,11 +1,10 @@ package com.zeapo.pwdstore.git import android.app.Activity -import android.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.PullCommand - import java.io.File /** @@ -30,14 +29,12 @@ class PullOperation(fileDir: File, callingActivity: Activity) : GitOperation(fil } override fun execute() { - if (this.provider != null) { - (this.command as PullCommand).setCredentialsProvider(this.provider) - } + (this.command as? PullCommand)?.setCredentialsProvider(this.provider) GitAsyncTask(callingActivity, true, false, this).execute(this.command) } override fun onError(errorMessage: String) { - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) .setMessage("Error occured during the pull operation, " + callingActivity.resources.getString(R.string.jgit_error_dialog_text) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt index 9674e5b0..0015c4d1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt @@ -1,11 +1,10 @@ package com.zeapo.pwdstore.git import android.app.Activity -import android.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.PushCommand - import java.io.File /** @@ -30,15 +29,13 @@ class PushOperation(fileDir: File, callingActivity: Activity) : GitOperation(fil } override fun execute() { - if (this.provider != null) { - (this.command as PushCommand).setCredentialsProvider(this.provider) - } + (this.command as? PushCommand)?.setCredentialsProvider(this.provider) GitAsyncTask(callingActivity, true, false, this).execute(this.command) } override fun onError(errorMessage: String) { // TODO handle the "Nothing to push" case - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) .setMessage(callingActivity.getString(R.string.jgit_error_push_dialog_text) + errorMessage) .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt new file mode 100644 index 00000000..1bab0981 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt @@ -0,0 +1,52 @@ +package com.zeapo.pwdstore.git + +import android.app.Activity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.R +import org.eclipse.jgit.api.AddCommand +import org.eclipse.jgit.api.FetchCommand +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.ResetCommand +import java.io.File + +/** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ +class ResetToRemoteOperation(fileDir: File, callingActivity: Activity) : GitOperation(fileDir, callingActivity) { + private var addCommand: AddCommand? = null + private var fetchCommand: FetchCommand? = null + private var resetCommand: ResetCommand? = null + + /** + * Sets the command + * + * @return the current object + */ + fun setCommands(): ResetToRemoteOperation { + val git = Git(repository) + this.addCommand = git.add().addFilepattern(".") + this.fetchCommand = git.fetch().setRemote("origin") + this.resetCommand = git.reset().setRef("origin/master").setMode(ResetCommand.ResetType.HARD) + return this + } + + override fun execute() { + this.fetchCommand?.setCredentialsProvider(this.provider) + GitAsyncTask(callingActivity, true, false, this) + .execute(this.addCommand, this.fetchCommand, this.resetCommand) + } + + override fun onError(errorMessage: String) { + MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) + .setMessage("Error occured during the sync operation, " + + "\nPlease check the FAQ for possible reasons why this error might occur." + + callingActivity.resources.getString(R.string.jgit_error_dialog_text) + + errorMessage) + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> } + .show() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt index 8712a200..a0fd52b2 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt @@ -1,7 +1,7 @@ package com.zeapo.pwdstore.git import android.app.Activity -import android.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R import org.eclipse.jgit.api.AddCommand import org.eclipse.jgit.api.CommitCommand @@ -9,7 +9,6 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.PullCommand import org.eclipse.jgit.api.PushCommand import org.eclipse.jgit.api.StatusCommand - import java.io.File /** @@ -42,14 +41,15 @@ class SyncOperation(fileDir: File, callingActivity: Activity) : GitOperation(fil override fun execute() { if (this.provider != null) { - this.pullCommand!!.setCredentialsProvider(this.provider) - this.pushCommand!!.setCredentialsProvider(this.provider) + this.pullCommand?.setCredentialsProvider(this.provider) + this.pushCommand?.setCredentialsProvider(this.provider) } GitAsyncTask(callingActivity, true, false, this).execute(this.addCommand, this.statusCommand, this.commitCommand, this.pullCommand, this.pushCommand) } override fun onError(errorMessage: String) { - AlertDialog.Builder(callingActivity).setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) + MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) .setMessage("Error occured during the sync operation, " + "\nPlease check the FAQ for possible reasons why this error might occur." + callingActivity.resources.getString(R.string.jgit_error_dialog_text) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java index 085beadd..dbb5b7b1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java @@ -5,8 +5,9 @@ import android.app.PendingIntent; import android.content.Intent; import android.content.IntentSender; -import androidx.appcompat.app.AlertDialog; +import androidx.annotation.NonNull; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.jcraft.jsch.Identity; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; @@ -49,9 +50,9 @@ public class SshApiSessionFactory extends GitConfigSessionFactory { this.identity = identity; } + @NonNull @Override - protected JSch - getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { + protected JSch getJSch(@NonNull final OpenSshConfig.Host hc, @NonNull FS fs) throws JSchException { JSch jsch = super.getJSch(hc, fs); jsch.removeAllIdentity(); jsch.addIdentity(identity, null); @@ -59,7 +60,7 @@ public class SshApiSessionFactory extends GitConfigSessionFactory { } @Override - protected void configure(OpenSshConfig.Host hc, Session session) { + protected void configure(@NonNull OpenSshConfig.Host hc, Session session) { session.setConfig("StrictHostKeyChecking", "no"); session.setConfig("PreferredAuthentications", "publickey"); @@ -204,8 +205,9 @@ public class SshApiSessionFactory extends GitConfigSessionFactory { @Override public void onError() { - new AlertDialog.Builder(callingActivity).setMessage(callingActivity.getString( - R.string.openkeychain_ssh_api_connect_fail)).show(); + new MaterialAlertDialogBuilder(callingActivity) + .setMessage(callingActivity.getString( + R.string.openkeychain_ssh_api_connect_fail)).show(); } }); |
