Convert java files to kotlin (#570)
* Break SshKeyGen into multiple files * Use tinted material button * Convert PasswordStore to kotlin * Remove SshKeyGen * Remove explicit imports and other tweaks Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
This commit is contained in:
parent
5749c97d7c
commit
9acad2abf6
12 changed files with 1016 additions and 1192 deletions
|
@ -42,9 +42,6 @@
|
|||
<activity
|
||||
android:name=".UserPreference"
|
||||
android:parentActivityName=".PasswordStore" />
|
||||
|
||||
<activity android:name=".SshKeyGen" />
|
||||
|
||||
<service
|
||||
android:name=".autofill.AutofillService"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
|
@ -69,6 +66,7 @@
|
|||
android:name=".crypto.PgpActivity"
|
||||
android:parentActivityName=".PasswordStore" />
|
||||
<activity android:name=".SelectFolderActivity" />
|
||||
<activity android:name=".sshkeygen.SshKeyGenActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -1,920 +0,0 @@
|
|||
/*
|
||||
* Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.zeapo.pwdstore.crypto.PgpActivity;
|
||||
import com.zeapo.pwdstore.git.GitActivity;
|
||||
import com.zeapo.pwdstore.git.GitAsyncTask;
|
||||
import com.zeapo.pwdstore.git.GitOperation;
|
||||
import com.zeapo.pwdstore.ui.adapters.PasswordRecyclerAdapter;
|
||||
import com.zeapo.pwdstore.utils.PasswordItem;
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
|
||||
public class PasswordStore extends AppCompatActivity {
|
||||
|
||||
public static final int REQUEST_CODE_SIGN = 9910;
|
||||
public static final int REQUEST_CODE_ENCRYPT = 9911;
|
||||
public static final int REQUEST_CODE_SIGN_AND_ENCRYPT = 9912;
|
||||
public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913;
|
||||
public static final int REQUEST_CODE_GET_KEY = 9914;
|
||||
public static final int REQUEST_CODE_GET_KEY_IDS = 9915;
|
||||
public static final int REQUEST_CODE_EDIT = 9916;
|
||||
public static final int REQUEST_CODE_SELECT_FOLDER = 9917;
|
||||
private static final String TAG = PasswordStore.class.getName();
|
||||
private static final int CLONE_REPO_BUTTON = 401;
|
||||
private static final int NEW_REPO_BUTTON = 402;
|
||||
private static final int HOME = 403;
|
||||
private static final int REQUEST_EXTERNAL_STORAGE = 50;
|
||||
private SharedPreferences settings;
|
||||
private Activity activity;
|
||||
private PasswordFragment plist;
|
||||
private ShortcutManager shortcutManager;
|
||||
private MenuItem searchItem = null;
|
||||
private SearchView searchView;
|
||||
|
||||
private static boolean isPrintable(char c) {
|
||||
Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
|
||||
return (!Character.isISOControl(c))
|
||||
&& block != null
|
||||
&& block != Character.UnicodeBlock.SPECIALS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
// open search view on search key, or Ctr+F
|
||||
if ((keyCode == KeyEvent.KEYCODE_SEARCH
|
||||
|| keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed())
|
||||
&& !searchItem.isActionViewExpanded()) {
|
||||
searchItem.expandActionView();
|
||||
return true;
|
||||
}
|
||||
|
||||
// open search view on any printable character and query for it
|
||||
char c = (char) event.getUnicodeChar();
|
||||
boolean printable = isPrintable(c);
|
||||
if (printable && !searchItem.isActionViewExpanded()) {
|
||||
searchItem.expandActionView();
|
||||
searchView.setQuery(Character.toString(c), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
settings = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
shortcutManager = getSystemService(ShortcutManager.class);
|
||||
}
|
||||
activity = this;
|
||||
|
||||
// If user opens app with permission granted then revokes and returns,
|
||||
// prevent attempt to create password list fragment
|
||||
if (savedInstanceState != null
|
||||
&& (!settings.getBoolean("git_external", false)
|
||||
|| ContextCompat.checkSelfPermission(
|
||||
activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED)) {
|
||||
savedInstanceState = null;
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pwdstore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// do not attempt to checkLocalRepository() if no storage permission: immediate crash
|
||||
if (settings.getBoolean("git_external", false)) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
// TODO: strings.xml
|
||||
Snackbar snack =
|
||||
Snackbar.make(
|
||||
findViewById(R.id.main_layout),
|
||||
"The store is on the sdcard but the app does not have permission to access it. Please give permission.",
|
||||
Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(
|
||||
R.string.dialog_ok,
|
||||
view ->
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
new String[] {
|
||||
Manifest.permission
|
||||
.READ_EXTERNAL_STORAGE
|
||||
},
|
||||
REQUEST_EXTERNAL_STORAGE));
|
||||
snack.show();
|
||||
View view = snack.getView();
|
||||
AppCompatTextView tv =
|
||||
view.findViewById(com.google.android.material.R.id.snackbar_text);
|
||||
tv.setTextColor(Color.WHITE);
|
||||
tv.setMaxLines(10);
|
||||
} else {
|
||||
// No explanation needed, we can request the permission.
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
REQUEST_EXTERNAL_STORAGE);
|
||||
}
|
||||
} else {
|
||||
checkLocalRepository();
|
||||
}
|
||||
|
||||
} else {
|
||||
checkLocalRepository();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
// If request is cancelled, the result arrays are empty.
|
||||
if (requestCode == REQUEST_EXTERNAL_STORAGE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
checkLocalRepository();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.main_menu, menu);
|
||||
searchItem = menu.findItem(R.id.action_search);
|
||||
searchView = (SearchView) searchItem.getActionView();
|
||||
|
||||
searchView.setOnQueryTextListener(
|
||||
new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String s) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String s) {
|
||||
filterListAdapter(s);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// When using the support library, the setOnActionExpandListener() method is
|
||||
// static and accepts the MenuItem object as an argument
|
||||
searchItem.setOnActionExpandListener(
|
||||
new MenuItem.OnActionExpandListener() {
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||
refreshListAdapter();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(MenuItem item) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@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();
|
||||
Intent intent;
|
||||
|
||||
final MaterialAlertDialogBuilder initBefore =
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(this.getResources().getString(R.string.creation_dialog_text))
|
||||
.setPositiveButton(this.getResources().getString(R.string.dialog_ok), null);
|
||||
|
||||
switch (id) {
|
||||
case R.id.user_pref:
|
||||
try {
|
||||
intent = new Intent(this, UserPreference.class);
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
System.out.println("Exception caught :(");
|
||||
e.printStackTrace();
|
||||
}
|
||||
return true;
|
||||
case R.id.git_push:
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
initBefore.show();
|
||||
break;
|
||||
}
|
||||
|
||||
intent = new Intent(this, GitActivity.class);
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_PUSH);
|
||||
startActivityForResult(intent, GitActivity.REQUEST_PUSH);
|
||||
return true;
|
||||
|
||||
case R.id.git_pull:
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
initBefore.show();
|
||||
break;
|
||||
}
|
||||
|
||||
intent = new Intent(this, GitActivity.class);
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_PULL);
|
||||
startActivityForResult(intent, GitActivity.REQUEST_PULL);
|
||||
return true;
|
||||
|
||||
case R.id.git_sync:
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
initBefore.show();
|
||||
break;
|
||||
}
|
||||
|
||||
intent = new Intent(this, GitActivity.class);
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_SYNC);
|
||||
startActivityForResult(intent, GitActivity.REQUEST_SYNC);
|
||||
return true;
|
||||
|
||||
case R.id.refresh:
|
||||
updateListAdapter();
|
||||
return true;
|
||||
|
||||
case android.R.id.home:
|
||||
this.onBackPressed();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void openSettings(View view) {
|
||||
Intent intent;
|
||||
|
||||
try {
|
||||
intent = new Intent(this, UserPreference.class);
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
System.out.println("Exception caught :(");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void cloneExistingRepository(View view) {
|
||||
initRepository(CLONE_REPO_BUTTON);
|
||||
}
|
||||
|
||||
public void createNewRepository(View view) {
|
||||
initRepository(NEW_REPO_BUTTON);
|
||||
}
|
||||
|
||||
private void createRepository() {
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
PasswordRepository.initialize(this);
|
||||
}
|
||||
|
||||
final File localDir = PasswordRepository.getRepositoryDirectory(getApplicationContext());
|
||||
try {
|
||||
if (!localDir.mkdir()) throw new IllegalStateException("Failed to create directory!");
|
||||
PasswordRepository.createRepository(localDir);
|
||||
if (new File(localDir.getAbsolutePath() + "/.gpg-id").createNewFile()) {
|
||||
settings.edit().putBoolean("repository_initialized", true).apply();
|
||||
} else {
|
||||
throw new IllegalStateException("Failed to initialize repository state.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if (!localDir.delete()) {
|
||||
Log.d(TAG, "Failed to delete local repository");
|
||||
}
|
||||
return;
|
||||
}
|
||||
checkLocalRepository();
|
||||
}
|
||||
|
||||
private void initializeRepositoryInfo() {
|
||||
final String externalRepoPath = settings.getString("git_external_repo", null);
|
||||
if (settings.getBoolean("git_external", false) && externalRepoPath != null) {
|
||||
File dir = new File(externalRepoPath);
|
||||
|
||||
if (dir.exists()
|
||||
&& dir.isDirectory()
|
||||
&& !PasswordRepository.getPasswords(
|
||||
dir,
|
||||
PasswordRepository.getRepositoryDirectory(this),
|
||||
getSortOrder())
|
||||
.isEmpty()) {
|
||||
|
||||
PasswordRepository.closeRepository();
|
||||
checkLocalRepository();
|
||||
return; // if not empty, just show me the passwords!
|
||||
}
|
||||
}
|
||||
|
||||
final Set<String> keyIds = settings.getStringSet("openpgp_key_ids_set", new HashSet<>());
|
||||
|
||||
if (keyIds.isEmpty())
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(this.getResources().getString(R.string.key_dialog_text))
|
||||
.setPositiveButton(
|
||||
this.getResources().getString(R.string.dialog_positive),
|
||||
(dialogInterface, i) -> {
|
||||
Intent intent = new Intent(activity, UserPreference.class);
|
||||
startActivityForResult(intent, GitActivity.REQUEST_INIT);
|
||||
})
|
||||
.setNegativeButton(
|
||||
this.getResources().getString(R.string.dialog_negative), null)
|
||||
.show();
|
||||
|
||||
createRepository();
|
||||
}
|
||||
|
||||
private void checkLocalRepository() {
|
||||
Repository repo = PasswordRepository.initialize(this);
|
||||
if (repo == null) {
|
||||
Intent intent = new Intent(activity, UserPreference.class);
|
||||
intent.putExtra("operation", "git_external");
|
||||
startActivityForResult(intent, HOME);
|
||||
} else {
|
||||
checkLocalRepository(
|
||||
PasswordRepository.getRepositoryDirectory(getApplicationContext()));
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLocalRepository(File localDir) {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
if (localDir != null && settings.getBoolean("repository_initialized", false)) {
|
||||
Log.d(TAG, "Check, dir: " + localDir.getAbsolutePath());
|
||||
// do not push the fragment if we already have it
|
||||
if (fragmentManager.findFragmentByTag("PasswordsList") == null
|
||||
|| settings.getBoolean("repo_changed", false)) {
|
||||
settings.edit().putBoolean("repo_changed", false).apply();
|
||||
|
||||
plist = new PasswordFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(
|
||||
"Path",
|
||||
PasswordRepository.getRepositoryDirectory(getApplicationContext())
|
||||
.getAbsolutePath());
|
||||
|
||||
// if the activity was started from the autofill settings, the
|
||||
// intent is to match a clicked pwd with app. pass this to fragment
|
||||
if (getIntent().getBooleanExtra("matchWith", false)) {
|
||||
args.putBoolean("matchWith", true);
|
||||
}
|
||||
|
||||
plist.setArguments(args);
|
||||
|
||||
getSupportActionBar().show();
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
|
||||
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
|
||||
fragmentTransaction.replace(R.id.main_layout, plist, "PasswordsList");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
} else {
|
||||
getSupportActionBar().hide();
|
||||
|
||||
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
|
||||
ToCloneOrNot cloneFrag = new ToCloneOrNot();
|
||||
fragmentTransaction.replace(R.id.main_layout, cloneFrag, "ToCloneOrNot");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if ((null != plist) && plist.isNotEmpty()) {
|
||||
plist.popBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
if (null != plist && !plist.isNotEmpty()) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private String getRelativePath(String fullPath, String repositoryPath) {
|
||||
return fullPath.replace(repositoryPath, "").replaceAll("/+", "/");
|
||||
}
|
||||
|
||||
public long getLastChangedTimestamp(String fullPath) {
|
||||
File repoPath = PasswordRepository.getRepositoryDirectory(this);
|
||||
Repository repository = PasswordRepository.getRepository(repoPath);
|
||||
|
||||
if (repository == null) {
|
||||
Log.d(TAG, "getLastChangedTimestamp: No git repository");
|
||||
return new File(fullPath).lastModified();
|
||||
}
|
||||
|
||||
Git git = new Git(repository);
|
||||
String relativePath =
|
||||
getRelativePath(fullPath, repoPath.getAbsolutePath())
|
||||
.substring(1); // Removes leading '/'
|
||||
|
||||
Iterator<RevCommit> iterator;
|
||||
try {
|
||||
iterator = git.log().addPath(relativePath).call().iterator();
|
||||
} catch (GitAPIException e) {
|
||||
Log.e(TAG, "getLastChangedTimestamp: GITAPIException", e);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
Log.w(TAG, "getLastChangedTimestamp: No commits for file: " + relativePath);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return ((long) iterator.next().getCommitTime()) * 1000;
|
||||
}
|
||||
|
||||
public void decryptPassword(PasswordItem item) {
|
||||
Intent decryptIntent = new Intent(this, PgpActivity.class);
|
||||
Intent authDecryptIntent = new Intent(this, LaunchActivity.class);
|
||||
for (Intent intent : new Intent[] {decryptIntent, authDecryptIntent}) {
|
||||
intent.putExtra("NAME", item.toString());
|
||||
intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath());
|
||||
intent.putExtra(
|
||||
"REPO_PATH",
|
||||
PasswordRepository.getRepositoryDirectory(getApplicationContext())
|
||||
.getAbsolutePath());
|
||||
intent.putExtra(
|
||||
"LAST_CHANGED_TIMESTAMP",
|
||||
getLastChangedTimestamp(item.getFile().getAbsolutePath()));
|
||||
intent.putExtra("OPERATION", "DECRYPT");
|
||||
}
|
||||
|
||||
// Adds shortcut
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutInfo shortcut =
|
||||
new ShortcutInfo.Builder(this, item.getFullPathToParent())
|
||||
.setShortLabel(item.toString())
|
||||
.setLongLabel(item.getFullPathToParent() + item.toString())
|
||||
.setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher))
|
||||
.setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action
|
||||
.build();
|
||||
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
|
||||
|
||||
if (shortcuts.size() >= shortcutManager.getMaxShortcutCountPerActivity()
|
||||
&& shortcuts.size() > 0) {
|
||||
shortcuts.remove(shortcuts.size() - 1);
|
||||
shortcuts.add(0, shortcut);
|
||||
shortcutManager.setDynamicShortcuts(shortcuts);
|
||||
} else {
|
||||
shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcut));
|
||||
}
|
||||
}
|
||||
startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY);
|
||||
}
|
||||
|
||||
public void editPassword(PasswordItem item) {
|
||||
Intent intent = new Intent(this, PgpActivity.class);
|
||||
intent.putExtra("NAME", item.toString());
|
||||
intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath());
|
||||
intent.putExtra("PARENT_PATH", getCurrentDir().getAbsolutePath());
|
||||
intent.putExtra(
|
||||
"REPO_PATH",
|
||||
PasswordRepository.getRepositoryDirectory(getApplicationContext())
|
||||
.getAbsolutePath());
|
||||
intent.putExtra("OPERATION", "EDIT");
|
||||
startActivityForResult(intent, REQUEST_CODE_EDIT);
|
||||
}
|
||||
|
||||
public void createPassword() {
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(this.getResources().getString(R.string.creation_dialog_text))
|
||||
.setPositiveButton(
|
||||
this.getResources().getString(R.string.dialog_ok),
|
||||
(dialogInterface, i) -> {})
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.getStringSet("openpgp_key_ids_set", new HashSet<>()).isEmpty()) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(this.getResources().getString(R.string.no_key_selected_dialog_title))
|
||||
.setMessage(this.getResources().getString(R.string.no_key_selected_dialog_text))
|
||||
.setPositiveButton(
|
||||
this.getResources().getString(R.string.dialog_ok),
|
||||
(dialogInterface, i) -> {
|
||||
Intent intent = new Intent(activity, UserPreference.class);
|
||||
startActivity(intent);
|
||||
})
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
File currentDir = getCurrentDir();
|
||||
Log.i(TAG, "Adding file to : " + currentDir.getAbsolutePath());
|
||||
|
||||
Intent intent = new Intent(this, PgpActivity.class);
|
||||
intent.putExtra("FILE_PATH", getCurrentDir().getAbsolutePath());
|
||||
intent.putExtra(
|
||||
"REPO_PATH",
|
||||
PasswordRepository.getRepositoryDirectory(getApplicationContext())
|
||||
.getAbsolutePath());
|
||||
intent.putExtra("OPERATION", "ENCRYPT");
|
||||
startActivityForResult(intent, REQUEST_CODE_ENCRYPT);
|
||||
}
|
||||
|
||||
// deletes passwords in order from top to bottom
|
||||
public void deletePasswords(
|
||||
final PasswordRecyclerAdapter adapter, final Set<Integer> selectedItems) {
|
||||
final Iterator it = selectedItems.iterator();
|
||||
if (!it.hasNext()) {
|
||||
return;
|
||||
}
|
||||
final int position = (int) it.next();
|
||||
final PasswordItem item = adapter.getValues().get(position);
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(
|
||||
getResources().getString(R.string.delete_dialog_text, item.getLongName()))
|
||||
.setPositiveButton(
|
||||
getResources().getString(R.string.dialog_yes),
|
||||
(dialogInterface, i) -> {
|
||||
item.getFile().delete();
|
||||
adapter.remove(position);
|
||||
it.remove();
|
||||
adapter.updateSelectedItems(position, selectedItems);
|
||||
|
||||
commitChange(
|
||||
getResources()
|
||||
.getString(
|
||||
R.string.git_commit_remove_text,
|
||||
item.getLongName()));
|
||||
deletePasswords(adapter, selectedItems);
|
||||
})
|
||||
.setNegativeButton(
|
||||
this.getResources().getString(R.string.dialog_no),
|
||||
(dialogInterface, i) -> {
|
||||
it.remove();
|
||||
deletePasswords(adapter, selectedItems);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public void movePasswords(ArrayList<PasswordItem> values) {
|
||||
Intent intent = new Intent(this, SelectFolderActivity.class);
|
||||
ArrayList<String> fileLocations = new ArrayList<>();
|
||||
for (PasswordItem passwordItem : values) {
|
||||
fileLocations.add(passwordItem.getFile().getAbsolutePath());
|
||||
}
|
||||
intent.putExtra("Files", fileLocations);
|
||||
intent.putExtra("Operation", "SELECTFOLDER");
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_FOLDER);
|
||||
}
|
||||
|
||||
/** clears adapter's content and updates it with a fresh list of passwords from the root */
|
||||
public void updateListAdapter() {
|
||||
if ((null != plist)) {
|
||||
plist.updateAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the adapter with the current view of passwords */
|
||||
private void refreshListAdapter() {
|
||||
if ((null != plist)) {
|
||||
plist.refreshAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
private void filterListAdapter(String filter) {
|
||||
if ((null != plist)) {
|
||||
plist.filterAdapter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
private File getCurrentDir() {
|
||||
if ((null != plist)) {
|
||||
return plist.getCurrentDir();
|
||||
}
|
||||
return PasswordRepository.getRepositoryDirectory(getApplicationContext());
|
||||
}
|
||||
|
||||
private void commitChange(final String message) {
|
||||
new GitOperation(PasswordRepository.getRepositoryDirectory(activity), activity) {
|
||||
@Override
|
||||
public void execute() {
|
||||
Log.d(TAG, "Committing with message " + message);
|
||||
Git git = new Git(getRepository());
|
||||
GitAsyncTask tasks = new GitAsyncTask(activity, false, true, this);
|
||||
tasks.execute(
|
||||
git.add().addFilepattern("."),
|
||||
git.commit().setAll(true).setMessage(message));
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
switch (requestCode) {
|
||||
case GitActivity.REQUEST_CLONE:
|
||||
// if we get here with a RESULT_OK then it's probably OK :)
|
||||
settings.edit().putBoolean("repository_initialized", true).apply();
|
||||
break;
|
||||
case REQUEST_CODE_DECRYPT_AND_VERIFY:
|
||||
// if went from decrypt->edit and user saved changes or HOTP counter was
|
||||
// incremented, we need to commitChange
|
||||
if (data != null && data.getBooleanExtra("needCommit", false)) {
|
||||
if (data.getStringExtra("OPERATION").equals("EDIT")) {
|
||||
commitChange(
|
||||
this.getResources()
|
||||
.getString(
|
||||
R.string.git_commit_edit_text,
|
||||
data.getExtras().getString("LONG_NAME")));
|
||||
} else {
|
||||
commitChange(
|
||||
this.getResources()
|
||||
.getString(
|
||||
R.string.git_commit_increment_text,
|
||||
data.getExtras().getString("LONG_NAME")));
|
||||
}
|
||||
}
|
||||
refreshListAdapter();
|
||||
break;
|
||||
case REQUEST_CODE_ENCRYPT:
|
||||
commitChange(
|
||||
this.getResources()
|
||||
.getString(
|
||||
R.string.git_commit_add_text,
|
||||
data.getExtras().getString("LONG_NAME")));
|
||||
refreshListAdapter();
|
||||
break;
|
||||
case REQUEST_CODE_EDIT:
|
||||
commitChange(
|
||||
this.getResources()
|
||||
.getString(
|
||||
R.string.git_commit_edit_text,
|
||||
data.getExtras().getString("LONG_NAME")));
|
||||
refreshListAdapter();
|
||||
break;
|
||||
case GitActivity.REQUEST_INIT:
|
||||
case NEW_REPO_BUTTON:
|
||||
initializeRepositoryInfo();
|
||||
break;
|
||||
case GitActivity.REQUEST_SYNC:
|
||||
case GitActivity.REQUEST_PULL:
|
||||
updateListAdapter();
|
||||
break;
|
||||
case HOME:
|
||||
checkLocalRepository();
|
||||
break;
|
||||
case CLONE_REPO_BUTTON:
|
||||
// duplicate code
|
||||
if (settings.getBoolean("git_external", false)
|
||||
&& settings.getString("git_external_repo", null) != null) {
|
||||
String externalRepoPath = settings.getString("git_external_repo", null);
|
||||
File dir = externalRepoPath != null ? new File(externalRepoPath) : null;
|
||||
|
||||
if (dir != null
|
||||
&& dir.exists()
|
||||
&& dir.isDirectory()
|
||||
&& !FileUtils.listFiles(dir, null, true).isEmpty()
|
||||
&& !PasswordRepository.getPasswords(
|
||||
dir,
|
||||
PasswordRepository.getRepositoryDirectory(this),
|
||||
getSortOrder())
|
||||
.isEmpty()) {
|
||||
PasswordRepository.closeRepository();
|
||||
checkLocalRepository();
|
||||
return; // if not empty, just show me the passwords!
|
||||
}
|
||||
}
|
||||
Intent intent = new Intent(activity, GitActivity.class);
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_CLONE);
|
||||
startActivityForResult(intent, GitActivity.REQUEST_CLONE);
|
||||
break;
|
||||
case REQUEST_CODE_SELECT_FOLDER:
|
||||
Log.d(
|
||||
TAG,
|
||||
"Moving passwords to " + data.getStringExtra("SELECTED_FOLDER_PATH"));
|
||||
Log.d(TAG, TextUtils.join(", ", data.getStringArrayListExtra("Files")));
|
||||
File target = new File(data.getStringExtra("SELECTED_FOLDER_PATH"));
|
||||
if (!target.isDirectory()) {
|
||||
Log.e(TAG, "Tried moving passwords to a non-existing folder.");
|
||||
break;
|
||||
}
|
||||
|
||||
String repositoryPath =
|
||||
PasswordRepository.getRepositoryDirectory(getApplicationContext())
|
||||
.getAbsolutePath();
|
||||
|
||||
// TODO move this to an async task
|
||||
for (String fileString : data.getStringArrayListExtra("Files")) {
|
||||
File source = new File(fileString);
|
||||
if (!source.exists()) {
|
||||
Log.e(TAG, "Tried moving something that appears non-existent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
File destinationFile =
|
||||
new File(target.getAbsolutePath() + "/" + source.getName());
|
||||
|
||||
String basename = FilenameUtils.getBaseName(source.getAbsolutePath());
|
||||
|
||||
String sourceLongName =
|
||||
PgpActivity.getLongName(
|
||||
source.getParent(), repositoryPath, basename);
|
||||
|
||||
String destinationLongName =
|
||||
PgpActivity.getLongName(
|
||||
target.getAbsolutePath(), repositoryPath, basename);
|
||||
|
||||
if (destinationFile.exists()) {
|
||||
Log.e(TAG, "Trying to move a file that already exists.");
|
||||
// TODO: Add option to cancel overwrite. Will be easier once this is an
|
||||
// async task.
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(
|
||||
getResources()
|
||||
.getString(R.string.password_exists_title))
|
||||
.setMessage(
|
||||
getResources()
|
||||
.getString(
|
||||
R.string.password_exists_message,
|
||||
destinationLongName,
|
||||
sourceLongName))
|
||||
.setPositiveButton("Okay", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
if (!source.renameTo(destinationFile)) {
|
||||
// TODO this should show a warning to the user
|
||||
Log.e(TAG, "Something went wrong while moving.");
|
||||
} else {
|
||||
commitChange(
|
||||
this.getResources()
|
||||
.getString(
|
||||
R.string.git_commit_move_text,
|
||||
sourceLongName,
|
||||
destinationLongName));
|
||||
}
|
||||
}
|
||||
updateListAdapter();
|
||||
if (plist != null) {
|
||||
plist.dismissActionMode();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void initRepository(final int operation) {
|
||||
PasswordRepository.closeRepository();
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(this.getResources().getString(R.string.location_dialog_title))
|
||||
.setMessage(this.getResources().getString(R.string.location_dialog_text))
|
||||
.setPositiveButton(
|
||||
this.getResources().getString(R.string.location_hidden),
|
||||
(dialog, whichButton) -> {
|
||||
settings.edit().putBoolean("git_external", false).apply();
|
||||
|
||||
switch (operation) {
|
||||
case NEW_REPO_BUTTON:
|
||||
initializeRepositoryInfo();
|
||||
break;
|
||||
case CLONE_REPO_BUTTON:
|
||||
PasswordRepository.initialize(PasswordStore.this);
|
||||
|
||||
Intent intent = new Intent(activity, GitActivity.class);
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_CLONE);
|
||||
startActivityForResult(intent, GitActivity.REQUEST_CLONE);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.setNegativeButton(
|
||||
this.getResources().getString(R.string.location_sdcard),
|
||||
(dialog, whichButton) -> {
|
||||
settings.edit().putBoolean("git_external", true).apply();
|
||||
|
||||
String externalRepo = settings.getString("git_external_repo", null);
|
||||
|
||||
if (externalRepo == null) {
|
||||
Intent intent = new Intent(activity, UserPreference.class);
|
||||
intent.putExtra("operation", "git_external");
|
||||
startActivityForResult(intent, operation);
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(
|
||||
getResources()
|
||||
.getString(
|
||||
R.string.directory_selected_title))
|
||||
.setMessage(
|
||||
getResources()
|
||||
.getString(
|
||||
R.string.directory_selected_message,
|
||||
externalRepo))
|
||||
.setPositiveButton(
|
||||
getResources().getString(R.string.use),
|
||||
(dialog1, which) -> {
|
||||
switch (operation) {
|
||||
case NEW_REPO_BUTTON:
|
||||
initializeRepositoryInfo();
|
||||
break;
|
||||
case CLONE_REPO_BUTTON:
|
||||
PasswordRepository.initialize(
|
||||
PasswordStore.this);
|
||||
|
||||
Intent intent =
|
||||
new Intent(
|
||||
activity,
|
||||
GitActivity.class);
|
||||
intent.putExtra(
|
||||
"Operation",
|
||||
GitActivity.REQUEST_CLONE);
|
||||
startActivityForResult(
|
||||
intent,
|
||||
GitActivity.REQUEST_CLONE);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.setNegativeButton(
|
||||
getResources().getString(R.string.change),
|
||||
(dialog12, which) -> {
|
||||
Intent intent =
|
||||
new Intent(
|
||||
activity, UserPreference.class);
|
||||
intent.putExtra("operation", "git_external");
|
||||
startActivityForResult(intent, operation);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public void matchPasswordWithApp(PasswordItem item) {
|
||||
String path =
|
||||
item.getFile()
|
||||
.getAbsolutePath()
|
||||
.replace(
|
||||
PasswordRepository.getRepositoryDirectory(getApplicationContext())
|
||||
+ "/",
|
||||
"")
|
||||
.replace(".gpg", "");
|
||||
Intent data = new Intent();
|
||||
data.putExtra("path", path);
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
}
|
||||
|
||||
private PasswordRepository.PasswordSortOrder getSortOrder() {
|
||||
return PasswordRepository.PasswordSortOrder.getSortOrder(settings);
|
||||
}
|
||||
}
|
731
app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
Normal file
731
app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
Normal file
|
@ -0,0 +1,731 @@
|
|||
/*
|
||||
* Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ShortcutInfo.Builder
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MenuItem.OnActionExpandListener
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.zeapo.pwdstore.crypto.PgpActivity
|
||||
import com.zeapo.pwdstore.crypto.PgpActivity.Companion.getLongName
|
||||
import com.zeapo.pwdstore.git.GitActivity
|
||||
import com.zeapo.pwdstore.git.GitAsyncTask
|
||||
import com.zeapo.pwdstore.git.GitOperation
|
||||
import com.zeapo.pwdstore.ui.adapters.PasswordRecyclerAdapter
|
||||
import com.zeapo.pwdstore.utils.PasswordItem
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.closeRepository
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.createRepository
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getPasswords
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepository
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.initialize
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder
|
||||
import java.io.File
|
||||
import java.lang.Character.UnicodeBlock
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.errors.GitAPIException
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
class PasswordStore : AppCompatActivity() {
|
||||
|
||||
private lateinit var activity: PasswordStore
|
||||
private lateinit var searchItem: MenuItem
|
||||
private lateinit var searchView: SearchView
|
||||
private lateinit var settings: SharedPreferences
|
||||
private var plist: PasswordFragment? = null
|
||||
private var shortcutManager: ShortcutManager? = null
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
// open search view on search key, or Ctr+F
|
||||
if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) &&
|
||||
!searchItem.isActionViewExpanded) {
|
||||
searchItem.expandActionView()
|
||||
return true
|
||||
}
|
||||
|
||||
// open search view on any printable character and query for it
|
||||
val c = event.unicodeChar.toChar()
|
||||
val printable = isPrintable(c)
|
||||
if (printable && !searchItem.isActionViewExpanded) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(c.toString(), true)
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
activity = this
|
||||
settings = PreferenceManager.getDefaultSharedPreferences(this.applicationContext)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
shortcutManager = getSystemService(ShortcutManager::class.java)
|
||||
}
|
||||
|
||||
// If user opens app with permission granted then revokes and returns,
|
||||
// prevent attempt to create password list fragment
|
||||
var savedInstance = savedInstanceState
|
||||
if (savedInstanceState != null && (!settings.getBoolean("git_external", false) ||
|
||||
ContextCompat.checkSelfPermission(
|
||||
activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED)) {
|
||||
savedInstance = null
|
||||
}
|
||||
super.onCreate(savedInstance)
|
||||
setContentView(R.layout.activity_pwdstore)
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
// do not attempt to checkLocalRepository() if no storage permission: immediate crash
|
||||
if (settings.getBoolean("git_external", false)) {
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
val snack = Snackbar.make(
|
||||
findViewById(R.id.main_layout),
|
||||
getString(R.string.access_sdcard_text),
|
||||
Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.dialog_ok) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||
REQUEST_EXTERNAL_STORAGE)
|
||||
}
|
||||
snack.show()
|
||||
val view = snack.view
|
||||
val tv: AppCompatTextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(Color.WHITE)
|
||||
tv.maxLines = 10
|
||||
} else {
|
||||
// No explanation needed, we can request the permission.
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||
REQUEST_EXTERNAL_STORAGE)
|
||||
}
|
||||
} else {
|
||||
checkLocalRepository()
|
||||
}
|
||||
} else {
|
||||
checkLocalRepository()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
// If request is cancelled, the result arrays are empty.
|
||||
if (requestCode == REQUEST_EXTERNAL_STORAGE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
checkLocalRepository()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
menuInflater.inflate(R.menu.main_menu, menu)
|
||||
searchItem = menu.findItem(R.id.action_search)
|
||||
searchView = searchItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(
|
||||
object : OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(s: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(s: String): Boolean {
|
||||
filterListAdapter(s)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When using the support library, the setOnActionExpandListener() method is
|
||||
// static and accepts the MenuItem object as an argument
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : OnActionExpandListener {
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
refreshListAdapter()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
// 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.
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
val intent: Intent
|
||||
val initBefore = MaterialAlertDialogBuilder(this)
|
||||
.setMessage(this.resources.getString(R.string.creation_dialog_text))
|
||||
.setPositiveButton(this.resources.getString(R.string.dialog_ok), null)
|
||||
when (id) {
|
||||
R.id.user_pref -> {
|
||||
try {
|
||||
intent = Intent(this, UserPreference::class.java)
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.git_push -> {
|
||||
if (!isInitialized) {
|
||||
initBefore.show()
|
||||
return false
|
||||
}
|
||||
intent = Intent(this, GitActivity::class.java)
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_PUSH)
|
||||
startActivityForResult(intent, GitActivity.REQUEST_PUSH)
|
||||
return true
|
||||
}
|
||||
R.id.git_pull -> {
|
||||
if (!isInitialized) {
|
||||
initBefore.show()
|
||||
return false
|
||||
}
|
||||
intent = Intent(this, GitActivity::class.java)
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_PULL)
|
||||
startActivityForResult(intent, GitActivity.REQUEST_PULL)
|
||||
return true
|
||||
}
|
||||
R.id.git_sync -> {
|
||||
if (!isInitialized) {
|
||||
initBefore.show()
|
||||
return false
|
||||
}
|
||||
intent = Intent(this, GitActivity::class.java)
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_SYNC)
|
||||
startActivityForResult(intent, GitActivity.REQUEST_SYNC)
|
||||
return true
|
||||
}
|
||||
R.id.refresh -> {
|
||||
updateListAdapter()
|
||||
return true
|
||||
}
|
||||
android.R.id.home -> onBackPressed()
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun openSettings(view: View?) {
|
||||
val intent: Intent
|
||||
try {
|
||||
intent = Intent(this, UserPreference::class.java)
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun cloneExistingRepository(view: View?) {
|
||||
initRepository(CLONE_REPO_BUTTON)
|
||||
}
|
||||
|
||||
fun createNewRepository(view: View?) {
|
||||
initRepository(NEW_REPO_BUTTON)
|
||||
}
|
||||
|
||||
private fun createRepository() {
|
||||
if (!isInitialized) {
|
||||
initialize(this)
|
||||
}
|
||||
val localDir = getRepositoryDirectory(applicationContext)
|
||||
try {
|
||||
check(localDir.mkdir()) { "Failed to create directory!" }
|
||||
createRepository(localDir)
|
||||
if (File(localDir.absolutePath + "/.gpg-id").createNewFile()) {
|
||||
settings.edit().putBoolean("repository_initialized", true).apply()
|
||||
} else {
|
||||
throw IllegalStateException("Failed to initialize repository state.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
if (!localDir.delete()) {
|
||||
Log.d(TAG, "Failed to delete local repository")
|
||||
}
|
||||
return
|
||||
}
|
||||
checkLocalRepository()
|
||||
}
|
||||
|
||||
private fun initializeRepositoryInfo() {
|
||||
val externalRepoPath = settings.getString("git_external_repo", null)
|
||||
if (settings.getBoolean("git_external", false) && externalRepoPath != null) {
|
||||
val dir = File(externalRepoPath)
|
||||
if (dir.exists() && dir.isDirectory &&
|
||||
getPasswords(dir, getRepositoryDirectory(this), sortOrder).isNotEmpty()) {
|
||||
closeRepository()
|
||||
checkLocalRepository()
|
||||
return // if not empty, just show me the passwords!
|
||||
}
|
||||
}
|
||||
val keyIds = settings.getStringSet("openpgp_key_ids_set", HashSet())
|
||||
if (keyIds != null && keyIds.isEmpty()) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(this.resources.getString(R.string.key_dialog_text))
|
||||
.setPositiveButton(this.resources.getString(R.string.dialog_positive)) { _, _ ->
|
||||
val intent = Intent(activity, UserPreference::class.java)
|
||||
startActivityForResult(intent, GitActivity.REQUEST_INIT)
|
||||
}
|
||||
.setNegativeButton(this.resources.getString(R.string.dialog_negative), null)
|
||||
.show()
|
||||
}
|
||||
createRepository()
|
||||
}
|
||||
|
||||
private fun checkLocalRepository() {
|
||||
val repo = initialize(this)
|
||||
if (repo == null) {
|
||||
val intent = Intent(activity, UserPreference::class.java)
|
||||
intent.putExtra("operation", "git_external")
|
||||
startActivityForResult(intent, HOME)
|
||||
} else {
|
||||
checkLocalRepository(getRepositoryDirectory(applicationContext))
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkLocalRepository(localDir: File?) {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val fragmentTransaction = fragmentManager.beginTransaction()
|
||||
if (localDir != null && settings.getBoolean("repository_initialized", false)) {
|
||||
Log.d(TAG, "Check, dir: " + localDir.absolutePath)
|
||||
// do not push the fragment if we already have it
|
||||
if (fragmentManager.findFragmentByTag("PasswordsList") == null ||
|
||||
settings.getBoolean("repo_changed", false)) {
|
||||
settings.edit().putBoolean("repo_changed", false).apply()
|
||||
plist = PasswordFragment()
|
||||
val args = Bundle()
|
||||
args.putString("Path", getRepositoryDirectory(applicationContext).absolutePath)
|
||||
|
||||
// if the activity was started from the autofill settings, the
|
||||
// intent is to match a clicked pwd with app. pass this to fragment
|
||||
if (intent.getBooleanExtra("matchWith", false)) {
|
||||
args.putBoolean("matchWith", true)
|
||||
}
|
||||
plist!!.arguments = args
|
||||
supportActionBar!!.show()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(false)
|
||||
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
fragmentTransaction.replace(R.id.main_layout, plist!!, "PasswordsList")
|
||||
fragmentTransaction.commit()
|
||||
}
|
||||
} else {
|
||||
supportActionBar!!.hide()
|
||||
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
val cloneFrag = ToCloneOrNot()
|
||||
fragmentTransaction.replace(R.id.main_layout, cloneFrag, "ToCloneOrNot")
|
||||
fragmentTransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (null != plist && plist!!.isNotEmpty) {
|
||||
plist!!.popBack()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
if (null != plist && !plist!!.isNotEmpty) {
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRelativePath(fullPath: String, repositoryPath: String): String {
|
||||
return fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
|
||||
}
|
||||
|
||||
private fun getLastChangedTimestamp(fullPath: String): Long {
|
||||
val repoPath = getRepositoryDirectory(this)
|
||||
val repository = getRepository(repoPath)
|
||||
if (repository == null) {
|
||||
Log.d(TAG, "getLastChangedTimestamp: No git repository")
|
||||
return File(fullPath).lastModified()
|
||||
}
|
||||
val git = Git(repository)
|
||||
val relativePath = getRelativePath(fullPath, repoPath.absolutePath).substring(1) // Removes leading '/'
|
||||
val iterator: Iterator<RevCommit>
|
||||
iterator = try {
|
||||
git.log().addPath(relativePath).call().iterator()
|
||||
} catch (e: GitAPIException) {
|
||||
Log.e(TAG, "getLastChangedTimestamp: GITAPIException", e)
|
||||
return -1
|
||||
}
|
||||
if (!iterator.hasNext()) {
|
||||
Log.w(TAG, "getLastChangedTimestamp: No commits for file: $relativePath")
|
||||
return -1
|
||||
}
|
||||
return iterator.next().commitTime.toLong() * 1000
|
||||
}
|
||||
|
||||
fun decryptPassword(item: PasswordItem) {
|
||||
val decryptIntent = Intent(this, PgpActivity::class.java)
|
||||
val authDecryptIntent = Intent(this, LaunchActivity::class.java)
|
||||
for (intent in arrayOf(decryptIntent, authDecryptIntent)) {
|
||||
intent.putExtra("NAME", item.toString())
|
||||
intent.putExtra("FILE_PATH", item.file.absolutePath)
|
||||
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
|
||||
intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.file.absolutePath))
|
||||
intent.putExtra("OPERATION", "DECRYPT")
|
||||
}
|
||||
|
||||
// Adds shortcut
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
val shortcut = Builder(this, item.fullPathToParent)
|
||||
.setShortLabel(item.toString())
|
||||
.setLongLabel(item.fullPathToParent + item.toString())
|
||||
.setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher))
|
||||
.setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action
|
||||
.build()
|
||||
val shortcuts = shortcutManager!!.dynamicShortcuts
|
||||
if (shortcuts.size >= shortcutManager!!.maxShortcutCountPerActivity && shortcuts.size > 0) {
|
||||
shortcuts.removeAt(shortcuts.size - 1)
|
||||
shortcuts.add(0, shortcut)
|
||||
shortcutManager!!.dynamicShortcuts = shortcuts
|
||||
} else {
|
||||
shortcutManager!!.addDynamicShortcuts(listOf(shortcut))
|
||||
}
|
||||
}
|
||||
startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY)
|
||||
}
|
||||
|
||||
fun editPassword(item: PasswordItem) {
|
||||
val intent = Intent(this, PgpActivity::class.java)
|
||||
intent.putExtra("NAME", item.toString())
|
||||
intent.putExtra("FILE_PATH", item.file.absolutePath)
|
||||
intent.putExtra("PARENT_PATH", currentDir!!.absolutePath)
|
||||
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
|
||||
intent.putExtra("OPERATION", "EDIT")
|
||||
startActivityForResult(intent, REQUEST_CODE_EDIT)
|
||||
}
|
||||
|
||||
fun createPassword() {
|
||||
if (!isInitialized) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(this.resources.getString(R.string.creation_dialog_text))
|
||||
.setPositiveButton(this.resources.getString(R.string.dialog_ok), null)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
if (settings.getStringSet("openpgp_key_ids_set", HashSet()).isNullOrEmpty()) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(this.resources.getString(R.string.no_key_selected_dialog_title))
|
||||
.setMessage(this.resources.getString(R.string.no_key_selected_dialog_text))
|
||||
.setPositiveButton(this.resources.getString(R.string.dialog_ok)) { _, _ ->
|
||||
val intent = Intent(activity, UserPreference::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
return
|
||||
}
|
||||
val currentDir = currentDir
|
||||
Log.i(TAG, "Adding file to : " + currentDir!!.absolutePath)
|
||||
val intent = Intent(this, PgpActivity::class.java)
|
||||
intent.putExtra("FILE_PATH", currentDir.absolutePath)
|
||||
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
|
||||
intent.putExtra("OPERATION", "ENCRYPT")
|
||||
startActivityForResult(intent, REQUEST_CODE_ENCRYPT)
|
||||
}
|
||||
|
||||
// deletes passwords in order from top to bottom
|
||||
fun deletePasswords(adapter: PasswordRecyclerAdapter, selectedItems: MutableSet<Int>) {
|
||||
val it: MutableIterator<*> = selectedItems.iterator()
|
||||
if (!it.hasNext()) {
|
||||
return
|
||||
}
|
||||
val position = it.next() as Int
|
||||
val item = adapter.values[position]
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(resources.getString(R.string.delete_dialog_text, item.longName))
|
||||
.setPositiveButton(resources.getString(R.string.dialog_yes)) { _, _ ->
|
||||
item.file.delete()
|
||||
adapter.remove(position)
|
||||
it.remove()
|
||||
adapter.updateSelectedItems(position, selectedItems)
|
||||
commitChange(resources.getString(R.string.git_commit_remove_text, item.longName))
|
||||
deletePasswords(adapter, selectedItems)
|
||||
}
|
||||
.setNegativeButton(this.resources.getString(R.string.dialog_no)) { _, _ ->
|
||||
it.remove()
|
||||
deletePasswords(adapter, selectedItems)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun movePasswords(values: ArrayList<PasswordItem>) {
|
||||
val intent = Intent(this, SelectFolderActivity::class.java)
|
||||
val fileLocations = ArrayList<String>()
|
||||
for ((_, _, _, file) in values) {
|
||||
fileLocations.add(file.absolutePath)
|
||||
}
|
||||
intent.putExtra("Files", fileLocations)
|
||||
intent.putExtra("Operation", "SELECTFOLDER")
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_FOLDER)
|
||||
}
|
||||
|
||||
/** clears adapter's content and updates it with a fresh list of passwords from the root */
|
||||
fun updateListAdapter() {
|
||||
plist?.updateAdapter()
|
||||
}
|
||||
|
||||
/** Updates the adapter with the current view of passwords */
|
||||
private fun refreshListAdapter() {
|
||||
plist?.refreshAdapter()
|
||||
}
|
||||
|
||||
private fun filterListAdapter(filter: String) {
|
||||
plist?.filterAdapter(filter)
|
||||
}
|
||||
|
||||
private val currentDir: File?
|
||||
get() = plist?.currentDir ?: getRepositoryDirectory(applicationContext)
|
||||
|
||||
private fun commitChange(message: String) {
|
||||
object : GitOperation(getRepositoryDirectory(activity), activity) {
|
||||
override fun execute() {
|
||||
Log.d(TAG, "Committing with message $message")
|
||||
val git = Git(repository)
|
||||
val tasks = GitAsyncTask(activity, false, true, this)
|
||||
tasks.execute(git.add().addFilepattern("."), git.commit().setAll(true).setMessage(message))
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
when (requestCode) {
|
||||
// if we get here with a RESULT_OK then it's probably OK :)
|
||||
GitActivity.REQUEST_CLONE -> settings.edit().putBoolean("repository_initialized", true).apply()
|
||||
// if went from decrypt->edit and user saved changes or HOTP counter was
|
||||
// incremented, we need to commitChange
|
||||
REQUEST_CODE_DECRYPT_AND_VERIFY -> {
|
||||
if (data != null && data.getBooleanExtra("needCommit", false)) {
|
||||
if (data.getStringExtra("OPERATION") == "EDIT") {
|
||||
commitChange(this.resources
|
||||
.getString(
|
||||
R.string.git_commit_edit_text,
|
||||
data.extras!!.getString("LONG_NAME")))
|
||||
} else {
|
||||
commitChange(this.resources
|
||||
.getString(
|
||||
R.string.git_commit_increment_text,
|
||||
data.extras!!.getString("LONG_NAME")))
|
||||
}
|
||||
}
|
||||
refreshListAdapter()
|
||||
}
|
||||
REQUEST_CODE_ENCRYPT -> {
|
||||
commitChange(this.resources
|
||||
.getString(
|
||||
R.string.git_commit_add_text,
|
||||
data!!.extras!!.getString("LONG_NAME")))
|
||||
refreshListAdapter()
|
||||
}
|
||||
REQUEST_CODE_EDIT -> {
|
||||
commitChange(
|
||||
this.resources
|
||||
.getString(
|
||||
R.string.git_commit_edit_text,
|
||||
data!!.extras!!.getString("LONG_NAME")))
|
||||
refreshListAdapter()
|
||||
}
|
||||
GitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo()
|
||||
GitActivity.REQUEST_SYNC, GitActivity.REQUEST_PULL -> updateListAdapter()
|
||||
HOME -> checkLocalRepository()
|
||||
// duplicate code
|
||||
CLONE_REPO_BUTTON -> {
|
||||
if (settings.getBoolean("git_external", false) &&
|
||||
settings.getString("git_external_repo", null) != null) {
|
||||
val externalRepoPath = settings.getString("git_external_repo", null)
|
||||
val dir = externalRepoPath?.let { File(it) }
|
||||
if (dir != null &&
|
||||
dir.exists() &&
|
||||
dir.isDirectory &&
|
||||
!FileUtils.listFiles(dir, null, true).isEmpty() &&
|
||||
getPasswords(dir, getRepositoryDirectory(this), sortOrder).isNotEmpty()) {
|
||||
closeRepository()
|
||||
checkLocalRepository()
|
||||
return // if not empty, just show me the passwords!
|
||||
}
|
||||
}
|
||||
val intent = Intent(activity, GitActivity::class.java)
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
|
||||
startActivityForResult(intent, GitActivity.REQUEST_CLONE)
|
||||
}
|
||||
REQUEST_CODE_SELECT_FOLDER -> {
|
||||
Log.d(TAG, "Moving passwords to " + data!!.getStringExtra("SELECTED_FOLDER_PATH"))
|
||||
Log.d(TAG, TextUtils.join(", ", requireNotNull(data.getStringArrayListExtra("Files"))))
|
||||
|
||||
val target = File(requireNotNull(data.getStringExtra("SELECTED_FOLDER_PATH")))
|
||||
val repositoryPath = getRepositoryDirectory(applicationContext).absolutePath
|
||||
if (!target.isDirectory) {
|
||||
Log.e(TAG, "Tried moving passwords to a non-existing folder.")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO move this to an async task
|
||||
for (fileString in requireNotNull(data.getStringArrayListExtra("Files"))) {
|
||||
val source = File(fileString)
|
||||
if (!source.exists()) {
|
||||
Log.e(TAG, "Tried moving something that appears non-existent.")
|
||||
continue
|
||||
}
|
||||
val destinationFile = File(target.absolutePath + "/" + source.name)
|
||||
val basename = FilenameUtils.getBaseName(source.absolutePath)
|
||||
val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename)
|
||||
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
|
||||
if (destinationFile.exists()) {
|
||||
Log.e(TAG, "Trying to move a file that already exists.")
|
||||
// TODO: Add option to cancel overwrite. Will be easier once this is an async task.
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(resources.getString(R.string.password_exists_title))
|
||||
.setMessage(resources
|
||||
.getString(
|
||||
R.string.password_exists_message,
|
||||
destinationLongName,
|
||||
sourceLongName))
|
||||
.setPositiveButton("Okay", null)
|
||||
.show()
|
||||
}
|
||||
if (!source.renameTo(destinationFile)) {
|
||||
// TODO this should show a warning to the user
|
||||
Log.e(TAG, "Something went wrong while moving.")
|
||||
} else {
|
||||
commitChange(this.resources
|
||||
.getString(
|
||||
R.string.git_commit_move_text,
|
||||
sourceLongName,
|
||||
destinationLongName))
|
||||
}
|
||||
}
|
||||
updateListAdapter()
|
||||
if (plist != null) {
|
||||
plist!!.dismissActionMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun initRepository(operation: Int) {
|
||||
closeRepository()
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(this.resources.getString(R.string.location_dialog_title))
|
||||
.setMessage(this.resources.getString(R.string.location_dialog_text))
|
||||
.setPositiveButton(this.resources.getString(R.string.location_hidden)) { _, _ ->
|
||||
settings.edit().putBoolean("git_external", false).apply()
|
||||
when (operation) {
|
||||
NEW_REPO_BUTTON -> initializeRepositoryInfo()
|
||||
CLONE_REPO_BUTTON -> {
|
||||
initialize(this@PasswordStore)
|
||||
val intent = Intent(activity, GitActivity::class.java)
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
|
||||
startActivityForResult(intent, GitActivity.REQUEST_CLONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(this.resources.getString(R.string.location_sdcard)) { _, _ ->
|
||||
settings.edit().putBoolean("git_external", true).apply()
|
||||
val externalRepo = settings.getString("git_external_repo", null)
|
||||
if (externalRepo == null) {
|
||||
val intent = Intent(activity, UserPreference::class.java)
|
||||
intent.putExtra("operation", "git_external")
|
||||
startActivityForResult(intent, operation)
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(resources.getString(R.string.directory_selected_title))
|
||||
.setMessage(resources.getString(R.string.directory_selected_message, externalRepo))
|
||||
.setPositiveButton(resources.getString(R.string.use)) { _, _ ->
|
||||
when (operation) {
|
||||
NEW_REPO_BUTTON -> initializeRepositoryInfo()
|
||||
CLONE_REPO_BUTTON -> {
|
||||
initialize(this@PasswordStore)
|
||||
val intent = Intent(activity, GitActivity::class.java)
|
||||
intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
|
||||
startActivityForResult(intent, GitActivity.REQUEST_CLONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(resources.getString(R.string.change)) { _, _ ->
|
||||
val intent = Intent(activity, UserPreference::class.java)
|
||||
intent.putExtra("operation", "git_external")
|
||||
startActivityForResult(intent, operation)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun matchPasswordWithApp(item: PasswordItem) {
|
||||
val path = item.file
|
||||
.absolutePath
|
||||
.replace(getRepositoryDirectory(applicationContext).toString() + "/", "")
|
||||
.replace(".gpg", "")
|
||||
val data = Intent()
|
||||
data.putExtra("path", path)
|
||||
setResult(Activity.RESULT_OK, data)
|
||||
finish()
|
||||
}
|
||||
|
||||
private val sortOrder: PasswordRepository.PasswordSortOrder
|
||||
get() = getSortOrder(settings)
|
||||
|
||||
companion object {
|
||||
const val REQUEST_CODE_SIGN = 9910
|
||||
const val REQUEST_CODE_ENCRYPT = 9911
|
||||
const val REQUEST_CODE_SIGN_AND_ENCRYPT = 9912
|
||||
const val REQUEST_CODE_DECRYPT_AND_VERIFY = 9913
|
||||
const val REQUEST_CODE_GET_KEY = 9914
|
||||
const val REQUEST_CODE_GET_KEY_IDS = 9915
|
||||
const val REQUEST_CODE_EDIT = 9916
|
||||
const val REQUEST_CODE_SELECT_FOLDER = 9917
|
||||
private val TAG = PasswordStore::class.java.name
|
||||
private const val CLONE_REPO_BUTTON = 401
|
||||
private const val NEW_REPO_BUTTON = 402
|
||||
private const val HOME = 403
|
||||
private const val REQUEST_EXTERNAL_STORAGE = 50
|
||||
private fun isPrintable(c: Char): Boolean {
|
||||
val block = UnicodeBlock.of(c)
|
||||
return (!Character.isISOControl(c) &&
|
||||
block != null && block !== UnicodeBlock.SPECIALS)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,264 +0,0 @@
|
|||
/*
|
||||
* Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.KeyPair;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
public class SshKeyGen extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
setTitle("Generate SSH Key");
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(android.R.id.content, new SshKeyGenFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
// The back arrow in the action bar should act the same as the back button.
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
// Invoked when 'Generate' button of SshKeyGenFragment clicked. Generates a
|
||||
// private and public key, then replaces the SshKeyGenFragment with a
|
||||
// ShowSshKeyFragment which displays the public key.
|
||||
public void generate(View view) {
|
||||
String length =
|
||||
Integer.toString((Integer) ((Spinner) findViewById(R.id.length)).getSelectedItem());
|
||||
String passphrase = ((EditText) findViewById(R.id.passphrase)).getText().toString();
|
||||
String comment = ((EditText) findViewById(R.id.comment)).getText().toString();
|
||||
new KeyGenerateTask(this).execute(length, passphrase, comment);
|
||||
|
||||
InputMethodManager imm =
|
||||
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
// SSH key generation UI
|
||||
public static class SshKeyGenFragment extends Fragment {
|
||||
public SshKeyGenFragment() {}
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View v = inflater.inflate(R.layout.fragment_ssh_keygen, container, false);
|
||||
Typeface monoTypeface =
|
||||
Typeface.createFromAsset(
|
||||
requireContext().getAssets(), "fonts/sourcecodepro.ttf");
|
||||
|
||||
Spinner spinner = v.findViewById(R.id.length);
|
||||
Integer[] lengths = new Integer[] {2048, 4096};
|
||||
ArrayAdapter<Integer> adapter =
|
||||
new ArrayAdapter<>(
|
||||
requireContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
lengths);
|
||||
spinner.setAdapter(adapter);
|
||||
|
||||
((TextInputEditText) v.findViewById(R.id.passphrase)).setTypeface(monoTypeface);
|
||||
|
||||
final CheckBox checkbox = v.findViewById(R.id.show_passphrase);
|
||||
checkbox.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
final TextInputEditText editText = v.findViewById(R.id.passphrase);
|
||||
final int selection = editText.getSelectionEnd();
|
||||
if (isChecked) {
|
||||
editText.setInputType(
|
||||
InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
|
||||
} else {
|
||||
editText.setInputType(
|
||||
InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
}
|
||||
editText.setSelection(selection);
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// Displays the generated public key .ssh_key.pub
|
||||
public static class ShowSshKeyFragment extends DialogFragment {
|
||||
public ShowSshKeyFragment() {}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = requireActivity();
|
||||
final MaterialAlertDialogBuilder builder =
|
||||
new MaterialAlertDialogBuilder(requireContext());
|
||||
LayoutInflater inflater = activity.getLayoutInflater();
|
||||
@SuppressLint("InflateParams")
|
||||
final View v = inflater.inflate(R.layout.fragment_show_ssh_key, null);
|
||||
builder.setView(v);
|
||||
|
||||
AppCompatTextView textView = v.findViewById(R.id.public_key);
|
||||
File file = new File(activity.getFilesDir() + "/.ssh_key.pub");
|
||||
try {
|
||||
textView.setText(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Exception caught :(");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
builder.setPositiveButton(
|
||||
getResources().getString(R.string.dialog_ok),
|
||||
(dialog, which) -> {
|
||||
if (activity instanceof SshKeyGen) activity.finish();
|
||||
});
|
||||
|
||||
builder.setNegativeButton(
|
||||
getResources().getString(R.string.dialog_cancel), (dialog, which) -> {});
|
||||
|
||||
builder.setNeutralButton(getResources().getString(R.string.ssh_keygen_copy), null);
|
||||
|
||||
final AlertDialog ad = builder.setTitle("Your public key").create();
|
||||
ad.setOnShowListener(
|
||||
dialog -> {
|
||||
Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
b.setOnClickListener(
|
||||
v1 -> {
|
||||
AppCompatTextView textView1 =
|
||||
getDialog().findViewById(R.id.public_key);
|
||||
ClipboardManager clipboard =
|
||||
(ClipboardManager)
|
||||
activity.getSystemService(
|
||||
Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip =
|
||||
ClipData.newPlainText(
|
||||
"public key", textView1.getText().toString());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
});
|
||||
});
|
||||
return ad;
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeyGenerateTask extends AsyncTask<String, Void, Exception> {
|
||||
private ProgressDialog pd;
|
||||
private WeakReference<SshKeyGen> weakReference;
|
||||
|
||||
private KeyGenerateTask(final SshKeyGen activity) {
|
||||
weakReference = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
pd = ProgressDialog.show(weakReference.get(), "", "Generating keys");
|
||||
}
|
||||
|
||||
protected Exception doInBackground(String... strings) {
|
||||
int length = Integer.parseInt(strings[0]);
|
||||
String passphrase = strings[1];
|
||||
String comment = strings[2];
|
||||
|
||||
JSch jsch = new JSch();
|
||||
try {
|
||||
KeyPair kp = KeyPair.genKeyPair(jsch, KeyPair.RSA, length);
|
||||
|
||||
File file = new File(weakReference.get().getFilesDir() + "/.ssh_key");
|
||||
FileOutputStream out = new FileOutputStream(file, false);
|
||||
if (passphrase.length() > 0) {
|
||||
kp.writePrivateKey(out, passphrase.getBytes());
|
||||
} else {
|
||||
kp.writePrivateKey(out);
|
||||
}
|
||||
|
||||
file = new File(weakReference.get().getFilesDir() + "/.ssh_key.pub");
|
||||
out = new FileOutputStream(file, false);
|
||||
kp.writePublicKey(out, comment);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
System.out.println("Exception caught :(");
|
||||
e.printStackTrace();
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Exception e) {
|
||||
super.onPostExecute(e);
|
||||
pd.dismiss();
|
||||
if (e == null) {
|
||||
Toast.makeText(weakReference.get(), "SSH-key generated", Toast.LENGTH_LONG).show();
|
||||
DialogFragment df = new ShowSshKeyFragment();
|
||||
df.show(weakReference.get().getSupportFragmentManager(), "public_key");
|
||||
SharedPreferences prefs =
|
||||
PreferenceManager.getDefaultSharedPreferences(weakReference.get());
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean("use_generated_key", true);
|
||||
editor.apply();
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(weakReference.get())
|
||||
.setTitle("Error while trying to generate the ssh-key")
|
||||
.setMessage(
|
||||
weakReference
|
||||
.get()
|
||||
.getResources()
|
||||
.getString(R.string.ssh_key_error_dialog_text)
|
||||
+ e.getMessage())
|
||||
.setPositiveButton(
|
||||
weakReference.get().getResources().getString(R.string.dialog_ok),
|
||||
(dialogInterface, i) -> {
|
||||
// pass
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
|
||||
import com.zeapo.pwdstore.crypto.PgpActivity
|
||||
import com.zeapo.pwdstore.git.GitActivity
|
||||
import com.zeapo.pwdstore.sshkeygen.ShowSshKeyFragment
|
||||
import com.zeapo.pwdstore.sshkeygen.SshKeyGenActivity
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||
import com.zeapo.pwdstore.utils.auth.AuthenticationResult
|
||||
import com.zeapo.pwdstore.utils.auth.Authenticator
|
||||
|
@ -141,7 +143,7 @@ class UserPreference : AppCompatActivity() {
|
|||
}
|
||||
|
||||
viewSshKeyPreference?.onPreferenceClickListener = ClickListener {
|
||||
val df = SshKeyGen.ShowSshKeyFragment()
|
||||
val df = ShowSshKeyFragment()
|
||||
df.show(requireFragmentManager(), "public_key")
|
||||
true
|
||||
}
|
||||
|
@ -377,7 +379,7 @@ class UserPreference : AppCompatActivity() {
|
|||
* Opens a key generator to generate a public/private key pair
|
||||
*/
|
||||
fun makeSshKey(fromPreferences: Boolean) {
|
||||
val intent = Intent(applicationContext, SshKeyGen::class.java)
|
||||
val intent = Intent(applicationContext, SshKeyGenActivity::class.java)
|
||||
startActivity(intent)
|
||||
if (!fromPreferences) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.sshkeygen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.zeapo.pwdstore.R
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
class ShowSshKeyFragment : DialogFragment() {
|
||||
|
||||
private lateinit var activity: SshKeyGenActivity
|
||||
private lateinit var builder: MaterialAlertDialogBuilder
|
||||
private lateinit var publicKey: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activity = requireActivity() as SshKeyGenActivity
|
||||
builder = MaterialAlertDialogBuilder(activity)
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val view = activity.layoutInflater.inflate(R.layout.fragment_show_ssh_key, null)
|
||||
publicKey = view.findViewById(R.id.public_key)
|
||||
readKeyFromFile()
|
||||
createMaterialDialog(view)
|
||||
val ad = builder.create()
|
||||
ad.setOnShowListener {
|
||||
val b = ad.getButton(AlertDialog.BUTTON_NEUTRAL)
|
||||
b.setOnClickListener {
|
||||
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("public key", publicKey.text.toString())
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
return ad
|
||||
}
|
||||
|
||||
private fun createMaterialDialog(view: View) {
|
||||
builder.setView(view)
|
||||
builder.setTitle(getString(R.string.your_public_key))
|
||||
builder.setPositiveButton(getString(R.string.dialog_ok)) { _, _ -> activity.finish() }
|
||||
builder.setNegativeButton(getString(R.string.dialog_cancel), null)
|
||||
builder.setNeutralButton(resources.getString(R.string.ssh_keygen_copy), null)
|
||||
}
|
||||
|
||||
private fun readKeyFromFile() {
|
||||
val file = File(activity.filesDir.toString() + "/.ssh_key.pub")
|
||||
try {
|
||||
publicKey.text = FileUtils.readFileToString(file, StandardCharsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.sshkeygen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class SshKeyGenActivity : AppCompatActivity() {
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
title = "Generate SSH Key"
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(android.R.id.content, SshKeyGenFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// The back arrow in the action bar should act the same as the back button.
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.sshkeygen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.zeapo.pwdstore.R
|
||||
|
||||
class SshKeyGenFragment : Fragment() {
|
||||
|
||||
private lateinit var checkBox: CheckBox
|
||||
private lateinit var comment: EditText
|
||||
private lateinit var generate: Button
|
||||
private lateinit var passphrase: TextInputEditText
|
||||
private lateinit var spinner: Spinner
|
||||
private lateinit var activity: SshKeyGenActivity
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_ssh_keygen, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
activity = requireActivity() as SshKeyGenActivity
|
||||
findViews(view)
|
||||
val lengths = arrayOf(2048, 4096)
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, lengths)
|
||||
spinner.adapter = adapter
|
||||
generate.setOnClickListener { generate() }
|
||||
checkBox.setOnCheckedChangeListener { _, isChecked: Boolean ->
|
||||
val selection = passphrase.selectionEnd
|
||||
if (isChecked) {
|
||||
passphrase.inputType = (
|
||||
InputType.TYPE_CLASS_TEXT
|
||||
or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)
|
||||
} else {
|
||||
passphrase.inputType = (
|
||||
InputType.TYPE_CLASS_TEXT
|
||||
or InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
||||
}
|
||||
passphrase.setSelection(selection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findViews(view: View) {
|
||||
checkBox = view.findViewById(R.id.show_passphrase)
|
||||
comment = view.findViewById(R.id.comment)
|
||||
generate = view.findViewById(R.id.generate)
|
||||
passphrase = view.findViewById(R.id.passphrase)
|
||||
spinner = view.findViewById(R.id.length)
|
||||
}
|
||||
|
||||
// Invoked when 'Generate' button of SshKeyGenFragment clicked. Generates a
|
||||
// private and public key, then replaces the SshKeyGenFragment with a
|
||||
// ShowSshKeyFragment which displays the public key.
|
||||
fun generate() {
|
||||
val length = (spinner.selectedItem as Int).toString()
|
||||
val passphrase = passphrase.text.toString()
|
||||
val comment = comment.text.toString()
|
||||
KeyGenerateTask(activity).execute(length, passphrase, comment)
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
val imm = activity.getSystemService<InputMethodManager>()
|
||||
var view = activity.currentFocus
|
||||
if (view == null) {
|
||||
view = View(activity)
|
||||
}
|
||||
imm?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.sshkeygen
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.os.AsyncTask
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.jcraft.jsch.JSch
|
||||
import com.jcraft.jsch.KeyPair
|
||||
import com.zeapo.pwdstore.R
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class KeyGenerateTask(activity: AppCompatActivity) : AsyncTask<String?, Void?, Exception?>() {
|
||||
private var pd: ProgressDialog? = null
|
||||
private val weakReference = WeakReference(activity)
|
||||
override fun onPreExecute() {
|
||||
super.onPreExecute()
|
||||
pd = ProgressDialog.show(weakReference.get(), "", "Generating keys")
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg strings: String?): Exception? {
|
||||
val length = strings[0]?.toInt()
|
||||
val passphrase = strings[1]
|
||||
val comment = strings[2]
|
||||
val jsch = JSch()
|
||||
try {
|
||||
val kp = length?.let { KeyPair.genKeyPair(jsch, KeyPair.RSA, it) }
|
||||
var file = File(weakReference.get()!!.filesDir.toString() + "/.ssh_key")
|
||||
var out = FileOutputStream(file, false)
|
||||
if (passphrase?.isNotEmpty()!!) {
|
||||
kp?.writePrivateKey(out, passphrase.toByteArray())
|
||||
} else {
|
||||
kp?.writePrivateKey(out)
|
||||
}
|
||||
file = File(weakReference.get()!!.filesDir.toString() + "/.ssh_key.pub")
|
||||
out = FileOutputStream(file, false)
|
||||
kp?.writePublicKey(out, comment)
|
||||
return null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute(e: Exception?) {
|
||||
super.onPostExecute(e)
|
||||
val activity = weakReference.get()
|
||||
if (activity is AppCompatActivity) {
|
||||
pd!!.dismiss()
|
||||
if (e == null) {
|
||||
Toast.makeText(activity, "SSH-key generated", Toast.LENGTH_LONG).show()
|
||||
val df: DialogFragment = ShowSshKeyFragment()
|
||||
df.show(activity.supportFragmentManager, "public_key")
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(weakReference.get())
|
||||
val editor = prefs.edit()
|
||||
editor.putBoolean("use_generated_key", true)
|
||||
editor.apply()
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(weakReference.get())
|
||||
.setTitle(activity.getString(R.string.error_generate_ssh_key))
|
||||
.setMessage(activity.getString(R.string.ssh_key_error_dialog_text) + e.message)
|
||||
.setPositiveButton(activity.getString(R.string.dialog_ok), null)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
// TODO: When activity is destroyed
|
||||
}
|
||||
}
|
||||
}
|
BIN
app/src/main/res/font/sourcecodepro.ttf
Normal file
BIN
app/src/main/res/font/sourcecodepro.ttf
Normal file
Binary file not shown.
|
@ -36,6 +36,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:fontFamily="@font/sourcecodepro"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
@ -62,12 +63,14 @@
|
|||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/generate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:onClick="generate"
|
||||
android:text="@string/ssh_keygen_generate" />
|
||||
android:text="@string/ssh_keygen_generate"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTint="?attr/colorSecondary" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
|
|
@ -271,4 +271,7 @@
|
|||
<string name="biometric_auth_summary">When enabled, Password Store will prompt you for your fingerprint when launching the app</string>
|
||||
<string name="biometric_auth_summary_error">Fingerprint hardware not accessible or missing</string>
|
||||
<string name="ssh_openkeystore_clear_keyid">Clear remembered OpenKeystore SSH Key ID</string>
|
||||
<string name="access_sdcard_text">The store is on the sdcard but the app does not have permission to access it. Please give permission.</string>
|
||||
<string name="your_public_key">Your public key</string>
|
||||
<string name="error_generate_ssh_key">Error while trying to generate the ssh-key</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue