From 073346c157ec34b750d175c8543f346e6eae94de Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 1 Oct 2019 22:14:28 +0530 Subject: [PATCH] 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. --- .gitignore | 3 + app/build.gradle.kts | 2 +- .../java/com/zeapo/pwdstore/DecryptTest.kt | 14 +- .../java/com/zeapo/pwdstore/EncryptTest.kt | 4 +- .../res/drawable/ic_launcher_foreground.xml | 37 +- app/src/main/AndroidManifest.xml | 11 +- .../java/com/zeapo/pwdstore/PasswordEntry.kt | 4 +- .../com/zeapo/pwdstore/PasswordFragment.java | 4 +- .../PasswordGeneratorDialogFragment.java | 10 +- .../com/zeapo/pwdstore/PasswordStore.java | 26 +- .../zeapo/pwdstore/SelectFolderActivity.kt | 2 +- .../zeapo/pwdstore/SelectFolderFragment.java | 8 +- .../java/com/zeapo/pwdstore/SshKeyGen.java | 50 +- .../java/com/zeapo/pwdstore/UserPreference.kt | 249 +++--- .../pwdstore/autofill/AutofillFragment.kt | 25 +- .../autofill/AutofillRecyclerAdapter.kt | 11 +- .../pwdstore/autofill/AutofillService.kt | 26 +- .../com/zeapo/pwdstore/crypto/PgpActivity.kt | 27 +- .../zeapo/pwdstore/git/BreakOutOfDetached.kt | 8 +- .../com/zeapo/pwdstore/git/CloneOperation.kt | 14 +- .../com/zeapo/pwdstore/git/GitActivity.java | 726 ------------------ .../com/zeapo/pwdstore/git/GitActivity.kt | 660 ++++++++++++++++ .../com/zeapo/pwdstore/git/GitOperation.kt | 22 +- .../com/zeapo/pwdstore/git/PullOperation.kt | 9 +- .../com/zeapo/pwdstore/git/PushOperation.kt | 9 +- .../pwdstore/git/ResetToRemoteOperation.kt | 52 ++ .../com/zeapo/pwdstore/git/SyncOperation.kt | 10 +- .../git/config/SshApiSessionFactory.java | 14 +- .../pwdstore/utils/EntryRecyclerAdapter.java | 155 ---- .../pwdstore/utils/EntryRecyclerAdapter.kt | 118 +++ .../com/zeapo/pwdstore/utils/Extensions.kt | 9 + .../pwdstore/utils/FolderRecyclerAdapter.java | 29 - .../pwdstore/utils/FolderRecyclerAdapter.kt | 20 + .../zeapo/pwdstore/utils/PasswordItem.java | 110 --- .../com/zeapo/pwdstore/utils/PasswordItem.kt | 81 ++ .../utils/PasswordRecyclerAdapter.java | 133 ---- .../pwdstore/utils/PasswordRecyclerAdapter.kt | 116 +++ .../pwdstore/utils/PasswordRepository.java | 284 ------- .../pwdstore/utils/PasswordRepository.kt | 282 +++++++ .../widget/MultiselectableLinearLayout.kt | 51 ++ app/src/main/res/color/text_color.xml | 5 - .../res/drawable/autofill_row_background.xml | 5 - app/src/main/res/drawable/bottom_line.xml | 4 +- app/src/main/res/drawable/divider.xml | 2 +- .../main/res/drawable/ic_action_secure.xml | 2 +- ...600_24dp.xml => ic_folder_tinted_24dp.xml} | 2 +- .../res/drawable/ic_launcher_foreground.xml | 9 +- .../res/drawable/password_row_background.xml | 12 + app/src/main/res/drawable/red_rectangle.xml | 2 +- .../main/res/layout/activity_git_clone.xml | 326 ++++---- .../main/res/layout/activity_git_config.xml | 208 +++-- app/src/main/res/layout/activity_pwdstore.xml | 4 +- .../main/res/layout/autofill_instructions.xml | 17 +- .../res/layout/autofill_recycler_view.xml | 4 +- .../main/res/layout/autofill_row_layout.xml | 36 +- app/src/main/res/layout/decrypt_layout.xml | 391 +++++----- app/src/main/res/layout/encrypt_layout.xml | 146 ++-- app/src/main/res/layout/fragment_autofill.xml | 16 +- app/src/main/res/layout/fragment_pwgen.xml | 7 +- .../main/res/layout/fragment_show_ssh_key.xml | 5 +- .../main/res/layout/fragment_ssh_keygen.xml | 18 +- .../res/layout/fragment_to_clone_or_not.xml | 14 +- .../main/res/layout/git_passphrase_layout.xml | 1 - .../res/layout/password_recycler_view.xml | 4 +- .../main/res/layout/password_row_layout.xml | 18 +- app/src/main/res/values-night/colors.xml | 18 + app/src/main/res/values-night/styles.xml | 7 + app/src/main/res/values/attrs.xml | 9 + app/src/main/res/values/bools.xml | 5 + app/src/main/res/values/colors.xml | 31 +- app/src/main/res/values/strings.xml | 14 +- app/src/main/res/values/styles.xml | 48 +- app/src/main/res/xml/preference.xml | 92 ++- 73 files changed, 2474 insertions(+), 2433 deletions(-) delete mode 100644 app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/widget/MultiselectableLinearLayout.kt delete mode 100644 app/src/main/res/color/text_color.xml delete mode 100644 app/src/main/res/drawable/autofill_row_background.xml rename app/src/main/res/drawable/{ic_folder_grey600_24dp.xml => ic_folder_tinted_24dp.xml} (89%) create mode 100644 app/src/main/res/drawable/password_row_background.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-night/styles.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/bools.xml diff --git a/.gitignore b/.gitignore index ab3a14ea..71d6cdf7 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,7 @@ project.properties .idea *.iml +# Visual Studio Code +.vscode/ + captures/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89add6f8..c1ea485e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,4 @@ -import org.gradle.api.JavaVersion.* +import org.gradle.api.JavaVersion.VERSION_1_8 import org.jetbrains.kotlin.config.KotlinCompilerVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt index f0c502d9..448edc00 100644 --- a/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt +++ b/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt @@ -5,13 +5,17 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.os.SystemClock -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.filters.LargeTest -import androidx.test.rule.ActivityTestRule -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.ActivityTestRule import com.zeapo.pwdstore.crypto.PgpActivity -import kotlinx.android.synthetic.main.decrypt_layout.* +import kotlinx.android.synthetic.main.decrypt_layout.crypto_extra_show +import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_category_decrypt +import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_file +import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_show +import kotlinx.android.synthetic.main.decrypt_layout.crypto_username_show import org.apache.commons.io.FileUtils import org.apache.commons.io.IOUtils import org.junit.Assert.assertEquals diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt index 64e7e143..1b164c63 100644 --- a/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt +++ b/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt @@ -4,7 +4,9 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml index 30de0468..9ca26a38 100644 --- a/app/src/debug/res/drawable/ic_launcher_foreground.xml +++ b/app/src/debug/res/drawable/ic_launcher_foreground.xml @@ -3,24 +3,23 @@ android:height="108dp" android:viewportWidth="110.34687" android:viewportHeight="110.34687"> - - - - - + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3bf13b9..abe12820 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,13 +28,8 @@ - - - + + 1) passContent[1] else "" // if there is a HOTP URI, we must return the extra content with the counter incremented return if (hasHotp()) { - extraContent.replaceFirst("counter=[0-9]+".toRegex(), "counter=" + java.lang.Long.toString(hotpCounter!!)) + extraContent.replaceFirst("counter=[0-9]+".toRegex(), "counter=" + (hotpCounter!!).toString()) } else extraContent } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java index f2c54ab8..33424353 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java @@ -211,8 +211,8 @@ public class PasswordFragment extends Fragment { } public void dismissActionMode() { - if (recyclerAdapter != null && recyclerAdapter.mActionMode != null) { - recyclerAdapter.mActionMode.finish(); + if (recyclerAdapter != null && recyclerAdapter.getActionMode() != null) { + recyclerAdapter.getActionMode().finish(); } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java b/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java index 0f2d22cf..6ec0529e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java @@ -12,12 +12,14 @@ import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.AppCompatTextView; import androidx.fragment.app.DialogFragment; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.zeapo.pwdstore.pwgen.PasswordGenerator; import org.jetbrains.annotations.NotNull; @@ -37,7 +39,7 @@ public class PasswordGeneratorDialogFragment extends DialogFragment { @SuppressLint("SetTextI18n") @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); final Activity callingActivity = requireActivity(); LayoutInflater inflater = callingActivity.getLayoutInflater(); @SuppressLint("InflateParams") final View view = inflater.inflate(R.layout.fragment_pwgen, null); @@ -66,10 +68,10 @@ public class PasswordGeneratorDialogFragment extends DialogFragment { checkBox = view.findViewById(R.id.pronounceable); checkBox.setChecked(!prefs.getBoolean("s", true)); - TextView textView = view.findViewById(R.id.lengthNumber); + AppCompatEditText textView = view.findViewById(R.id.lengthNumber); textView.setText(Integer.toString(prefs.getInt("length", 20))); - TextView passwordText = view.findViewById(R.id.passwordText); + AppCompatTextView passwordText = view.findViewById(R.id.passwordText); passwordText.setTypeface(monoTypeface); builder.setPositiveButton(getResources().getString(R.string.dialog_ok), (dialog, which) -> { diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index 95eb5a9b..469dac32 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -18,11 +18,10 @@ import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; 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; @@ -30,6 +29,7 @@ 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; @@ -143,7 +143,7 @@ public class PasswordStore extends AppCompatActivity { REQUEST_EXTERNAL_STORAGE)); snack.show(); View view = snack.getView(); - TextView tv = view.findViewById(com.google.android.material.R.id.snackbar_text); + AppCompatTextView tv = view.findViewById(com.google.android.material.R.id.snackbar_text); tv.setTextColor(Color.WHITE); tv.setMaxLines(10); } else { @@ -217,7 +217,7 @@ public class PasswordStore extends AppCompatActivity { int id = item.getItemId(); Intent intent; - AlertDialog.Builder initBefore = new AlertDialog.Builder(this) + final MaterialAlertDialogBuilder initBefore = new MaterialAlertDialogBuilder(this) .setMessage(this.getResources().getString(R.string.creation_dialog_text)) .setPositiveButton(this.getResources().getString(R.string.dialog_ok), null); @@ -342,7 +342,7 @@ public class PasswordStore extends AppCompatActivity { final Set keyIds = settings.getStringSet("openpgp_key_ids_set", new HashSet<>()); if (keyIds.isEmpty()) - new AlertDialog.Builder(this) + 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); @@ -498,7 +498,7 @@ public class PasswordStore extends AppCompatActivity { public void createPassword() { if (!PasswordRepository.isInitialized()) { - new AlertDialog.Builder(this) + new MaterialAlertDialogBuilder(this) .setMessage(this.getResources().getString(R.string.creation_dialog_text)) .setPositiveButton(this.getResources().getString(R.string.dialog_ok), (dialogInterface, i) -> { }).show(); @@ -506,7 +506,7 @@ public class PasswordStore extends AppCompatActivity { } if (settings.getStringSet("openpgp_key_ids_set", new HashSet<>()).isEmpty()) { - new AlertDialog.Builder(this) + 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) -> { @@ -534,7 +534,7 @@ public class PasswordStore extends AppCompatActivity { } final int position = (int) it.next(); final PasswordItem item = adapter.getValues().get(position); - new AlertDialog.Builder(this) + 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(); @@ -642,6 +642,7 @@ public class PasswordStore extends AppCompatActivity { refreshListAdapter(); break; case GitActivity.REQUEST_INIT: + case NEW_REPO_BUTTON: initializeRepositoryInfo(); break; case GitActivity.REQUEST_SYNC: @@ -651,9 +652,6 @@ public class PasswordStore extends AppCompatActivity { case HOME: checkLocalRepository(); break; - case NEW_REPO_BUTTON: - initializeRepositoryInfo(); - break; case CLONE_REPO_BUTTON: // duplicate code if (settings.getBoolean("git_external", false) && settings.getString("git_external_repo", null) != null) { @@ -708,7 +706,7 @@ public class PasswordStore extends AppCompatActivity { 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 AlertDialog.Builder(this) + new MaterialAlertDialogBuilder(this) .setTitle(getResources().getString(R.string.password_exists_title)) .setMessage(getResources().getString(R.string.password_exists_message, destinationLongName, sourceLongName)) @@ -739,7 +737,7 @@ public class PasswordStore extends AppCompatActivity { private void initRepository(final int operation) { PasswordRepository.closeRepository(); - new AlertDialog.Builder(this) + 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) -> { @@ -768,7 +766,7 @@ public class PasswordStore extends AppCompatActivity { intent.putExtra("operation", "git_external"); startActivityForResult(intent, operation); } else { - new AlertDialog.Builder(activity) + 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) -> { diff --git a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt index 2e894987..2eda0f73 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt @@ -24,7 +24,7 @@ class SelectFolderActivity : AppCompatActivity() { passwordList = SelectFolderFragment() val args = Bundle() - args.putString("Path", PasswordRepository.getRepositoryDirectory(applicationContext).absolutePath) + args.putString("Path", PasswordRepository.getRepositoryDirectory(applicationContext)?.absolutePath) passwordList.arguments = args diff --git a/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java b/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java index 144e8ab5..b17ed746 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java @@ -50,8 +50,12 @@ public class SelectFolderFragment extends Fragment { String path = getArguments().getString("Path"); pathStack = new Stack<>(); - recyclerAdapter = new FolderRecyclerAdapter((SelectFolderActivity) requireActivity(), mListener, - PasswordRepository.getPasswords(new File(path), PasswordRepository.getRepositoryDirectory(requireActivity()), getSortOrder())); + recyclerAdapter = new FolderRecyclerAdapter(mListener, + PasswordRepository.getPasswords( + new File(path), + PasswordRepository.getRepositoryDirectory(requireActivity()), getSortOrder() + ) + ); } @Override diff --git a/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java b/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java index 593a9bbb..77fbcf27 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java +++ b/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java @@ -2,8 +2,6 @@ package com.zeapo.pwdstore; import android.annotation.SuppressLint; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; import android.app.ProgressDialog; import android.content.ClipData; import android.content.ClipboardManager; @@ -22,13 +20,19 @@ import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.Spinner; -import android.widget.TextView; 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; @@ -37,6 +41,7 @@ import org.apache.commons.io.FileUtils; import java.io.File; import java.io.FileOutputStream; import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; public class SshKeyGen extends AppCompatActivity { @@ -50,7 +55,7 @@ public class SshKeyGen extends AppCompatActivity { setTitle("Generate SSH Key"); if (savedInstanceState == null) { - getFragmentManager().beginTransaction() + getSupportFragmentManager().beginTransaction() .replace(android.R.id.content, new SshKeyGenFragment()).commit(); } } @@ -77,20 +82,20 @@ public class SshKeyGen extends AppCompatActivity { 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(getActivity().getAssets(), "fonts/sourcecodepro.ttf"); + Typeface monoTypeface = Typeface.createFromAsset(requireContext().getAssets(), "fonts/sourcecodepro.ttf"); Spinner spinner = v.findViewById(R.id.length); Integer[] lengths = new Integer[]{2048, 4096}; - ArrayAdapter adapter = new ArrayAdapter<>(getActivity(), + ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_dropdown_item, lengths); spinner.setAdapter(adapter); - ((EditText) v.findViewById(R.id.passphrase)).setTypeface(monoTypeface); + ((TextInputEditText) v.findViewById(R.id.passphrase)).setTypeface(monoTypeface); - CheckBox checkbox = v.findViewById(R.id.show_passphrase); + final CheckBox checkbox = v.findViewById(R.id.show_passphrase); checkbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - EditText editText = v.findViewById(R.id.passphrase); - int selection = editText.getSelectionEnd(); + 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 { @@ -108,25 +113,27 @@ public class SshKeyGen extends AppCompatActivity { public ShowSshKeyFragment() { } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = getActivity().getLayoutInflater(); + 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); - TextView textView = v.findViewById(R.id.public_key); - File file = new File(getActivity().getFilesDir() + "/.ssh_key.pub"); + AppCompatTextView textView = v.findViewById(R.id.public_key); + File file = new File(activity.getFilesDir() + "/.ssh_key.pub"); try { - textView.setText(FileUtils.readFileToString(file)); + 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 (getActivity() instanceof SshKeyGen) - getActivity().finish(); + if (activity instanceof SshKeyGen) + activity.finish(); }); builder.setNegativeButton(getResources().getString(R.string.dialog_cancel), (dialog, which) -> { @@ -139,8 +146,8 @@ public class SshKeyGen extends AppCompatActivity { ad.setOnShowListener(dialog -> { Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); b.setOnClickListener(v1 -> { - TextView textView1 = getDialog().findViewById(R.id.public_key); - ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + 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); }); @@ -198,20 +205,19 @@ public class SshKeyGen extends AppCompatActivity { if (e == null) { Toast.makeText(weakReference.get(), "SSH-key generated", Toast.LENGTH_LONG).show(); DialogFragment df = new ShowSshKeyFragment(); - df.show(weakReference.get().getFragmentManager(), "public_key"); + 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 AlertDialog.Builder(weakReference.get()) + 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(); } - } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 349b5b1b..3384356f 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -8,19 +8,19 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment -import android.preference.CheckBoxPreference -import android.preference.Preference -import android.preference.PreferenceFragment -import android.preference.PreferenceManager import android.provider.DocumentsContract import android.provider.Settings import android.util.Log import android.view.MenuItem import android.view.accessibility.AccessibilityManager import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.documentfile.provider.DocumentFile +import androidx.preference.CheckBoxPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +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 @@ -35,74 +35,135 @@ import java.util.Calendar import java.util.HashSet import java.util.TimeZone +typealias ClickListener = Preference.OnPreferenceClickListener +typealias ChangeListener = Preference.OnPreferenceChangeListener + class UserPreference : AppCompatActivity() { private lateinit var prefsFragment: PrefsFragment - class PrefsFragment : PreferenceFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val callingActivity = activity as UserPreference + class PrefsFragment : PreferenceFragmentCompat() { + private var autofillDependencies = listOf() + private var autoFillEnablePreference: CheckBoxPreference? = null + private lateinit var callingActivity: UserPreference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + callingActivity = requireActivity() as UserPreference + val context = requireContext() val sharedPreferences = preferenceManager.sharedPreferences addPreferencesFromResource(R.xml.preference) - findPreference("app_version").summary = "Version: ${BuildConfig.VERSION_NAME}" + // Git preferences + val gitServerPreference = findPreference("git_server_info") + val gitConfigPreference = findPreference("git_config") + val sshKeyPreference = findPreference("ssh_key") + val sshKeygenPreference = findPreference("ssh_keygen") + val sshClearPassphrasePreference = findPreference("ssh_key_clear_passphrase") + val clearHotpIncrementPreference = findPreference("hotp_remember_clear_choice") + val viewSshKeyPreference = findPreference("ssh_see_key") + val deleteRepoPreference = findPreference("git_delete_repo") + val externalGitRepositoryPreference = findPreference("git_external") + val selectExternalGitRepositoryPreference = findPreference("pref_select_external") - findPreference("openpgp_key_id_pref").onPreferenceClickListener = Preference.OnPreferenceClickListener { + + // Crypto preferences + val keyPreference = findPreference("openpgp_key_id_pref") + + // General preferences + val clearAfterCopyPreference = findPreference("clear_after_copy") + val clearClipboard20xPreference = findPreference("clear_clipboard_20x") + + // Autofill preferences + autoFillEnablePreference = findPreference("autofill_enable") + val autoFillAppsPreference = findPreference("autofill_apps") + val autoFillDefaultPreference = findPreference("autofill_default") + val autoFillAlwaysShowDialogPreference = findPreference("autofill_always") + autofillDependencies = listOf( + autoFillAppsPreference, + autoFillDefaultPreference, + autoFillAlwaysShowDialogPreference + ) + + // Misc preferences + val appVersionPreference = findPreference("app_version") + + selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected)) + viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean("use_generated_key", false) + deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean("git_external", false) + sshClearPassphrasePreference?.isVisible = sharedPreferences.getString("ssh_key_passphrase", null)?.isNotEmpty() + ?: false + clearHotpIncrementPreference?.isVisible = sharedPreferences.getBoolean("hotp_remember_check", false) + clearAfterCopyPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0 + clearClipboard20xPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0 + val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null) + ?: HashSet()).toTypedArray() + keyPreference?.summary = if (selectedKeys.isEmpty()) { + this.resources.getString(R.string.pref_no_key_selected) + } else { + selectedKeys.joinToString(separator = ";") { s -> + OpenPgpUtils.convertKeyIdToHex(java.lang.Long.valueOf(s)) + } + } + + // see if the autofill service is enabled and check the preference accordingly + autoFillEnablePreference?.isChecked = callingActivity.isServiceEnabled + autofillDependencies.forEach { it?.isVisible = callingActivity.isServiceEnabled } + + appVersionPreference?.summary = "Version: ${BuildConfig.VERSION_NAME}" + + keyPreference?.onPreferenceClickListener = ClickListener { val intent = Intent(callingActivity, PgpActivity::class.java) intent.putExtra("OPERATION", "GET_KEY_ID") startActivityForResult(intent, IMPORT_PGP_KEY) true } - findPreference("ssh_key").onPreferenceClickListener = Preference.OnPreferenceClickListener { + sshKeyPreference?.onPreferenceClickListener = ClickListener { callingActivity.getSshKey() true } - findPreference("ssh_keygen").onPreferenceClickListener = Preference.OnPreferenceClickListener { + sshKeygenPreference?.onPreferenceClickListener = ClickListener { callingActivity.makeSshKey(true) true } - findPreference("ssh_see_key").onPreferenceClickListener = Preference.OnPreferenceClickListener { + viewSshKeyPreference?.onPreferenceClickListener = ClickListener { val df = SshKeyGen.ShowSshKeyFragment() - df.show(fragmentManager, "public_key") + df.show(requireFragmentManager(), "public_key") true } - findPreference("ssh_key_clear_passphrase").onPreferenceClickListener = - Preference.OnPreferenceClickListener { - sharedPreferences.edit().putString("ssh_key_passphrase", null).apply() - it.isEnabled = false - true - } + sshClearPassphrasePreference?.onPreferenceClickListener = ClickListener { + sharedPreferences.edit().putString("ssh_key_passphrase", null).apply() + it.isVisible = false + true + } - findPreference("hotp_remember_clear_choice").onPreferenceClickListener = - Preference.OnPreferenceClickListener { - sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply() - it.isEnabled = false - true - } + clearHotpIncrementPreference?.onPreferenceClickListener = ClickListener { + sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply() + it.isVisible = false + true + } - findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener { + gitServerPreference?.onPreferenceClickListener = ClickListener { val intent = Intent(callingActivity, GitActivity::class.java) intent.putExtra("Operation", GitActivity.EDIT_SERVER) startActivityForResult(intent, EDIT_GIT_INFO) true } - findPreference("git_config").onPreferenceClickListener = Preference.OnPreferenceClickListener { + gitConfigPreference?.onPreferenceClickListener = ClickListener { val intent = Intent(callingActivity, GitActivity::class.java) intent.putExtra("Operation", GitActivity.EDIT_GIT_CONFIG) startActivityForResult(intent, EDIT_GIT_CONFIG) true } - findPreference("git_delete_repo").onPreferenceClickListener = Preference.OnPreferenceClickListener { + deleteRepoPreference?.onPreferenceClickListener = ClickListener { val repoDir = PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext) - AlertDialog.Builder(callingActivity) + MaterialAlertDialogBuilder(callingActivity) .setTitle(R.string.pref_dialog_delete_title) .setMessage(resources.getString(R.string.dialog_delete_msg, repoDir)) .setCancelable(false) @@ -110,8 +171,8 @@ class UserPreference : AppCompatActivity() { try { FileUtils.cleanDirectory(PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext)) PasswordRepository.closeRepository() - } catch (e: Exception) { - //TODO Handle the different cases of exceptions + } catch (ignored: Exception) { + // TODO Handle the different cases of exceptions } sharedPreferences.edit().putBoolean("repository_initialized", false).apply() @@ -124,90 +185,77 @@ class UserPreference : AppCompatActivity() { true } - val externalRepo = findPreference("pref_select_external") - externalRepo.summary = - sharedPreferences.getString("git_external_repo", callingActivity.getString(R.string.no_repo_selected)) - externalRepo.onPreferenceClickListener = Preference.OnPreferenceClickListener { + selectExternalGitRepositoryPreference?.summary = + sharedPreferences.getString("git_external_repo", context.getString(R.string.no_repo_selected)) + selectExternalGitRepositoryPreference?.onPreferenceClickListener = ClickListener { callingActivity.selectExternalGitRepository() true } val resetRepo = Preference.OnPreferenceChangeListener { _, o -> - findPreference("git_delete_repo").isEnabled = !(o as Boolean) + deleteRepoPreference?.isVisible = !(o as Boolean) PasswordRepository.closeRepository() sharedPreferences.edit().putBoolean("repo_changed", true).apply() true } - findPreference("pref_select_external").onPreferenceChangeListener = resetRepo - findPreference("git_external").onPreferenceChangeListener = resetRepo + selectExternalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo + externalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo - findPreference("autofill_apps").onPreferenceClickListener = Preference.OnPreferenceClickListener { + autoFillAppsPreference?.onPreferenceClickListener = ClickListener { val intent = Intent(callingActivity, AutofillPreferenceActivity::class.java) startActivity(intent) true } - findPreference("autofill_enable").onPreferenceClickListener = Preference.OnPreferenceClickListener { - AlertDialog.Builder(callingActivity).setTitle(R.string.pref_autofill_enable_title) - .setView(R.layout.autofill_instructions).setPositiveButton(R.string.dialog_ok) { _, _ -> - val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - startActivity(intent) - }.setNegativeButton(R.string.dialog_cancel, null).setOnDismissListener { - (findPreference("autofill_enable") as CheckBoxPreference).isChecked = - (activity as UserPreference).isServiceEnabled - }.show() + autoFillEnablePreference?.onPreferenceClickListener = ClickListener { + var isEnabled = callingActivity.isServiceEnabled + if (isEnabled) { + startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) + } else { + MaterialAlertDialogBuilder(callingActivity) + .setTitle(R.string.pref_autofill_enable_title) + .setView(R.layout.autofill_instructions) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) + } + .setNegativeButton(R.string.dialog_cancel, null) + .setOnDismissListener { + isEnabled = callingActivity.isServiceEnabled + autoFillEnablePreference?.isChecked = isEnabled + autofillDependencies.forEach { it?.isVisible = isEnabled } + } + .show() + } true } - findPreference("export_passwords").apply { - isEnabled = sharedPreferences.getBoolean("repository_initialized", false) + findPreference("export_passwords")?.apply { + isVisible = sharedPreferences.getBoolean("repository_initialized", false) onPreferenceClickListener = Preference.OnPreferenceClickListener { callingActivity.exportPasswords() true } } - findPreference("general_show_time").onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> - try { - findPreference("clear_after_copy").isEnabled = newValue.toString().toInt() != 0 - findPreference("clear_clipboard_20x").isEnabled = newValue.toString().toInt() != 0 - true - } catch (e: NumberFormatException) { - false - } - } + findPreference("general_show_time")?.onPreferenceChangeListener = + ChangeListener { _, newValue: Any? -> + try { + val isEnabled = newValue.toString().toInt() != 0 + clearAfterCopyPreference?.isVisible = isEnabled + clearClipboard20xPreference?.isVisible = isEnabled + true + } catch (e: NumberFormatException) { + false + } + } } - override fun onStart() { - super.onStart() - val sharedPreferences = preferenceManager.sharedPreferences - findPreference("pref_select_external").summary = - preferenceManager.sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected)) - findPreference("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false) - findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false) - findPreference("ssh_key_clear_passphrase").isEnabled = sharedPreferences.getString( - "ssh_key_passphrase", - null - )?.isNotEmpty() ?: false - findPreference("hotp_remember_clear_choice").isEnabled = - sharedPreferences.getBoolean("hotp_remember_check", false) - findPreference("clear_after_copy").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0 - findPreference("clear_clipboard_20x").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0 - val keyPref = findPreference("openpgp_key_id_pref") - val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null) - ?: HashSet()).toTypedArray() - if (selectedKeys.isEmpty()) { - keyPref.summary = this.resources.getString(R.string.pref_no_key_selected) - } else { - keyPref.summary = selectedKeys.joinToString(separator = ";") { s -> - OpenPgpUtils.convertKeyIdToHex(java.lang.Long.valueOf(s)) - } - } - - // see if the autofill service is enabled and check the preference accordingly - (findPreference("autofill_enable") as CheckBoxPreference).isChecked = - (activity as UserPreference).isServiceEnabled + override fun onResume() { + super.onResume() + val isEnabled = callingActivity.isServiceEnabled + autoFillEnablePreference?.isChecked = isEnabled + autofillDependencies.forEach { it?.isVisible = isEnabled } } } @@ -220,19 +268,24 @@ class UserPreference : AppCompatActivity() { } prefsFragment = PrefsFragment() - fragmentManager.beginTransaction().replace(android.R.id.content, prefsFragment).commit() + supportFragmentManager + .beginTransaction() + .replace(android.R.id.content, prefsFragment) + .commit() supportActionBar?.setDisplayHomeAsUpEnabled(true) } fun selectExternalGitRepository() { - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(this.resources.getString(R.string.external_repository_dialog_title)) .setMessage(this.resources.getString(R.string.external_repository_dialog_text)) .setPositiveButton(R.string.dialog_ok) { _, _ -> val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(Intent.createChooser(i, "Choose Directory"), SELECT_GIT_DIRECTORY) - }.setNegativeButton(R.string.dialog_cancel, null).show() + } + .setNegativeButton(R.string.dialog_cancel, null) + .show() } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -350,7 +403,7 @@ class UserPreference : AppCompatActivity() { finish() } catch (e: IOException) { - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(this.resources.getString(R.string.ssh_key_error_dialog_title)) .setMessage(this.resources.getString(R.string.ssh_key_error_dialog_text) + e.message) .setPositiveButton(this.resources.getString(R.string.dialog_ok), null) @@ -372,7 +425,7 @@ class UserPreference : AppCompatActivity() { Log.d(TAG, "Selected repository path is $repoPath") if (Environment.getExternalStorageDirectory().path == repoPath) { - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.sdcard_root_warning_title)) .setMessage(getString(R.string.sdcard_root_warning_message)) .setPositiveButton("Remove everything") { _, _ -> @@ -380,7 +433,9 @@ class UserPreference : AppCompatActivity() { .edit() .putString("git_external_repo", uri?.path) .apply() - }.setNegativeButton(R.string.dialog_cancel, null).show() + } + .setNegativeButton(R.string.dialog_cancel, null) + .show() } PreferenceManager.getDefaultSharedPreferences(applicationContext) @@ -413,7 +468,7 @@ class UserPreference : AppCompatActivity() { */ private fun exportPasswords(targetDirectory: DocumentFile) { - val repositoryDirectory = PasswordRepository.getRepositoryDirectory(applicationContext) + val repositoryDirectory = requireNotNull(PasswordRepository.getRepositoryDirectory(applicationContext)) val sourcePassDir = DocumentFile.fromFile(repositoryDirectory) Log.d(TAG, "Copying ${repositoryDirectory.path} to $targetDirectory") diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt index d01ccdc8..04b8d7ef 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager +import android.net.Uri import android.os.Bundle import android.view.View import android.view.ViewGroup @@ -15,21 +16,23 @@ import android.widget.EditText import android.widget.ListView import android.widget.RadioButton import android.widget.RadioGroup -import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat +import androidx.appcompat.widget.AppCompatTextView import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.PasswordStore import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.utils.resolveAttribute import com.zeapo.pwdstore.utils.splitLines + class AutofillFragment : DialogFragment() { private var adapter: ArrayAdapter? = null private var isWeb: Boolean = false override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = AlertDialog.Builder(requireContext()) + val builder = MaterialAlertDialogBuilder(requireContext()) // this fragment is only created from the settings page (AutofillPreferenceActivity) // need to interact with the recyclerAdapter which is a member of activity val callingActivity = requireActivity() as AutofillPreferenceActivity @@ -51,9 +54,13 @@ class AutofillFragment : DialogFragment() { builder.setTitle(appName) view.findViewById(R.id.webURL).visibility = View.GONE } else { - iconPackageName = "com.android.browser" + val browserIntent = Intent("android.intent.action.VIEW", Uri.parse("http://")) + val resolveInfo = requireContext().packageManager + .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) + iconPackageName = resolveInfo?.activityInfo?.packageName builder.setTitle("Website") - (view.findViewById(R.id.webURL) as EditText).setText(packageName) + (view.findViewById(R.id.webURL) as EditText).setText(packageName + ?: "com.android.browser") } try { builder.setIcon(callingActivity.packageManager.getApplicationIcon(iconPackageName)) @@ -65,15 +72,17 @@ class AutofillFragment : DialogFragment() { adapter = object : ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, android.R.id.text1) { // set text color to black because default is white... override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val textView = super.getView(position, convertView, parent) as TextView - textView.setTextColor(ContextCompat.getColor(context, R.color.grey_black_1000)) + val textView = super.getView(position, convertView, parent) as AppCompatTextView + textView.setTextColor(requireContext().resolveAttribute(android.R.attr.textColor)) return textView } } (view.findViewById(R.id.matched) as ListView).adapter = adapter // delete items by clicking them (view.findViewById(R.id.matched) as ListView).onItemClickListener = - AdapterView.OnItemClickListener { _, _, position, _ -> adapter!!.remove(adapter!!.getItem(position)) } + AdapterView.OnItemClickListener { _, _, position, _ -> + adapter!!.remove(adapter!!.getItem(position)) + } // set the existing preference, if any val prefs: SharedPreferences = if (!isWeb) { diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt index f4c9357b..7cd7fcb7 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt @@ -7,8 +7,8 @@ import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedListAdapterCallback @@ -68,7 +68,6 @@ internal class AutofillRecyclerAdapter( holder.name.text = app.appName holder.secondary.visibility = View.VISIBLE - holder.view.setBackgroundResource(R.color.grey_white_1000) val prefs: SharedPreferences prefs = if (app.appName != app.packageName) { @@ -151,9 +150,9 @@ internal class AutofillRecyclerAdapter( } internal inner class ViewHolder(var view: View) : RecyclerView.ViewHolder(view), View.OnClickListener { - var name: TextView = view.findViewById(R.id.app_name) - var icon: ImageView = view.findViewById(R.id.app_icon) - var secondary: TextView = view.findViewById(R.id.secondary_text) + var name: AppCompatTextView = view.findViewById(R.id.app_name) + var icon: AppCompatImageView = view.findViewById(R.id.app_icon) + var secondary: AppCompatTextView = view.findViewById(R.id.secondary_text) var packageName: String? = null var appName: String? = null var isWeb: Boolean = false diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt index e4d1d04b..d9f42b00 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt @@ -12,7 +12,6 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle -import android.preference.PreferenceManager import android.provider.Settings import android.util.Log import android.view.WindowManager @@ -21,6 +20,8 @@ import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityWindowInfo import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.PasswordEntry import com.zeapo.pwdstore.R import com.zeapo.pwdstore.utils.PasswordRepository @@ -30,7 +31,6 @@ import org.openintents.openpgp.IOpenPgpService2 import org.openintents.openpgp.OpenPgpError import org.openintents.openpgp.util.OpenPgpApi import org.openintents.openpgp.util.OpenPgpServiceConnection - import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException @@ -291,7 +291,7 @@ class AutofillService : AccessibilityService() { when (preference) { "/first" -> { - if (!PasswordRepository.isInitialized()) { + if (!PasswordRepository.isInitialized) { PasswordRepository.initialize(this) } items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), webViewTitle) @@ -313,7 +313,7 @@ class AutofillService : AccessibilityService() { when (preference) { "/first" -> { - if (!PasswordRepository.isInitialized()) { + if (!PasswordRepository.isInitialized) { PasswordRepository.initialize(this) } items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName) @@ -326,7 +326,7 @@ class AutofillService : AccessibilityService() { // Put the newline separated list of passwords from the SharedPreferences // file into the items list. private fun getPreferredPasswords(preference: String) { - if (!PasswordRepository.isInitialized()) { + if (!PasswordRepository.isInitialized) { PasswordRepository.initialize(this) } val preferredPasswords = preference.splitLines() @@ -366,7 +366,7 @@ class AutofillService : AccessibilityService() { dialog = null } - val builder = AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog) + val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog) builder.setNegativeButton(R.string.dialog_cancel) { _, _ -> dialog!!.dismiss() dialog = null @@ -391,7 +391,7 @@ class AutofillService : AccessibilityService() { dialog = null } - val builder = AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog) + val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog) builder.setNegativeButton(R.string.dialog_cancel) { _, _ -> dialog!!.dismiss() dialog = null @@ -525,11 +525,13 @@ class AutofillService : AccessibilityService() { } OpenPgpApi.RESULT_CODE_ERROR -> { val error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) - Toast.makeText(this@AutofillService, - "Error from OpenKeyChain : " + error.message, - Toast.LENGTH_LONG).show() - Log.e(Constants.TAG, "onError getErrorId:" + error.errorId) - Log.e(Constants.TAG, "onError getMessage:" + error.message) + if (error != null) { + Toast.makeText(this@AutofillService, + "Error from OpenKeyChain : " + error.message, + Toast.LENGTH_LONG).show() + Log.e(Constants.TAG, "onError getErrorId:" + error.errorId) + Log.e(Constants.TAG, "onError getMessage:" + error.message) + } } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt index 665e8b7b..81fef413 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt @@ -12,7 +12,6 @@ import android.os.AsyncTask import android.os.Bundle import android.os.ConditionVariable import android.os.Handler -import android.preference.PreferenceManager import android.text.TextUtils import android.text.format.DateUtils import android.text.method.PasswordTransformationMethod @@ -29,8 +28,9 @@ import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.PasswordEntry import com.zeapo.pwdstore.PasswordGeneratorDialogFragment import com.zeapo.pwdstore.R @@ -156,8 +156,8 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { return true } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { android.R.id.home -> { if (passwordEntry?.hotpIsIncremented() == false) { setResult(RESULT_CANCELED) @@ -196,10 +196,10 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { private fun handleUserInteractionRequest(result: Intent, requestCode: Int) { Log.i(TAG, "RESULT_CODE_USER_INTERACTION_REQUIRED") - val pi: PendingIntent = result.getParcelableExtra(RESULT_INTENT) + val pi: PendingIntent? = result.getParcelableExtra(RESULT_INTENT) try { this@PgpActivity.startIntentSenderFromChild( - this@PgpActivity, pi.intentSender, requestCode, + this@PgpActivity, pi?.intentSender, requestCode, null, 0, 0, 0 ) } catch (e: IntentSender.SendIntentException) { @@ -219,10 +219,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { * * Check in open-pgp-lib how their definitions and error code */ - val error: OpenPgpError = result.getParcelableExtra(RESULT_ERROR) - showToast("Error from OpenKeyChain : " + error.message) - Log.e(TAG, "onError getErrorId:" + error.errorId) - Log.e(TAG, "onError getMessage:" + error.message) + val error: OpenPgpError? = result.getParcelableExtra(RESULT_ERROR) + if (error != null) { + showToast("Error from OpenKeyChain : " + error.message) + Log.e(TAG, "onError getErrorId:" + error.errorId) + Log.e(TAG, "onError getMessage:" + error.message) + } } private fun initOpenPgpApi() { @@ -354,7 +356,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { val checkLayout = checkInflater.inflate(R.layout.otp_confirm_layout, null) val rememberCheck: CheckBox = checkLayout.findViewById(R.id.hotp_remember_checkbox) - val dialogBuilder = AlertDialog.Builder(this) + val dialogBuilder = MaterialAlertDialogBuilder(this) dialogBuilder.setView(checkLayout) dialogBuilder.setMessage(R.string.dialog_update_body) .setCancelable(false) @@ -554,6 +556,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { RESULT_CODE_SUCCESS -> { try { val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS) + ?: LongArray(0) val keys = ids.map { it.toString() }.toSet() // use Long @@ -754,7 +757,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { val extraText = findViewById(R.id.crypto_extra_show) - if (extraText?.text?.isNotEmpty() ?: false) + if (extraText?.text?.isNotEmpty() == true) findViewById(R.id.crypto_extra_show_layout)?.visibility = View.VISIBLE if (showTime == 0) { 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 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 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(R.id.clone_protocol) + val connectionModeSpinner = findViewById(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(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(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(R.id.server_url) + val serverPort = findViewById(R.id.server_port) + val serverPath = findViewById(R.id.server_path) + val serverUser = findViewById(R.id.server_user) + val serverUri = findViewById(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(R.id.clone_button).visibility = View.INVISIBLE + findViewById(R.id.save_button).visibility = View.VISIBLE + } else { + findViewById(R.id.clone_button).visibility = View.VISIBLE + findViewById(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(R.id.clone_uri) + val serverUrl = findViewById(R.id.server_url) + val serverPort = findViewById(R.id.server_port) + val serverPath = findViewById(R.id.server_path) + val serverUser = findViewById(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(R.id.warn_url).visibility = View.GONE + } else { + val warnUrl = findViewById(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(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(R.id.clone_uri) + val serverUrl = findViewById(R.id.server_url) + val serverPort = findViewById(R.id.server_port) + val serverPath = findViewById(R.id.server_path) + val serverUser = findViewById(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(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(R.id.server_url) as TextInputEditText).text.toString()) + editor.putString("git_remote_location", (findViewById(R.id.server_path) as TextInputEditText).text.toString()) + editor.putString("git_remote_username", (findViewById(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(R.id.server_port) as TextInputEditText).text.toString()) + editor.putString("git_remote_uri", (findViewById(R.id.clone_uri) as TextInputEditText).text.toString()) + + // 'save' hostname variable for use by addRemote() either here or later + // in syncRepository() + hostname = (findViewById(R.id.clone_uri) as TextInputEditText).text.toString() + val port = (findViewById(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(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(R.id.git_user_name) + val email = findViewById(R.id.git_user_email) + val abort = findViewById(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(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(R.id.git_user_email) as TextInputEditText).text!!.toString() + editor.putString("git_config_user_email", email) + editor.putString("git_config_user_name", (findViewById(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(); } }); diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java deleted file mode 100644 index eba6a02e..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; - -import com.zeapo.pwdstore.R; - -import java.util.ArrayList; -import java.util.Set; -import java.util.TreeSet; - -public abstract class EntryRecyclerAdapter extends RecyclerView.Adapter { - final Set selectedItems = new TreeSet<>(); - private final Activity activity; - private final ArrayList values; - - EntryRecyclerAdapter(Activity activity, ArrayList values) { - this.activity = activity; - this.values = values; - } - - // Return the size of your dataset (invoked by the layout manager) - @Override - public int getItemCount() { - return values.size(); - } - - public ArrayList getValues() { - return this.values; - } - - public void clear() { - this.values.clear(); - this.notifyDataSetChanged(); - } - - public void addAll(ArrayList list) { - this.values.addAll(list); - this.notifyDataSetChanged(); - } - - public void add(PasswordItem item) { - this.values.add(item); - this.notifyItemInserted(getItemCount()); - } - - void toggleSelection(int position) { - if (!selectedItems.remove(position)) { - selectedItems.add(position); - } - } - - // use this after an item is removed to update the positions of items in set - // that followed the removed position - public void updateSelectedItems(int position, Set selectedItems) { - Set temp = new TreeSet<>(); - for (int selected : selectedItems) { - if (selected > position) { - temp.add(selected - 1); - } else { - temp.add(selected); - } - } - selectedItems.clear(); - selectedItems.addAll(temp); - } - - public void remove(int position) { - this.values.remove(position); - this.notifyItemRemoved(position); - - // keep selectedItems updated so we know what to notifyItemChanged - // (instead of just using notifyDataSetChanged) - updateSelectedItems(position, selectedItems); - } - - @NonNull - View.OnLongClickListener getOnLongClickListener(ViewHolder holder, PasswordItem pass) { - return v -> false; - } - - // Replace the contents of a view (invoked by the layout manager) - @SuppressLint("SetTextI18n") - @Override - public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { - final PasswordItem pass = getValues().get(position); - holder.name.setText(pass.toString()); - if (pass.getType() == PasswordItem.TYPE_CATEGORY) { - holder.typeImage.setImageResource(R.drawable.ic_folder_grey600_24dp); - } else { - holder.typeImage.setImageResource(R.drawable.ic_action_secure); - holder.name.setText(pass.toString()); - } - - holder.type.setText(pass.getFullPathToParent().replaceAll("(^/)|(/$)", "")); - - holder.view.setOnClickListener(getOnClickListener(holder, pass)); - - holder.view.setOnLongClickListener(getOnLongClickListener(holder, pass)); - - // after removal, everything is rebound for some reason; views are shuffled? - boolean selected = selectedItems.contains(position); - holder.view.setSelected(selected); - if (selected) { - holder.itemView.setBackgroundResource(R.color.deep_orange_200); - holder.type.setTextColor(Color.BLACK); - } else { - holder.itemView.setBackgroundColor(Color.alpha(1)); - holder.type.setTextColor(ContextCompat.getColor(activity, R.color.grey_500)); - } - } - - @NonNull - protected abstract View.OnClickListener getOnClickListener(ViewHolder holder, PasswordItem pass); - - // Create new views (invoked by the layout manager) - @Override - @NonNull - public PasswordRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, - int viewType) { - // create a new view - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.password_row_layout, parent, false); - return new ViewHolder(v); - } - - // Provide a reference to the views for each data item - // Complex data items may need more than one view per item, and - // you provide access to all the views for a data item in a view holder - static class ViewHolder extends RecyclerView.ViewHolder { - // each data item is just a string in this case - public final View view; - public final TextView name; - final TextView type; - final ImageView typeImage; - - ViewHolder(View v) { - super(v); - view = v; - name = view.findViewById(R.id.label); - type = view.findViewById(R.id.type); - typeImage = view.findViewById(R.id.type_image); - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt new file mode 100644 index 00000000..f52756f4 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt @@ -0,0 +1,118 @@ +package com.zeapo.pwdstore.utils + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView +import androidx.recyclerview.widget.RecyclerView + +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.widget.MultiselectableLinearLayout + +import java.util.ArrayList +import java.util.TreeSet + +abstract class EntryRecyclerAdapter internal constructor(val values: ArrayList) : RecyclerView.Adapter() { + internal val selectedItems: MutableSet = TreeSet() + + // Return the size of your dataset (invoked by the layout manager) + override fun getItemCount(): Int { + return values.size + } + + fun clear() { + this.values.clear() + this.notifyDataSetChanged() + } + + fun addAll(list: ArrayList) { + this.values.addAll(list) + this.notifyDataSetChanged() + } + + fun add(item: PasswordItem) { + this.values.add(item) + this.notifyItemInserted(itemCount) + } + + internal fun toggleSelection(position: Int) { + if (!selectedItems.remove(position)) { + selectedItems.add(position) + } + } + + // use this after an item is removed to update the positions of items in set + // that followed the removed position + fun updateSelectedItems(position: Int, selectedItems: MutableSet) { + val temp = TreeSet() + for (selected in selectedItems) { + if (selected > position) { + temp.add(selected - 1) + } else { + temp.add(selected) + } + } + selectedItems.clear() + selectedItems.addAll(temp) + } + + fun remove(position: Int) { + this.values.removeAt(position) + this.notifyItemRemoved(position) + + // keep selectedItems updated so we know what to notifyItemChanged + // (instead of just using notifyDataSetChanged) + updateSelectedItems(position, selectedItems) + } + + internal open fun getOnLongClickListener(holder: ViewHolder, pass: PasswordItem): View.OnLongClickListener { + return View.OnLongClickListener { false } + } + + // Replace the contents of a view (invoked by the layout manager) + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val pass = values[position] + holder.name.text = pass.toString() + if (pass.type == PasswordItem.TYPE_CATEGORY) { + holder.typeImage.setImageResource(R.drawable.ic_folder_tinted_24dp) + } else { + holder.typeImage.setImageResource(R.drawable.ic_action_secure) + holder.name.text = pass.toString() + } + + holder.type.text = pass.fullPathToParent.replace("(^/)|(/$)".toRegex(), "") + + holder.view.setOnClickListener(getOnClickListener(holder, pass)) + + holder.view.setOnLongClickListener(getOnLongClickListener(holder, pass)) + + // after removal, everything is rebound for some reason; views are shuffled? + val selected = selectedItems.contains(position) + holder.view.isSelected = selected + (holder.itemView as MultiselectableLinearLayout).setMultiSelected(selected) + } + + protected abstract fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener + + // Create new views (invoked by the layout manager) + override fun onCreateViewHolder(parent: ViewGroup, + viewType: Int): ViewHolder { + // create a new view + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.password_row_layout, parent, false) + return ViewHolder(v) + } + + // Provide a reference to the views for each data item + // Complex data items may need more than one view per item, and + // you provide access to all the views for a data item in a view holder + class ViewHolder(// each data item is just a string in this case + val view: View) : RecyclerView.ViewHolder(view) { + val name: AppCompatTextView = view.findViewById(R.id.label) + val type: AppCompatTextView = view.findViewById(R.id.type) + val typeImage: AppCompatImageView = view.findViewById(R.id.type_image) + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index a2be3ddc..7c2a28c3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -1,5 +1,14 @@ package com.zeapo.pwdstore.utils +import android.content.Context +import android.util.TypedValue + fun String.splitLines(): Array { return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() } + +fun Context.resolveAttribute(attr: Int): Int { + val typedValue = TypedValue() + this.theme.resolveAttribute(attr, typedValue, true) + return typedValue.data +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java deleted file mode 100644 index 1754e0f9..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.view.View; - -import androidx.annotation.NonNull; - -import com.zeapo.pwdstore.SelectFolderActivity; -import com.zeapo.pwdstore.SelectFolderFragment; - -import java.util.ArrayList; - -public class FolderRecyclerAdapter extends EntryRecyclerAdapter { - private final SelectFolderFragment.OnFragmentInteractionListener listener; - - // Provide a suitable constructor (depends on the kind of dataset) - public FolderRecyclerAdapter(SelectFolderActivity activity, SelectFolderFragment.OnFragmentInteractionListener listener, ArrayList values) { - super(activity, values); - this.listener = listener; - } - - @NonNull - protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) { - return v -> { - listener.onFragmentInteraction(pass); - notifyItemChanged(holder.getAdapterPosition()); - }; - } - -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt new file mode 100644 index 00000000..4f0ca4c8 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt @@ -0,0 +1,20 @@ +package com.zeapo.pwdstore.utils + +import android.view.View + +import com.zeapo.pwdstore.SelectFolderFragment + +import java.util.ArrayList + +class FolderRecyclerAdapter(private val listener: SelectFolderFragment.OnFragmentInteractionListener, + values: ArrayList +) : EntryRecyclerAdapter(values) { + + override fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener { + return View.OnClickListener { + listener.onFragmentInteraction(pass) + notifyItemChanged(holder.adapterPosition) + } + } + +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java deleted file mode 100644 index 1a498721..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import androidx.annotation.NonNull; - -import com.zeapo.pwdstore.crypto.PgpActivity; - -import java.io.File; - -public class PasswordItem implements Comparable { - - public final static char TYPE_CATEGORY = 'c'; - public final static char TYPE_PASSWORD = 'p'; - - private final char type; - private final String name; - private final PasswordItem parent; - private final File file; - private final String fullPathToParent; - private final String longName; - - /** - * Create a password item - *

