aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/ui/passwords
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2022-07-15 00:53:48 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2022-07-15 01:13:47 +0530
commit549ee790d3e52bc62565ddf92e6a556e98b5195e (patch)
treed5758e5eb80093704e683c8da926838e18182588 /app/src/main/java/dev/msfjarvis/aps/ui/passwords
parent010c6e227c9cc27f4d01bc912311f977b2aeb3a7 (diff)
downloadHardenedPass-549ee790d3e52bc62565ddf92e6a556e98b5195e.tar.gz
HardenedPass-549ee790d3e52bc62565ddf92e6a556e98b5195e.zip
all: re-do package structure yet again
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/ui/passwords')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt382
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt639
2 files changed, 0 insertions, 1021 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt
deleted file mode 100644
index c70f0b5a..00000000
--- a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt
+++ /dev/null
@@ -1,382 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package dev.msfjarvis.aps.ui.passwords
-
-import android.content.Context
-import android.content.SharedPreferences
-import android.os.Bundle
-import android.os.Parcelable
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.view.animation.Animation
-import android.view.animation.AnimationUtils
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.appcompat.view.ActionMode
-import androidx.core.content.edit
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.github.michaelbull.result.fold
-import com.github.michaelbull.result.onFailure
-import com.github.michaelbull.result.runCatching
-import dagger.hilt.android.AndroidEntryPoint
-import dev.msfjarvis.aps.R
-import dev.msfjarvis.aps.data.password.PasswordItem
-import dev.msfjarvis.aps.data.repo.PasswordRepository
-import dev.msfjarvis.aps.databinding.PasswordRecyclerViewBinding
-import dev.msfjarvis.aps.injection.prefs.SettingsPreferences
-import dev.msfjarvis.aps.ui.adapters.PasswordItemRecyclerAdapter
-import dev.msfjarvis.aps.ui.dialogs.BasicBottomSheet
-import dev.msfjarvis.aps.ui.dialogs.ItemCreationBottomSheet
-import dev.msfjarvis.aps.ui.git.base.BaseGitActivity
-import dev.msfjarvis.aps.ui.git.config.GitServerConfigActivity
-import dev.msfjarvis.aps.ui.util.OnOffItemAnimator
-import dev.msfjarvis.aps.util.extensions.base64
-import dev.msfjarvis.aps.util.extensions.getString
-import dev.msfjarvis.aps.util.extensions.sharedPrefs
-import dev.msfjarvis.aps.util.extensions.viewBinding
-import dev.msfjarvis.aps.util.settings.AuthMode
-import dev.msfjarvis.aps.util.settings.GitSettings
-import dev.msfjarvis.aps.util.settings.PasswordSortOrder
-import dev.msfjarvis.aps.util.settings.PreferenceKeys
-import dev.msfjarvis.aps.util.shortcuts.ShortcutHandler
-import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
-import java.io.File
-import javax.inject.Inject
-import kotlinx.coroutines.launch
-import me.zhanghai.android.fastscroll.FastScrollerBuilder
-
-@AndroidEntryPoint
-class PasswordFragment : Fragment(R.layout.password_recycler_view) {
-
- @Inject lateinit var gitSettings: GitSettings
- @Inject lateinit var shortcutHandler: ShortcutHandler
- @Inject @SettingsPreferences lateinit var prefs: SharedPreferences
- private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter
- private lateinit var listener: OnFragmentInteractionListener
- private lateinit var settings: SharedPreferences
-
- private var recyclerViewStateToRestore: Parcelable? = null
- private var actionMode: ActionMode? = null
- private var scrollTarget: File? = null
-
- private val model: SearchableRepositoryViewModel by activityViewModels()
- private val binding by viewBinding(PasswordRecyclerViewBinding::bind)
- private val swipeResult =
- registerForActivityResult(StartActivityForResult()) {
- binding.swipeRefresher.isRefreshing = false
- requireStore().refreshPasswordList()
- }
-
- val currentDir: File
- get() = model.currentDir.value!!
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- settings = requireContext().sharedPrefs
- initializePasswordList()
- binding.fab.setOnClickListener {
- ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET")
- }
- childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) {
- _,
- bundle ->
- when (bundle.getString(ACTION_KEY)) {
- ACTION_FOLDER -> requireStore().createFolder()
- ACTION_PASSWORD -> requireStore().createPassword()
- }
- }
- }
-
- private fun initializePasswordList() {
- val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git")
- val hasGitDir =
- gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true)
- binding.swipeRefresher.setOnRefreshListener {
- if (!hasGitDir) {
- requireStore().refreshPasswordList()
- binding.swipeRefresher.isRefreshing = false
- } else if (!PasswordRepository.isGitRepo()) {
- BasicBottomSheet.Builder(requireContext())
- .setMessageRes(R.string.clone_git_repo)
- .setPositiveButtonClickListener(getString(R.string.clone_button)) {
- swipeResult.launch(GitServerConfigActivity.createCloneIntent(requireContext()))
- }
- .build()
- .show(requireActivity().supportFragmentManager, "NOT_A_GIT_REPO")
- binding.swipeRefresher.isRefreshing = false
- } else {
- // When authentication is set to AuthMode.None then the only git operation we can
- // run is a pull, so automatically fallback to that.
- val operationId =
- when (gitSettings.authMode) {
- AuthMode.None -> BaseGitActivity.GitOp.PULL
- else -> BaseGitActivity.GitOp.SYNC
- }
- requireStore().apply {
- lifecycleScope.launch {
- launchGitOperation(operationId)
- .fold(
- success = {
- binding.swipeRefresher.isRefreshing = false
- refreshPasswordList()
- },
- failure = { err ->
- promptOnErrorHandler(err) { binding.swipeRefresher.isRefreshing = false }
- },
- )
- }
- }
- }
- }
-
- recyclerAdapter =
- PasswordItemRecyclerAdapter(lifecycleScope)
- .onItemClicked { _, item -> listener.onFragmentInteraction(item) }
- .onSelectionChanged { selection ->
- // In order to not interfere with drag selection, we disable the
- // SwipeRefreshLayout
- // once an item is selected.
- binding.swipeRefresher.isEnabled = selection.isEmpty
-
- if (actionMode == null)
- actionMode =
- requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged
-
- if (!selection.isEmpty) {
- actionMode!!.title =
- resources.getQuantityString(
- R.plurals.delete_title,
- selection.size(),
- selection.size()
- )
- actionMode!!.invalidate()
- } else {
- actionMode!!.finish()
- }
- }
- val recyclerView = binding.passRecycler
- recyclerView.apply {
- addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL))
- layoutManager = LinearLayoutManager(requireContext())
- itemAnimator = OnOffItemAnimator()
- adapter = recyclerAdapter
- }
-
- FastScrollerBuilder(recyclerView).build()
- recyclerAdapter.makeSelectable(recyclerView)
- registerForContextMenu(recyclerView)
-
- val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
- model.navigateTo(File(path), pushPreviousLocation = false)
- model.searchResult.observe(viewLifecycleOwner) { result ->
- // Only run animations when the new list is filtered, i.e., the user submitted a search,
- // and not on folder navigations since the latter leads to too many removal animations.
- (recyclerView.itemAnimator as OnOffItemAnimator).isEnabled = result.isFiltered
- recyclerAdapter.submitList(result.passwordItems) {
- when {
- result.isFiltered -> {
- // When the result is filtered, we always scroll to the top since that is
- // where
- // the best fuzzy match appears.
- recyclerView.scrollToPosition(0)
- }
- scrollTarget != null -> {
- scrollTarget?.let {
- recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
- }
- scrollTarget = null
- }
- else -> {
- // When the result is not filtered and there is a saved scroll position for
- // it,
- // we try to restore it.
- recyclerViewStateToRestore?.let {
- recyclerView.layoutManager!!.onRestoreInstanceState(it)
- }
- recyclerViewStateToRestore = null
- }
- }
- }
- }
- }
-
- 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
- animateFab(false)
- 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 {
- val selectedItems = recyclerAdapter.getSelectedItems()
- menu.findItem(R.id.menu_edit_password).isVisible =
- selectedItems.all { it.type == PasswordItem.TYPE_CATEGORY }
- menu.findItem(R.id.menu_pin_password).isVisible =
- selectedItems.size == 1 && selectedItems[0].type == PasswordItem.TYPE_PASSWORD
- return true
- }
-
- // Called when the user selects a contextual menu item
- override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.menu_delete_password -> {
- requireStore().deletePasswords(recyclerAdapter.getSelectedItems())
- // Action picked, so close the CAB
- mode.finish()
- true
- }
- R.id.menu_move_password -> {
- requireStore().movePasswords(recyclerAdapter.getSelectedItems())
- false
- }
- R.id.menu_edit_password -> {
- requireStore().renameCategory(recyclerAdapter.getSelectedItems())
- mode.finish()
- false
- }
- R.id.menu_pin_password -> {
- val passwordItem = recyclerAdapter.getSelectedItems()[0]
- shortcutHandler.addPinnedShortcut(
- passwordItem,
- passwordItem.createAuthEnabledIntent(requireContext())
- )
- false
- }
- else -> false
- }
- }
-
- // Called when the user exits the action mode
- override fun onDestroyActionMode(mode: ActionMode) {
- recyclerAdapter.requireSelectionTracker().clearSelection()
- actionMode = null
- // show the fab
- animateFab(true)
- }
-
- private fun animateFab(show: Boolean) =
- with(binding.fab) {
- val animation =
- AnimationUtils.loadAnimation(context, if (show) R.anim.scale_up else R.anim.scale_down)
- animation.setAnimationListener(
- object : Animation.AnimationListener {
- override fun onAnimationRepeat(animation: Animation?) {}
-
- override fun onAnimationEnd(animation: Animation?) {
- if (!show) visibility = View.GONE
- }
-
- override fun onAnimationStart(animation: Animation?) {
- if (show) visibility = View.VISIBLE
- }
- }
- )
- animate()
- .rotationBy(if (show) -90f else 90f)
- .setStartDelay(if (show) 100 else 0)
- .setDuration(100)
- .start()
- startAnimation(animation)
- }
- }
-
- override fun onResume() {
- super.onResume()
- binding.swipeRefresher.isEnabled = !prefs.getBoolean(PreferenceKeys.DISABLE_SYNC_ACTION, false)
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- runCatching {
- listener =
- object : OnFragmentInteractionListener {
- override fun onFragmentInteraction(item: PasswordItem) {
- if (
- settings.getString(PreferenceKeys.SORT_ORDER) ==
- PasswordSortOrder.RECENTLY_USED.name
- ) {
- // save the time when password was used
- val preferences =
- context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
- preferences.edit {
- putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString())
- }
- }
-
- if (item.type == PasswordItem.TYPE_CATEGORY) {
- navigateTo(item.file)
- } else {
- if (requireArguments().getBoolean("matchWith", false)) {
- requireStore().matchPasswordWithApp(item)
- } else {
- requireStore().decryptPassword(item)
- }
- }
- }
- }
- }
- .onFailure {
- throw ClassCastException("$context must implement OnFragmentInteractionListener")
- }
- }
-
- private fun requireStore() = requireActivity() as PasswordStore
-
- /** Returns true if the back press was handled by the [Fragment]. */
- fun onBackPressedInActivity(): Boolean {
- if (!model.canNavigateBack) return false
- // The RecyclerView state is restored when the asynchronous update operation on the
- // adapter is completed.
- recyclerViewStateToRestore = model.navigateBack()
- if (!model.canNavigateBack) requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(false)
- return true
- }
-
- fun dismissActionMode() {
- actionMode?.finish()
- }
-
- companion object {
-
- const val ITEM_CREATION_REQUEST_KEY = "creation_key"
- const val ACTION_KEY = "action"
- const val ACTION_FOLDER = "folder"
- const val ACTION_PASSWORD = "password"
-
- fun newInstance(args: Bundle): PasswordFragment {
- val fragment = PasswordFragment()
- fragment.arguments = args
- return fragment
- }
- }
-
- fun navigateTo(file: File) {
- requireStore().clearSearch()
- model.navigateTo(
- file,
- recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()
- )
- requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
-
- fun scrollToOnNextRefresh(file: File) {
- scrollTarget = file
- }
-
- interface OnFragmentInteractionListener {
-
- fun onFragmentInteraction(item: PasswordItem)
- }
-}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt
deleted file mode 100644
index 0ee42896..00000000
--- a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt
+++ /dev/null
@@ -1,639 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package dev.msfjarvis.aps.ui.passwords
-
-import android.annotation.SuppressLint
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.view.KeyEvent
-import android.view.Menu
-import android.view.MenuItem
-import android.view.MenuItem.OnActionExpandListener
-import android.view.WindowManager
-import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.activity.viewModels
-import androidx.appcompat.widget.SearchView
-import androidx.appcompat.widget.SearchView.OnQueryTextListener
-import androidx.core.content.edit
-import androidx.fragment.app.FragmentManager
-import androidx.fragment.app.commit
-import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.lifecycleScope
-import com.github.michaelbull.result.fold
-import com.github.michaelbull.result.onFailure
-import com.github.michaelbull.result.runCatching
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.textfield.TextInputEditText
-import dagger.hilt.android.AndroidEntryPoint
-import dev.msfjarvis.aps.R
-import dev.msfjarvis.aps.data.password.PasswordItem
-import dev.msfjarvis.aps.data.repo.PasswordRepository
-import dev.msfjarvis.aps.ui.crypto.BasePgpActivity
-import dev.msfjarvis.aps.ui.crypto.BasePgpActivity.Companion.getLongName
-import dev.msfjarvis.aps.ui.crypto.DecryptActivity
-import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2
-import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity
-import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivityV2
-import dev.msfjarvis.aps.ui.dialogs.FolderCreationDialogFragment
-import dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity
-import dev.msfjarvis.aps.ui.git.base.BaseGitActivity
-import dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity
-import dev.msfjarvis.aps.ui.settings.SettingsActivity
-import dev.msfjarvis.aps.util.autofill.AutofillMatcher
-import dev.msfjarvis.aps.util.extensions.base64
-import dev.msfjarvis.aps.util.extensions.commitChange
-import dev.msfjarvis.aps.util.extensions.contains
-import dev.msfjarvis.aps.util.extensions.getString
-import dev.msfjarvis.aps.util.extensions.isInsideRepository
-import dev.msfjarvis.aps.util.extensions.listFilesRecursively
-import dev.msfjarvis.aps.util.extensions.sharedPrefs
-import dev.msfjarvis.aps.util.features.Feature
-import dev.msfjarvis.aps.util.features.Features
-import dev.msfjarvis.aps.util.settings.AuthMode
-import dev.msfjarvis.aps.util.settings.PreferenceKeys
-import dev.msfjarvis.aps.util.shortcuts.ShortcutHandler
-import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
-import java.io.File
-import java.lang.Character.UnicodeBlock
-import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import logcat.LogPriority.ERROR
-import logcat.LogPriority.INFO
-import logcat.logcat
-
-const val PASSWORD_FRAGMENT_TAG = "PasswordsList"
-
-@AndroidEntryPoint
-class PasswordStore : BaseGitActivity() {
-
- @Inject lateinit var features: Features
- @Inject lateinit var shortcutHandler: ShortcutHandler
- private lateinit var searchItem: MenuItem
- private val settings by lazy { sharedPrefs }
-
- private val model: SearchableRepositoryViewModel by viewModels {
- ViewModelProvider.AndroidViewModelFactory(application)
- }
-
- private val storagePermissionRequest =
- registerForActivityResult(RequestPermission()) { granted ->
- if (granted) checkLocalRepository()
- }
-
- private val directorySelectAction =
- registerForActivityResult(StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- checkLocalRepository()
- }
- }
-
- private val listRefreshAction =
- registerForActivityResult(StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- refreshPasswordList()
- }
- }
-
- private val passwordMoveAction =
- registerForActivityResult(StartActivityForResult()) { result ->
- val intentData = result.data ?: return@registerForActivityResult
- val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files"))
- val target = File(requireNotNull(intentData.getStringExtra("SELECTED_FOLDER_PATH")))
- val repositoryPath = PasswordRepository.getRepositoryDirectory().absolutePath
- if (!target.isDirectory) {
- logcat(ERROR) { "Tried moving passwords to a non-existing folder." }
- return@registerForActivityResult
- }
-
- logcat { "Moving passwords to ${intentData.getStringExtra("SELECTED_FOLDER_PATH")}" }
- logcat { filesToMove.joinToString(", ") }
-
- lifecycleScope.launch(Dispatchers.IO) {
- for (file in filesToMove) {
- val source = File(file)
- if (!source.exists()) {
- logcat(ERROR) { "Tried moving something that appears non-existent." }
- continue
- }
- val destinationFile = File(target.absolutePath + "/" + source.name)
- val basename = source.nameWithoutExtension
- val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename)
- val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
- if (destinationFile.exists()) {
- logcat(ERROR) { "Trying to move a file that already exists." }
- withContext(Dispatchers.Main) {
- MaterialAlertDialogBuilder(this@PasswordStore)
- .setTitle(resources.getString(R.string.password_exists_title))
- .setMessage(
- resources.getString(
- R.string.password_exists_message,
- destinationLongName,
- sourceLongName
- )
- )
- .setPositiveButton(R.string.dialog_ok) { _, _ ->
- launch(Dispatchers.IO) { moveFile(source, destinationFile) }
- }
- .setNegativeButton(R.string.dialog_cancel, null)
- .show()
- }
- } else {
- launch(Dispatchers.IO) { moveFile(source, destinationFile) }
- }
- }
- when (filesToMove.size) {
- 1 -> {
- val source = File(filesToMove[0])
- val basename = source.nameWithoutExtension
- val sourceLongName =
- getLongName(requireNotNull(source.parent), repositoryPath, basename)
- val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
- withContext(Dispatchers.Main) {
- commitChange(
- resources.getString(
- R.string.git_commit_move_text,
- sourceLongName,
- destinationLongName
- ),
- )
- }
- }
- else -> {
- val repoDir = PasswordRepository.getRepositoryDirectory().absolutePath
- val relativePath = getRelativePath("${target.absolutePath}/", repoDir)
- withContext(Dispatchers.Main) {
- commitChange(
- resources.getString(R.string.git_commit_move_multiple_text, relativePath),
- )
- }
- }
- }
- }
- refreshPasswordList()
- getPasswordFragment()?.dismissActionMode()
- }
-
- override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
- // open search view on search key, or Ctr+F
- if (
- (keyCode == KeyEvent.KEYCODE_SEARCH ||
- keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && !searchItem.isActionViewExpanded
- ) {
- searchItem.expandActionView()
- return true
- }
-
- // open search view on any printable character and query for it
- val c = event.unicodeChar.toChar()
- val printable = isPrintable(c)
- if (printable && !searchItem.isActionViewExpanded) {
- searchItem.expandActionView()
- (searchItem.actionView as SearchView).setQuery(c.toString(), true)
- return true
- }
- return super.onKeyDown(keyCode, event)
- }
-
- @SuppressLint("NewApi")
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_pwdstore)
-
- model.currentDir.observe(this) { dir ->
- val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
- supportActionBar?.apply {
- if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
- }
- }
- }
-
- override fun onStart() {
- super.onStart()
- refreshPasswordList()
- }
-
- override fun onResume() {
- super.onResume()
- checkLocalRepository()
- if (settings.getBoolean(PreferenceKeys.SEARCH_ON_START, false) && ::searchItem.isInitialized) {
- if (!searchItem.isActionViewExpanded) {
- searchItem.expandActionView()
- }
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- val menuRes =
- when {
- gitSettings.authMode == AuthMode.None -> R.menu.main_menu_no_auth
- PasswordRepository.isGitRepo() -> R.menu.main_menu_git
- else -> R.menu.main_menu_non_git
- }
- menuInflater.inflate(menuRes, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onPrepareOptionsMenu(menu: Menu): Boolean {
- // Invalidation forces onCreateOptionsMenu to be called again. This is cheap and quick so
- // we can get by without any noticeable difference in performance.
- invalidateOptionsMenu()
- searchItem = menu.findItem(R.id.action_search)
- val searchView = searchItem.actionView as SearchView
- searchView.setOnQueryTextListener(
- object : OnQueryTextListener {
- override fun onQueryTextSubmit(s: String): Boolean {
- searchView.clearFocus()
- return true
- }
-
- override fun onQueryTextChange(s: String): Boolean {
- val filter = s.trim()
- // List the contents of the current directory if the user enters a blank
- // search term.
- if (filter.isEmpty())
- model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false)
- else model.search(filter)
- return true
- }
- }
- )
-
- // When using the support library, the setOnActionExpandListener() method is
- // static and accepts the MenuItem object as an argument
- searchItem.setOnActionExpandListener(
- object : OnActionExpandListener {
- override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
- refreshPasswordList()
- return true
- }
-
- override fun onMenuItemActionExpand(item: MenuItem): Boolean {
- return true
- }
- }
- )
- if (settings.getBoolean(PreferenceKeys.SEARCH_ON_START, false)) {
- searchItem.expandActionView()
- }
- return super.onPrepareOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- val id = item.itemId
- val initBefore =
- MaterialAlertDialogBuilder(this)
- .setMessage(resources.getString(R.string.creation_dialog_text))
- .setPositiveButton(resources.getString(R.string.dialog_ok), null)
- when (id) {
- R.id.user_pref -> {
- runCatching { startActivity(Intent(this, SettingsActivity::class.java)) }
- .onFailure { e -> e.printStackTrace() }
- }
- R.id.git_push -> {
- if (!PasswordRepository.isInitialized) {
- initBefore.show()
- } else {
- runGitOperation(GitOp.PUSH)
- }
- }
- R.id.git_pull -> {
- if (!PasswordRepository.isInitialized) {
- initBefore.show()
- } else {
- runGitOperation(GitOp.PULL)
- }
- }
- R.id.git_sync -> {
- if (!PasswordRepository.isInitialized) {
- initBefore.show()
- } else {
- runGitOperation(GitOp.SYNC)
- }
- }
- R.id.refresh -> refreshPasswordList()
- android.R.id.home -> onBackPressed()
- else -> return super.onOptionsItemSelected(item)
- }
- return true
- }
-
- override fun onBackPressed() {
- if (getPasswordFragment()?.onBackPressedInActivity() != true) super.onBackPressed()
- }
-
- private fun getPasswordFragment(): PasswordFragment? {
- return supportFragmentManager.findFragmentByTag(PASSWORD_FRAGMENT_TAG) as? PasswordFragment
- }
-
- fun clearSearch() {
- if (searchItem.isActionViewExpanded) searchItem.collapseActionView()
- }
-
- private fun runGitOperation(operation: GitOp) =
- lifecycleScope.launch {
- launchGitOperation(operation)
- .fold(
- success = { refreshPasswordList() },
- failure = { promptOnErrorHandler(it) },
- )
- }
-
- private fun checkLocalRepository() {
- PasswordRepository.initialize()
- checkLocalRepository(PasswordRepository.getRepositoryDirectory())
- }
-
- private fun checkLocalRepository(localDir: File?) {
- if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) {
- logcat { "Check, dir: ${localDir.absolutePath}" }
- // do not push the fragment if we already have it
- if (
- getPasswordFragment() == null || settings.getBoolean(PreferenceKeys.REPO_CHANGED, false)
- ) {
- settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) }
- val args = Bundle()
- args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
-
- // if the activity was started from the autofill settings, the
- // intent is to match a clicked pwd with app. pass this to fragment
- if (intent.getBooleanExtra("matchWith", false)) {
- args.putBoolean("matchWith", true)
- }
- supportActionBar?.apply {
- show()
- setDisplayHomeAsUpEnabled(false)
- }
- supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
- supportFragmentManager.commit {
- replace(R.id.main_layout, PasswordFragment.newInstance(args), PASSWORD_FRAGMENT_TAG)
- }
- }
- } else {
- startActivity(Intent(this, OnboardingActivity::class.java))
- }
- }
-
- private fun getRelativePath(fullPath: String, repositoryPath: String): String {
- return fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
- }
-
- fun decryptPassword(item: PasswordItem) {
- val authDecryptIntent = item.createAuthEnabledIntent(this)
- val decryptIntent =
- (authDecryptIntent.clone() as Intent).setComponent(
- ComponentName(
- this,
- if (features.isEnabled(Feature.EnablePGPainlessBackend)) {
- DecryptActivityV2::class.java
- } else {
- DecryptActivity::class.java
- }
- )
- )
-
- startActivity(decryptIntent)
-
- // Adds shortcut
- shortcutHandler.addDynamicShortcut(item, authDecryptIntent)
- }
-
- private fun validateState(): Boolean {
- if (!PasswordRepository.isInitialized) {
- MaterialAlertDialogBuilder(this)
- .setMessage(resources.getString(R.string.creation_dialog_text))
- .setPositiveButton(resources.getString(R.string.dialog_ok), null)
- .show()
- return false
- }
- return true
- }
-
- fun createPassword() {
- if (!validateState()) return
- val currentDir = currentDir
- logcat(INFO) { "Adding file to : ${currentDir.absolutePath}" }
- val creationActivity =
- if (features.isEnabled(Feature.EnablePGPainlessBackend))
- PasswordCreationActivityV2::class.java
- else PasswordCreationActivity::class.java
- val intent = Intent(this, creationActivity)
- intent.putExtra(BasePgpActivity.EXTRA_FILE_PATH, currentDir.absolutePath)
- intent.putExtra(
- BasePgpActivity.EXTRA_REPO_PATH,
- PasswordRepository.getRepositoryDirectory().absolutePath
- )
- listRefreshAction.launch(intent)
- }
-
- fun createFolder() {
- if (!validateState()) return
- FolderCreationDialogFragment.newInstance(currentDir.path).show(supportFragmentManager, null)
- }
-
- fun deletePasswords(selectedItems: List<PasswordItem>) {
- var size = 0
- selectedItems.forEach {
- if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size
- }
- if (size == 0) {
- selectedItems.map { item -> item.file.deleteRecursively() }
- refreshPasswordList()
- return
- }
- MaterialAlertDialogBuilder(this)
- .setMessage(resources.getQuantityString(R.plurals.delete_dialog_text, size, size))
- .setPositiveButton(resources.getString(R.string.dialog_yes)) { _, _ ->
- val filesToDelete = arrayListOf<File>()
- selectedItems.forEach { item ->
- if (item.file.isDirectory) filesToDelete.addAll(item.file.listFilesRecursively())
- else filesToDelete.add(item.file)
- }
- selectedItems.map { item -> item.file.deleteRecursively() }
- refreshPasswordList()
- AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete)
- val fmt =
- selectedItems.joinToString(separator = ", ") { item ->
- item.file.toRelativeString(PasswordRepository.getRepositoryDirectory())
- }
- lifecycleScope.launch {
- commitChange(
- resources.getString(R.string.git_commit_remove_text, fmt),
- )
- }
- }
- .setNegativeButton(resources.getString(R.string.dialog_no), null)
- .show()
- }
-
- fun movePasswords(values: List<PasswordItem>) {
- val intent = Intent(this, SelectFolderActivity::class.java)
- val fileLocations = values.map { it.file.absolutePath }.toTypedArray()
- intent.putExtra("Files", fileLocations)
- passwordMoveAction.launch(intent)
- }
-
- enum class CategoryRenameError(val resource: Int) {
- None(0),
- EmptyField(R.string.message_category_error_empty_field),
- CategoryExists(R.string.message_category_error_category_exists),
- DestinationOutsideRepo(R.string.message_error_destination_outside_repo),
- }
-
- /**
- * Prompt the user with a new category name to assign, if the new category forms/leads a path
- * (i.e. contains "/"), intermediate directories will be created and new category will be placed
- * inside.
- *
- * @param oldCategory The category to change its name
- * @param error Determines whether to show an error to the user in the alert dialog, this error
- * may be due to the new category the user entered already exists or the field was empty or the
- * destination path is outside the repository
- *
- * @see [CategoryRenameError]
- * @see [isInsideRepository]
- */
- private fun renameCategory(
- oldCategory: PasswordItem,
- error: CategoryRenameError = CategoryRenameError.None
- ) {
- val view = layoutInflater.inflate(R.layout.folder_dialog_fragment, null)
- val newCategoryEditText = view.findViewById<TextInputEditText>(R.id.folder_name_text)
-
- if (error != CategoryRenameError.None) {
- newCategoryEditText.error = getString(error.resource)
- }
-
- val dialog =
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.title_rename_folder)
- .setView(view)
- .setMessage(getString(R.string.message_rename_folder, oldCategory.name))
- .setPositiveButton(R.string.dialog_ok) { _, _ ->
- val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}")
- when {
- newCategoryEditText.text.isNullOrBlank() ->
- renameCategory(oldCategory, CategoryRenameError.EmptyField)
- newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists)
- !newCategory.isInsideRepository() ->
- renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
- else ->
- lifecycleScope.launch(Dispatchers.IO) {
- moveFile(oldCategory.file, newCategory)
-
- // associate the new category with the last category's timestamp in
- // history
- val preference =
- getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
- val timestamp = preference.getString(oldCategory.file.absolutePath.base64())
- if (timestamp != null) {
- preference.edit {
- remove(oldCategory.file.absolutePath.base64())
- putString(newCategory.absolutePath.base64(), timestamp)
- }
- }
-
- withContext(Dispatchers.Main) {
- commitChange(
- resources.getString(
- R.string.git_commit_move_text,
- oldCategory.name,
- newCategory.name
- ),
- )
- }
- }
- }
- }
- .setNegativeButton(R.string.dialog_skip, null)
- .create()
-
- dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
- dialog.show()
- }
-
- fun renameCategory(categories: List<PasswordItem>) {
- for (oldCategory in categories) {
- renameCategory(oldCategory)
- }
- }
-
- /**
- * Refreshes the password list by re-executing the last navigation or search action, preserving
- * the navigation stack and scroll position. If the current directory no longer exists, navigation
- * is reset to the repository root. If the optional [target] argument is provided, it will be
- * entered if it is a directory or scrolled into view if it is a file (both inside the current
- * directory).
- */
- fun refreshPasswordList(target: File? = null) {
- val plist = getPasswordFragment()
- if (target?.isDirectory == true && model.currentDir.value?.contains(target) == true) {
- plist?.navigateTo(target)
- } else if (target?.isFile == true && model.currentDir.value?.contains(target) == true) {
- // Creating new passwords is handled by an activity, so we will refresh in onStart.
- plist?.scrollToOnNextRefresh(target)
- } else if (model.currentDir.value?.isDirectory == true) {
- model.forceRefresh()
- } else {
- model.reset()
- supportActionBar?.setDisplayHomeAsUpEnabled(false)
- }
- }
-
- private val currentDir: File
- get() = getPasswordFragment()?.currentDir ?: PasswordRepository.getRepositoryDirectory()
-
- private suspend fun moveFile(source: File, destinationFile: File) {
- val sourceDestinationMap =
- if (source.isDirectory) {
- destinationFile.mkdirs()
- // Recursively list all files (not directories) below `source`, then
- // obtain the corresponding target file by resolving the relative path
- // starting at the destination folder.
- source.listFilesRecursively().associateWith {
- destinationFile.resolve(it.relativeTo(source))
- }
- } else {
- mapOf(source to destinationFile)
- }
- if (!source.renameTo(destinationFile)) {
- logcat(ERROR) { "Something went wrong while moving $source to $destinationFile." }
- withContext(Dispatchers.Main) {
- MaterialAlertDialogBuilder(this@PasswordStore)
- .setTitle(R.string.password_move_error_title)
- .setMessage(getString(R.string.password_move_error_message, source, destinationFile))
- .setCancelable(true)
- .setPositiveButton(android.R.string.ok, null)
- .show()
- }
- } else {
- AutofillMatcher.updateMatches(this, sourceDestinationMap)
- }
- }
-
- fun matchPasswordWithApp(item: PasswordItem) {
- val path =
- item.file.absolutePath
- .replace(PasswordRepository.getRepositoryDirectory().toString() + "/", "")
- .replace(".gpg", "")
- val data = Intent()
- data.putExtra("path", path)
- setResult(RESULT_OK, data)
- finish()
- }
-
- companion object {
-
- const val REQUEST_ARG_PATH = "PATH"
- private fun isPrintable(c: Char): Boolean {
- val block = UnicodeBlock.of(c)
- return (!Character.isISOControl(c) && block != null && block !== UnicodeBlock.SPECIALS)
- }
- }
-}