- * Make it protected so that we use a builder - */ - private PasswordItem(String name, PasswordItem parent, char type, File file, File rootDir) { - this.name = name; - this.parent = parent; - this.type = type; - this.file = file; - fullPathToParent = file.getAbsolutePath() - .replace(rootDir.getAbsolutePath(), "") - .replace(file.getName(), ""); - longName = PgpActivity.getLongName(fullPathToParent, rootDir.getAbsolutePath(), toString()); - } - - /** - * Create a new Category item - */ - public static PasswordItem newCategory(String name, File file, PasswordItem parent, File rootDir) { - return new PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir); - } - - /** - * Create a new parentless category item - */ - public static PasswordItem newCategory(String name, File file, File rootDir) { - return new PasswordItem(name, null, TYPE_CATEGORY, file, rootDir); - } - - /** - * Create a new password item - */ - public static PasswordItem newPassword(String name, File file, PasswordItem parent, File rootDir) { - return new PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir); - } - - /** - * Create a new parentless password item - */ - public static PasswordItem newPassword(String name, File file, File rootDir) { - return new PasswordItem(name, null, TYPE_PASSWORD, file, rootDir); - } - - public char getType() { - return this.type; - } - - String getName() { - return this.name; - } - - public PasswordItem getParent() { - return this.parent; - } - - public File getFile() { - return this.file; - } - - public String getFullPathToParent() { - return this.fullPathToParent; - } - - public String getLongName() { - return longName; - } - - @NonNull - @Override - public String toString() { - return this.getName().replace(".gpg", ""); - } - - @Override - public boolean equals(Object o) { - // Makes it possible to have a category and a password with the same name - return o != null - && o.getClass() == PasswordItem.class - && ((PasswordItem) o).getFile().equals(this.getFile()); - } - - @Override - public int compareTo(@NonNull Object o) { - PasswordItem other = (PasswordItem) o; - // Appending the type will make the sort type dependent - return (this.getType() + this.getName()) - .compareToIgnoreCase(other.getType() + other.getName()); - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt new file mode 100644 index 00000000..ddecd2da --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt @@ -0,0 +1,81 @@ +package com.zeapo.pwdstore.utils + +import com.zeapo.pwdstore.crypto.PgpActivity +import java.io.File + +data class PasswordItem( + val name: String, + val parent: PasswordItem? = null, + val type: Char, + val file: File, + val rootDir: File +) : Comparable { + val fullPathToParent = file.absolutePath + .replace(rootDir.absolutePath, "") + .replace(file.name, "") + + val longName = PgpActivity.getLongName( + fullPathToParent, + rootDir.absolutePath, + toString()) + + override fun equals(other: Any?): Boolean { + return (other is PasswordItem) && (other.file == file) + } + + override fun compareTo(other: PasswordItem): Int { + return (type + name).compareTo(other.type + other.name, ignoreCase = true) + } + + override fun toString(): String { + return name.replace(".gpg", "") + } + + override fun hashCode(): Int { + return 0 + } + + companion object { + const val TYPE_CATEGORY = 'c' + const val TYPE_PASSWORD = 'p' + + @JvmStatic + fun newCategory( + name: String, + file: File, + parent: PasswordItem, + rootDir: File + ): PasswordItem { + return PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir) + } + + @JvmStatic + fun newCategory( + name: String, + file: File, + rootDir: File + ): PasswordItem { + return PasswordItem(name, null, TYPE_CATEGORY, file, rootDir) + } + + @JvmStatic + fun newPassword( + name: String, + file: File, + parent: PasswordItem, + rootDir: File + ): PasswordItem { + return PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir) + } + + @JvmStatic + fun newPassword( + name: String, + file: File, + rootDir: File + ): PasswordItem { + return PasswordItem(name, null, TYPE_PASSWORD, file, rootDir) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java deleted file mode 100644 index 9170e408..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.appcompat.view.ActionMode; - -import com.zeapo.pwdstore.PasswordFragment; -import com.zeapo.pwdstore.PasswordStore; -import com.zeapo.pwdstore.R; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.TreeSet; - -public class PasswordRecyclerAdapter extends EntryRecyclerAdapter { - private final PasswordStore activity; - private final PasswordFragment.OnFragmentInteractionListener listener; - public ActionMode mActionMode; - private Boolean canEdit; - private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { - - // Called when the action mode is created; startActionMode() was called - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - // Inflate a menu resource providing context menu items - mode.getMenuInflater().inflate(R.menu.context_pass, menu); - // hide the fab - activity.findViewById(R.id.fab).setVisibility(View.GONE); - return true; - } - - // Called each time the action mode is shown. Always called after onCreateActionMode, but - // may be called multiple times if the mode is invalidated. - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - if (canEdit) { - menu.findItem(R.id.menu_edit_password).setVisible(true); - } else { - menu.findItem(R.id.menu_edit_password).setVisible(false); - } - return true; // Return false if nothing is done - } - - // Called when the user selects a contextual menu item - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_delete_password: - activity.deletePasswords(PasswordRecyclerAdapter.this, new TreeSet<>(selectedItems)); - mode.finish(); // Action picked, so close the CAB - return true; - case R.id.menu_edit_password: - activity.editPassword(getValues().get(selectedItems.iterator().next())); - mode.finish(); - return true; - case R.id.menu_move_password: - ArrayList selectedPasswords = new ArrayList<>(); - for (Integer id : selectedItems) { - selectedPasswords.add(getValues().get(id)); - } - activity.movePasswords(selectedPasswords); - default: - return false; - } - } - - // Called when the user exits the action mode - @Override - public void onDestroyActionMode(ActionMode mode) { - for (Iterator it = selectedItems.iterator(); it.hasNext(); ) { - // need the setSelected line in onBind - notifyItemChanged(it.next()); - it.remove(); - } - mActionMode = null; - // show the fab - activity.findViewById(R.id.fab).setVisibility(View.VISIBLE); - } - }; - - // Provide a suitable constructor (depends on the kind of dataset) - public PasswordRecyclerAdapter(PasswordStore activity, PasswordFragment.OnFragmentInteractionListener listener, ArrayList values) { - super(activity, values); - this.activity = activity; - this.listener = listener; - } - - @Override - @NonNull - protected View.OnLongClickListener getOnLongClickListener(final ViewHolder holder, final PasswordItem pass) { - return v -> { - if (mActionMode != null) { - return false; - } - toggleSelection(holder.getAdapterPosition()); - canEdit = pass.getType() == PasswordItem.TYPE_PASSWORD; - // Start the CAB using the ActionMode.Callback - mActionMode = activity.startSupportActionMode(mActionModeCallback); - mActionMode.setTitle("" + selectedItems.size()); - mActionMode.invalidate(); - notifyItemChanged(holder.getAdapterPosition()); - return true; - }; - } - - @Override - @NonNull - protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) { - return v -> { - if (mActionMode != null) { - toggleSelection(holder.getAdapterPosition()); - mActionMode.setTitle("" + selectedItems.size()); - if (selectedItems.isEmpty()) { - mActionMode.finish(); - } else if (selectedItems.size() == 1 && !canEdit) { - if (getValues().get(selectedItems.iterator().next()).getType() == PasswordItem.TYPE_PASSWORD) { - canEdit = true; - mActionMode.invalidate(); - } - } else if (selectedItems.size() >= 1 && canEdit) { - canEdit = false; - mActionMode.invalidate(); - } - } else { - listener.onFragmentInteraction(pass); - } - notifyItemChanged(holder.getAdapterPosition()); - }; - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt new file mode 100644 index 00000000..d7493a48 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt @@ -0,0 +1,116 @@ +package com.zeapo.pwdstore.utils + +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.view.ActionMode + +import com.zeapo.pwdstore.PasswordFragment +import com.zeapo.pwdstore.PasswordStore +import com.zeapo.pwdstore.R + +import java.util.ArrayList +import java.util.TreeSet + +class PasswordRecyclerAdapter(private val activity: PasswordStore, + private val listener: PasswordFragment.OnFragmentInteractionListener, + values: ArrayList +) : EntryRecyclerAdapter(values) { + var actionMode: ActionMode? = null + private var canEdit: Boolean = false + private val actionModeCallback = object : ActionMode.Callback { + + // Called when the action mode is created; startActionMode() was called + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + // Inflate a menu resource providing context menu items + mode.menuInflater.inflate(R.menu.context_pass, menu) + // hide the fab + activity.findViewById(R.id.fab).visibility = View.GONE + return true + } + + // Called each time the action mode is shown. Always called after onCreateActionMode, but + // may be called multiple times if the mode is invalidated. + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + menu.findItem(R.id.menu_edit_password).isVisible = canEdit + return true // Return false if nothing is done + } + + // Called when the user selects a contextual menu item + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_delete_password -> { + activity.deletePasswords(this@PasswordRecyclerAdapter, TreeSet(selectedItems)) + mode.finish() // Action picked, so close the CAB + return true + } + R.id.menu_edit_password -> { + activity.editPassword(values[selectedItems.iterator().next()]) + mode.finish() + return true + } + R.id.menu_move_password -> { + val selectedPasswords = ArrayList() + for (id in selectedItems) { + selectedPasswords.add(values[id]) + } + activity.movePasswords(selectedPasswords) + return false + } + else -> return false + } + } + + // Called when the user exits the action mode + override fun onDestroyActionMode(mode: ActionMode) { + val it = selectedItems.iterator() + while (it.hasNext()) { + // need the setSelected line in onBind + notifyItemChanged(it.next()) + it.remove() + } + actionMode = null + // show the fab + activity.findViewById(R.id.fab).visibility = View.VISIBLE + } + } + + override fun getOnLongClickListener(holder: ViewHolder, pass: PasswordItem): View.OnLongClickListener { + return View.OnLongClickListener { + if (actionMode != null) { + return@OnLongClickListener false + } + toggleSelection(holder.adapterPosition) + canEdit = pass.type == PasswordItem.TYPE_PASSWORD + // Start the CAB using the ActionMode.Callback + actionMode = activity.startSupportActionMode(actionModeCallback) + actionMode?.title = "" + selectedItems.size + actionMode?.invalidate() + notifyItemChanged(holder.adapterPosition) + true + } + } + + override fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener { + return View.OnClickListener { + if (actionMode != null) { + toggleSelection(holder.adapterPosition) + actionMode?.title = "" + selectedItems.size + if (selectedItems.isEmpty()) { + actionMode?.finish() + } else if (selectedItems.size == 1 && (canEdit.not())) { + if (values[selectedItems.iterator().next()].type == PasswordItem.TYPE_PASSWORD) { + canEdit = true + actionMode?.invalidate() + } + } else if (selectedItems.size >= 1 && canEdit) { + canEdit = false + actionMode?.invalidate() + } + } else { + listener.onFragmentInteraction(pass) + } + notifyItemChanged(holder.adapterPosition) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java deleted file mode 100644 index 9c27a448..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import org.apache.commons.io.filefilter.FileFilterUtils; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import org.eclipse.jgit.transport.RefSpec; -import org.eclipse.jgit.transport.RemoteConfig; -import org.eclipse.jgit.transport.URIish; - -import java.io.File; -import java.io.FileFilter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.Set; - -import static java.util.Collections.sort; - -public class PasswordRepository { - - private static Repository repository; - - protected PasswordRepository() { - } - - /** - * Returns the git repository - * - * @param localDir needed only on the creation - * @return the git repository - */ - public static Repository getRepository(File localDir) { - if (repository == null && localDir != null) { - FileRepositoryBuilder builder = new FileRepositoryBuilder(); - try { - repository = builder.setGitDir(localDir) - .readEnvironment() - .build(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - return repository; - } - - public static boolean isInitialized() { - return repository != null; - } - - public static void createRepository(File localDir) throws Exception { - localDir.delete(); - - Git.init().setDirectory(localDir).call(); - getRepository(localDir); - } - - // TODO add multiple remotes support for pull/push - public static void addRemote(String name, String url, Boolean replace) { - StoredConfig storedConfig = repository.getConfig(); - Set remotes = storedConfig.getSubsections("remote"); - - if (!remotes.contains(name)) { - try { - URIish uri = new URIish(url); - RefSpec refSpec = new RefSpec("+refs/head/*:refs/remotes/" + name + "/*"); - - RemoteConfig remoteConfig = new RemoteConfig(storedConfig, name); - remoteConfig.addFetchRefSpec(refSpec); - remoteConfig.addPushRefSpec(refSpec); - remoteConfig.addURI(uri); - remoteConfig.addPushURI(uri); - - remoteConfig.update(storedConfig); - - storedConfig.save(); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (replace) { - try { - URIish uri = new URIish(url); - - RemoteConfig remoteConfig = new RemoteConfig(storedConfig, name); - // remove the first and eventually the only uri - if (remoteConfig.getURIs().size() > 0) { - remoteConfig.removeURI(remoteConfig.getURIs().get(0)); - } - if (remoteConfig.getPushURIs().size() > 0) { - remoteConfig.removePushURI(remoteConfig.getPushURIs().get(0)); - } - - remoteConfig.addURI(uri); - remoteConfig.addPushURI(uri); - - remoteConfig.update(storedConfig); - - storedConfig.save(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public static void closeRepository() { - if (repository != null) repository.close(); - repository = null; - } - - public static File getRepositoryDirectory(Context context) { - File dir = null; - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - - if (settings.getBoolean("git_external", false)) { - String external_repo = settings.getString("git_external_repo", null); - if (external_repo != null) { - dir = new File(external_repo); - } - } else { - dir = new File(context.getFilesDir() + "/store"); - } - - return dir; - } - - public static Repository initialize(Context context) { - File dir = getRepositoryDirectory(context); - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - - if (dir == null) { - return null; - } - - // uninitialize the repo if the dir does not exist or is absolutely empty - if (!dir.exists() || !dir.isDirectory() || dir.listFiles().length == 0) { - settings.edit().putBoolean("repository_initialized", false).apply(); - } else { - settings.edit().putBoolean("repository_initialized", true).apply(); - } - - // create the repository static variable in PasswordRepository - return PasswordRepository.getRepository(new File(dir.getAbsolutePath() + "/.git")); - } - - /** - * Gets the password items in the root directory - * - * @return a list of passwords in the root direcotyr - */ - public static ArrayList getPasswords(File rootDir, PasswordSortOrder sortOrder) { - return getPasswords(rootDir, rootDir, sortOrder); - } - - /** - * Gets the .gpg files in a directory - * - * @param path the directory path - * @return the list of gpg files in that directory - */ - public static ArrayList getFilesList(File path) { - if (path == null || !path.exists()) return new ArrayList<>(); - - Log.d("REPO", "current path: " + path.getPath()); - List directories = Arrays.asList(path.listFiles((FileFilter) FileFilterUtils.directoryFileFilter())); - List files = Arrays.asList(path.listFiles((FileFilter) FileFilterUtils.suffixFileFilter(".gpg"))); - - ArrayList items = new ArrayList<>(); - items.addAll(directories); - items.addAll(files); - - return items; - } - - /** - * Gets the passwords (PasswordItem) in a directory - * - * @param path the directory path - * @return a list of password items - */ - public static ArrayList getPasswords(File path, File rootDir, PasswordSortOrder sortOrder) { - //We need to recover the passwords then parse the files - ArrayList passList = getFilesList(path); - - if (passList.size() == 0) return new ArrayList<>(); - - ArrayList passwordList = new ArrayList<>(); - - for (File file : passList) { - if (file.isFile()) { - if (!file.isHidden()) { - passwordList.add(PasswordItem.newPassword(file.getName(), file, rootDir)); - } - } else { - if (!file.isHidden()) { - passwordList.add(PasswordItem.newCategory(file.getName(), file, rootDir)); - } - } - } - sort(passwordList, sortOrder.comparator); - return passwordList; - } - - /** - * Sets the git user name - * - * @param username username - */ - public static void setUserName(String username) { - setStringConfig("user", null, "name", username); - } - - /** - * Sets the git user email - * - * @param email email - */ - public static void setUserEmail(String email) { - setStringConfig("user", null, "email", email); - } - - /** - * Sets a git config value - * - * @param section config section name - * @param subsection config subsection name - * @param name config name - * @param value the value to be set - */ - private static void setStringConfig(String section, String subsection, String name, String value) { - if (isInitialized()) { - StoredConfig config = repository.getConfig(); - config.setString(section, subsection, name, value); - try { - config.save(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public enum PasswordSortOrder { - - FOLDER_FIRST(new Comparator() { - @Override - public int compare(PasswordItem p1, PasswordItem p2) { - return (p1.getType() + p1.getName()) - .compareToIgnoreCase(p2.getType() + p2.getName()); - } - }), - - INDEPENDENT(new Comparator() { - @Override - public int compare(PasswordItem p1, PasswordItem p2) { - return p1.getName().compareToIgnoreCase(p2.getName()); - } - }), - - FILE_FIRST(new Comparator() { - @Override - public int compare(PasswordItem p1, PasswordItem p2) { - return (p2.getType() + p1.getName()) - .compareToIgnoreCase(p1.getType() + p2.getName()); - } - }); - - private Comparator comparator; - - PasswordSortOrder(Comparator comparator) { - this.comparator = comparator; - } - - public static PasswordSortOrder getSortOrder(SharedPreferences settings) { - return valueOf(settings.getString("sort_order", FOLDER_FIRST.name())); - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt new file mode 100644 index 00000000..9e94e400 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt @@ -0,0 +1,282 @@ +package com.zeapo.pwdstore.utils + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import org.apache.commons.io.filefilter.FileFilterUtils +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Repository +import org.eclipse.jgit.storage.file.FileRepositoryBuilder +import org.eclipse.jgit.transport.RefSpec +import org.eclipse.jgit.transport.RemoteConfig +import org.eclipse.jgit.transport.URIish +import java.io.File +import java.io.FileFilter +import java.util.Comparator + +open class PasswordRepository protected constructor() { + + @Suppress("Unused") + enum class PasswordSortOrder(val comparator: Comparator) { + + FOLDER_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem -> + (p1.type + p1.name) + .compareTo(p2.type + p2.name, ignoreCase = true) + }), + + INDEPENDENT(Comparator { p1: PasswordItem, p2: PasswordItem -> + p1.name.compareTo(p2.name, ignoreCase = true) + }), + + FILE_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem -> + (p2.type + p1.name).compareTo(p1.type + p2.name, ignoreCase = true) + }); + + + companion object { + @JvmStatic + fun getSortOrder(settings: SharedPreferences): PasswordSortOrder { + return valueOf(settings.getString("sort_order", null) ?: FOLDER_FIRST.name) + } + } + } + + companion object { + + private var repository: Repository? = null + + /** + * Returns the git repository + * + * @param localDir needed only on the creation + * @return the git repository + */ + @JvmStatic + fun getRepository(localDir: File?): Repository? { + if (repository == null && localDir != null) { + val builder = FileRepositoryBuilder() + try { + repository = builder.setGitDir(localDir) + .readEnvironment() + .build() + } catch (e: Exception) { + e.printStackTrace() + return null + } + + } + return repository + } + + @JvmStatic + val isInitialized: Boolean + get() = repository != null + + @JvmStatic + @Throws(Exception::class) + fun createRepository(localDir: File) { + localDir.delete() + + Git.init().setDirectory(localDir).call() + getRepository(localDir) + } + + // TODO add multiple remotes support for pull/push + @JvmStatic + fun addRemote(name: String, url: String, replace: Boolean?) { + val storedConfig = repository!!.config + val remotes = storedConfig.getSubsections("remote") + + if (!remotes.contains(name)) { + try { + val uri = URIish(url) + val refSpec = RefSpec("+refs/head/*:refs/remotes/$name/*") + + val remoteConfig = RemoteConfig(storedConfig, name) + remoteConfig.addFetchRefSpec(refSpec) + remoteConfig.addPushRefSpec(refSpec) + remoteConfig.addURI(uri) + remoteConfig.addPushURI(uri) + + remoteConfig.update(storedConfig) + + storedConfig.save() + } catch (e: Exception) { + e.printStackTrace() + } + + } else if (replace!!) { + try { + val uri = URIish(url) + + val remoteConfig = RemoteConfig(storedConfig, name) + // remove the first and eventually the only uri + if (remoteConfig.urIs.size > 0) { + remoteConfig.removeURI(remoteConfig.urIs[0]) + } + if (remoteConfig.pushURIs.size > 0) { + remoteConfig.removePushURI(remoteConfig.pushURIs[0]) + } + + remoteConfig.addURI(uri) + remoteConfig.addPushURI(uri) + + remoteConfig.update(storedConfig) + + storedConfig.save() + } catch (e: Exception) { + e.printStackTrace() + } + + } + } + + @JvmStatic + fun closeRepository() { + if (repository != null) repository!!.close() + repository = null + } + + @JvmStatic + fun getRepositoryDirectory(context: Context): File? { + var dir: File? = null + val settings = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) + + if (settings.getBoolean("git_external", false)) { + val externalRepo = settings.getString("git_external_repo", null) + if (externalRepo != null) { + dir = File(externalRepo) + } + } else { + dir = File(context.filesDir.toString() + "/store") + } + + return dir + } + + @JvmStatic + fun initialize(context: Context): Repository? { + val dir = getRepositoryDirectory(context) + val settings = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) + + if (dir == null) { + return null + } + + // uninitialize the repo if the dir does not exist or is absolutely empty + if (!dir.exists() || !dir.isDirectory || dir.listFiles()!!.isEmpty()) { + settings.edit().putBoolean("repository_initialized", false).apply() + } else { + settings.edit().putBoolean("repository_initialized", true).apply() + } + + // create the repository static variable in PasswordRepository + return getRepository(File(dir.absolutePath + "/.git")) + } + + /** + * Gets the password items in the root directory + * + * @return a list of passwords in the root direcotyr + */ + @JvmStatic + fun getPasswords(rootDir: File, sortOrder: PasswordSortOrder): ArrayList { + return getPasswords(rootDir, rootDir, sortOrder) + } + + /** + * Gets the .gpg files in a directory + * + * @param path the directory path + * @return the list of gpg files in that directory + */ + @JvmStatic + fun getFilesList(path: File?): ArrayList { + if (path == null || !path.exists()) return ArrayList() + + val directories = (path.listFiles(FileFilterUtils.directoryFileFilter() as FileFilter) + ?: emptyArray()).toList() + val files = (path.listFiles(FileFilterUtils.suffixFileFilter(".gpg") as FileFilter) + ?: emptyArray()).toList() + + val items = ArrayList() + items.addAll(directories) + items.addAll(files) + + return items + } + + /** + * Gets the passwords (PasswordItem) in a directory + * + * @param path the directory path + * @return a list of password items + */ + @JvmStatic + fun getPasswords(path: File, rootDir: File, sortOrder: PasswordSortOrder): ArrayList { + //We need to recover the passwords then parse the files + val passList = getFilesList(path) + + if (passList.size == 0) return ArrayList() + + val passwordList = ArrayList() + + for (file in passList) { + if (file.isFile) { + if (!file.isHidden) { + passwordList.add(PasswordItem.newPassword(file.name, file, rootDir)) + } + } else { + if (!file.isHidden) { + passwordList.add(PasswordItem.newCategory(file.name, file, rootDir)) + } + } + } + passwordList.sortWith(sortOrder.comparator) + return passwordList + } + + /** + * Sets the git user name + * + * @param username username + */ + @JvmStatic + fun setUserName(username: String) { + setStringConfig("user", null, "name", username) + } + + /** + * Sets the git user email + * + * @param email email + */ + @JvmStatic + fun setUserEmail(email: String) { + setStringConfig("user", null, "email", email) + } + + /** + * Sets a git config value + * + * @param section config section name + * @param subsection config subsection name + * @param name config name + * @param value the value to be set + */ + @JvmStatic + @Suppress("SameParameterValue") + private fun setStringConfig(section: String, subsection: String?, name: String, value: String) { + if (isInitialized) { + val config = repository!!.config + config.setString(section, subsection, name, value) + try { + config.save() + } catch (e: Exception) { + e.printStackTrace() + } + + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/widget/MultiselectableLinearLayout.kt b/app/src/main/java/com/zeapo/pwdstore/widget/MultiselectableLinearLayout.kt new file mode 100644 index 00000000..7ceea1db --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/widget/MultiselectableLinearLayout.kt @@ -0,0 +1,51 @@ +/* + * Copyright © 2017-2018 WireGuard LLC. + * Copyright © 2018-2019 Harsh Shandilya . All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.zeapo.pwdstore.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import com.zeapo.pwdstore.R + +class MultiselectableLinearLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { + private var multiselected: Boolean = false + + override fun onCreateDrawableState(extraSpace: Int): IntArray { + if (multiselected) { + val drawableState = super.onCreateDrawableState(extraSpace + 1) + View.mergeDrawableStates(drawableState, STATE_MULTISELECTED) + return drawableState + } + return super.onCreateDrawableState(extraSpace) + } + + fun setMultiSelected(on: Boolean) { + if (!multiselected) { + multiselected = true + refreshDrawableState() + } + isActivated = on + } + + fun setSingleSelected(on: Boolean) { + if (multiselected) { + multiselected = false + refreshDrawableState() + } + isActivated = on + } + + companion object { + private val STATE_MULTISELECTED = intArrayOf(R.attr.state_multiselected) + } +} \ No newline at end of file diff --git a/app/src/main/res/color/text_color.xml b/app/src/main/res/color/text_color.xml deleted file mode 100644 index 9e70185e..00000000 --- a/app/src/main/res/color/text_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/autofill_row_background.xml b/app/src/main/res/drawable/autofill_row_background.xml deleted file mode 100644 index 05e887ca..00000000 --- a/app/src/main/res/drawable/autofill_row_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_line.xml b/app/src/main/res/drawable/bottom_line.xml index 323b03e0..e67c96fa 100644 --- a/app/src/main/res/drawable/bottom_line.xml +++ b/app/src/main/res/drawable/bottom_line.xml @@ -2,13 +2,13 @@ - + - + diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml index cf2134ff..23a88317 100644 --- a/app/src/main/res/drawable/divider.xml +++ b/app/src/main/res/drawable/divider.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action_secure.xml b/app/src/main/res/drawable/ic_action_secure.xml index 8f7b7539..af182711 100644 --- a/app/src/main/res/drawable/ic_action_secure.xml +++ b/app/src/main/res/drawable/ic_action_secure.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="@color/grey_600"> + android:tint="?attr/passwordIconColor"> diff --git a/app/src/main/res/drawable/ic_folder_grey600_24dp.xml b/app/src/main/res/drawable/ic_folder_tinted_24dp.xml similarity index 89% rename from app/src/main/res/drawable/ic_folder_grey600_24dp.xml rename to app/src/main/res/drawable/ic_folder_tinted_24dp.xml index dbd73db5..51380a42 100644 --- a/app/src/main/res/drawable/ic_folder_grey600_24dp.xml +++ b/app/src/main/res/drawable/ic_folder_tinted_24dp.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="@color/grey_600"> + android:tint="?attr/passwordIconColor"> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 318de001..4072a948 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,9 +1,8 @@ + android:width="108dp" + android:height="108dp" + android:viewportWidth="110.34687" + android:viewportHeight="110.34687"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/red_rectangle.xml b/app/src/main/res/drawable/red_rectangle.xml index a6c48d0f..8213a7e3 100644 --- a/app/src/main/res/drawable/red_rectangle.xml +++ b/app/src/main/res/drawable/red_rectangle.xml @@ -13,7 +13,7 @@ - + + android:background="?android:attr/windowBackground"> - + android:layout_margin="8dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + - + + + + android:inputType="textWebEmailAddress" /> + + - - - - - - + android:id="@+id/server_url" + android:inputType="textWebEmailAddress" /> - + - - - + + + android:inputType="number" /> - + - + - - - - - - - + android:inputType="textWebEmailAddress"/> - + - - + - - - - + android:inputType="textWebEmailAddress"/> - + - + + - + - - + -