aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2024-05-31 17:30:59 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2024-07-28 23:00:16 +0530
commit36b45f90f90e76ca2611e9cfcca9a6ee440683b3 (patch)
treedef33851faa9c76fcfc4a6e06f77cffee6475cb4 /app
parent7010ee85b4bdfa0c8120f15597a9471151576b2c (diff)
refactor: migrate to NIO
Diffstat (limited to 'app')
-rw-r--r--app/src/main/java/app/passwordstore/data/password/PasswordItem.kt40
-rw-r--r--app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt81
-rw-r--r--app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt8
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt52
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt2
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt10
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt23
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt6
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt5
-rw-r--r--app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt16
-rw-r--r--app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt5
-rw-r--r--app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt11
-rw-r--r--app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt15
-rw-r--r--app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt10
-rw-r--r--app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt4
-rw-r--r--app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt26
-rw-r--r--app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt129
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt6
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt6
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt6
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt37
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt33
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt5
-rw-r--r--app/src/main/java/app/passwordstore/util/extensions/Extensions.kt28
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt2
-rw-r--r--app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt23
-rw-r--r--app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt11
-rw-r--r--app/src/main/java/app/passwordstore/util/services/PasswordExportService.kt5
-rw-r--r--app/src/main/java/app/passwordstore/util/settings/GitSettings.kt8
-rw-r--r--app/src/main/java/app/passwordstore/util/settings/Migrations.kt5
-rw-r--r--app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt5
-rw-r--r--app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt68
32 files changed, 374 insertions, 317 deletions
diff --git a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt
index 330c1e40..22f14a08 100644
--- a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt
+++ b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt
@@ -9,19 +9,25 @@ import android.content.Intent
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.crypto.BasePGPActivity
import app.passwordstore.ui.main.LaunchActivity
-import java.io.File
+import java.nio.file.Path
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.pathString
+import kotlin.io.path.relativeTo
data class PasswordItem(
- val name: String,
val parent: PasswordItem? = null,
val type: Char,
- val file: File,
- val rootDir: File,
+ val file: Path,
+ val rootDir: Path,
) : Comparable<PasswordItem> {
- val fullPathToParent = file.absolutePath.replace(rootDir.absolutePath, "").replace(file.name, "")
+ val name = file.nameWithoutExtension
- val longName = BasePGPActivity.getLongName(fullPathToParent, rootDir.absolutePath, toString())
+ val fullPathToParent = file.relativeTo(rootDir).parent.pathString
+
+ val longName =
+ BasePGPActivity.getLongName(fullPathToParent, rootDir.absolutePathString(), toString())
override fun equals(other: Any?): Boolean {
return (other is PasswordItem) && (other.file == file)
@@ -32,7 +38,7 @@ data class PasswordItem(
}
override fun toString(): String {
- return name.replace("\\.gpg$".toRegex(), "")
+ return name
}
override fun hashCode(): Int {
@@ -43,8 +49,8 @@ data class PasswordItem(
fun createAuthEnabledIntent(context: Context): Intent {
val intent = Intent(context, LaunchActivity::class.java)
intent.putExtra("NAME", toString())
- intent.putExtra("FILE_PATH", file.absolutePath)
- intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath)
+ intent.putExtra("FILE_PATH", file.absolutePathString())
+ intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePathString())
intent.action = LaunchActivity.ACTION_DECRYPT_PASS
return intent
}
@@ -55,23 +61,23 @@ data class PasswordItem(
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)
+ fun newCategory(path: Path, parent: PasswordItem, rootDir: Path): PasswordItem {
+ return PasswordItem(parent, TYPE_CATEGORY, path, rootDir)
}
@JvmStatic
- fun newCategory(name: String, file: File, rootDir: File): PasswordItem {
- return PasswordItem(name, null, TYPE_CATEGORY, file, rootDir)
+ fun newCategory(path: Path, rootDir: Path): PasswordItem {
+ return PasswordItem(null, TYPE_CATEGORY, path, rootDir)
}
@JvmStatic
- fun newPassword(name: String, file: File, parent: PasswordItem, rootDir: File): PasswordItem {
- return PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir)
+ fun newPassword(path: Path, parent: PasswordItem, rootDir: Path): PasswordItem {
+ return PasswordItem(parent, TYPE_PASSWORD, path, rootDir)
}
@JvmStatic
- fun newPassword(name: String, file: File, rootDir: File): PasswordItem {
- return PasswordItem(name, null, TYPE_PASSWORD, file, rootDir)
+ fun newPassword(path: Path, rootDir: Path): PasswordItem {
+ return PasswordItem(null, TYPE_PASSWORD, path, rootDir)
}
}
}
diff --git a/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt b/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt
index ed18f87a..53dfc03a 100644
--- a/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt
+++ b/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt
@@ -6,15 +6,19 @@ package app.passwordstore.data.repo
import androidx.core.content.edit
import app.passwordstore.Application
-import app.passwordstore.data.password.PasswordItem
import app.passwordstore.util.extensions.sharedPrefs
import app.passwordstore.util.extensions.unsafeLazy
-import app.passwordstore.util.settings.PasswordSortOrder
import app.passwordstore.util.settings.PreferenceKeys
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
-import java.io.File
+import java.nio.file.Path
+import kotlin.io.path.ExperimentalPathApi
+import kotlin.io.path.PathWalkOption
+import kotlin.io.path.deleteRecursively
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+import kotlin.io.path.walk
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.Repository
@@ -23,12 +27,13 @@ import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.transport.RemoteConfig
import org.eclipse.jgit.transport.URIish
+@OptIn(ExperimentalPathApi::class)
object PasswordRepository {
var repository: Repository? = null
private val settings by unsafeLazy { Application.instance.sharedPrefs }
private val filesDir
- get() = Application.instance.filesDir
+ get() = Application.instance.filesDir.toPath()
val isInitialized: Boolean
get() = repository != null
@@ -41,19 +46,20 @@ object PasswordRepository {
* Takes in a [repositoryDir] to initialize a Git repository with, and assigns it to [repository]
* as static state.
*/
- private fun initializeRepository(repositoryDir: File) {
+ private fun initializeRepository(repositoryDir: Path) {
val builder = FileRepositoryBuilder()
repository =
- runCatching { builder.setGitDir(repositoryDir).build() }
+ runCatching { builder.setGitDir(repositoryDir.toFile()).build() }
.getOrElse { e ->
e.printStackTrace()
null
}
}
- fun createRepository(repositoryDir: File) {
- repositoryDir.delete()
- repository = Git.init().setDirectory(repositoryDir).call().repository
+ @OptIn(ExperimentalPathApi::class)
+ fun createRepository(repositoryDir: Path) {
+ repositoryDir.deleteRecursively()
+ repository = Git.init().setDirectory(repositoryDir.toFile()).call().repository
}
// TODO add multiple remotes support for pull/push
@@ -106,8 +112,8 @@ object PasswordRepository {
repository = null
}
- fun getRepositoryDirectory(): File {
- return File(filesDir.toString(), "/store")
+ fun getRepositoryDirectory(): Path {
+ return filesDir.resolve("store")
}
fun initialize(): Repository? {
@@ -116,8 +122,8 @@ object PasswordRepository {
settings.edit {
if (
!dir.exists() ||
- !dir.isDirectory ||
- requireNotNull(dir.listFiles()) { "Failed to list files in ${dir.path}" }.isEmpty()
+ !dir.isDirectory() ||
+ dir.walk(PathWalkOption.INCLUDE_DIRECTORIES).toList().isEmpty()
) {
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
} else {
@@ -141,53 +147,4 @@ object PasswordRepository {
null
}
}
-
- /**
- * Gets the .gpg files in a directory
- *
- * @param path the directory path
- * @return the list of gpg files in that directory
- */
- private fun getFilesList(path: File): ArrayList<File> {
- if (!path.exists()) return ArrayList()
- val files =
- (path.listFiles { file -> file.isDirectory || file.extension == "gpg" } ?: emptyArray())
- .toList()
- val items = ArrayList<File>()
- items.addAll(files)
- return items
- }
-
- /**
- * Gets the passwords (PasswordItem) in a directory
- *
- * @param path the directory path
- * @return a list of password items
- */
- fun getPasswords(
- path: File,
- rootDir: File,
- sortOrder: PasswordSortOrder,
- ): ArrayList<PasswordItem> {
- // We need to recover the passwords then parse the files
- val passList = getFilesList(path).also { it.sortBy { f -> f.name } }
- val passwordList = ArrayList<PasswordItem>()
- val showHidden = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
-
- if (passList.size == 0) return passwordList
- if (!showHidden) {
- passList.filter { !it.isHidden }.toCollection(passList.apply { clear() })
- }
- passList.forEach { file ->
- passwordList.add(
- if (file.isFile) {
- PasswordItem.newPassword(file.name, file, rootDir)
- } else {
- PasswordItem.newCategory(file.name, file, rootDir)
- }
- )
- }
- passwordList.sortWith(sortOrder.comparator)
- return passwordList
- }
}
diff --git a/app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt b/app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt
index f7d5cf9a..3e2993b4 100644
--- a/app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt
+++ b/app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt
@@ -18,6 +18,9 @@ import app.passwordstore.data.password.PasswordItem
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.viewmodel.SearchableRepositoryAdapter
import app.passwordstore.util.viewmodel.stableId
+import kotlin.io.path.extension
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
@@ -71,7 +74,10 @@ open class PasswordItemRecyclerAdapter(
folderIndicator.visibility = View.VISIBLE
val count =
withContext(dispatcherProvider.io()) {
- item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0
+ item.file
+ .listDirectoryEntries()
+ .filter { it.isDirectory() || it.extension == "gpg" }
+ .size
}
childCount.visibility = if (count > 0) View.VISIBLE else View.GONE
childCount.text = "$count"
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
index 9601d75e..b575bdad 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
@@ -39,8 +39,11 @@ import com.github.michaelbull.result.onSuccess
import com.github.michaelbull.result.runCatching
import dagger.hilt.android.AndroidEntryPoint
import java.io.ByteArrayOutputStream
-import java.io.File
+import java.nio.file.Path
+import java.nio.file.Paths
import javax.inject.Inject
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.readBytes
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
@@ -59,12 +62,14 @@ class AutofillDecryptActivity : BasePGPActivity() {
override fun onStart() {
super.onStart()
val filePath =
- intent?.getStringExtra(EXTRA_FILE_PATH)
- ?: run {
- logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" }
- finish()
- return
- }
+ Paths.get(
+ intent?.getStringExtra(EXTRA_FILE_PATH)
+ ?: run {
+ logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" }
+ finish()
+ return
+ }
+ )
val clientState =
intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE)
?: run {
@@ -93,14 +98,17 @@ class AutofillDecryptActivity : BasePGPActivity() {
}
private fun decrypt(
- filePath: String,
+ filePath: Path,
clientState: Bundle,
action: AutofillAction,
authResult: BiometricResult,
) {
val gpgIdentifiers =
getPGPIdentifiers(
- getParentPath(filePath, PasswordRepository.getRepositoryDirectory().toString())
+ getParentPath(
+ filePath.absolutePathString(),
+ PasswordRepository.getRepositoryDirectory().toString(),
+ )
) ?: return
lifecycleScope.launch(dispatcherProvider.main()) {
when (authResult) {
@@ -126,13 +134,7 @@ class AutofillDecryptActivity : BasePGPActivity() {
gpgIdentifiers.first(),
)
if (cachedPassphrase != null) {
- decryptWithPassphrase(
- File(filePath),
- gpgIdentifiers,
- clientState,
- action,
- cachedPassphrase,
- )
+ decryptWithPassphrase(filePath, gpgIdentifiers, clientState, action, cachedPassphrase)
} else {
askPassphrase(filePath, gpgIdentifiers, clientState, action)
}
@@ -142,13 +144,13 @@ class AutofillDecryptActivity : BasePGPActivity() {
}
private suspend fun askPassphrase(
- filePath: String,
+ filePath: Path,
identifiers: List<PGPIdentifier>,
clientState: Bundle,
action: AutofillAction,
) {
if (!repository.isPasswordProtected(identifiers)) {
- decryptWithPassphrase(File(filePath), identifiers, clientState, action, password = "")
+ decryptWithPassphrase(filePath, identifiers, clientState, action, password = "")
return
}
val dialog =
@@ -162,14 +164,14 @@ class AutofillDecryptActivity : BasePGPActivity() {
val value = bundle.getString(PasswordDialog.PASSWORD_PHRASE_KEY)!!
clearCache = bundle.getBoolean(PasswordDialog.PASSWORD_CLEAR_KEY)
lifecycleScope.launch(dispatcherProvider.main()) {
- decryptWithPassphrase(File(filePath), identifiers, clientState, action, value)
+ decryptWithPassphrase(filePath, identifiers, clientState, action, value)
}
}
}
}
private suspend fun decryptWithPassphrase(
- filePath: File,
+ filePath: Path,
identifiers: List<PGPIdentifier>,
clientState: Bundle,
action: AutofillAction,
@@ -199,7 +201,7 @@ class AutofillDecryptActivity : BasePGPActivity() {
}
private suspend fun decryptCredential(
- file: File,
+ file: Path,
password: String,
identifiers: List<PGPIdentifier>,
): Credentials? {
@@ -240,19 +242,19 @@ class AutofillDecryptActivity : BasePGPActivity() {
private var decryptFileRequestCode = 1
- fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent {
+ fun makeDecryptFileIntent(file: Path, forwardedExtras: Bundle, context: Context): Intent {
return Intent(context, AutofillDecryptActivity::class.java).apply {
putExtras(forwardedExtras)
putExtra(EXTRA_SEARCH_ACTION, true)
- putExtra(EXTRA_FILE_PATH, file.absolutePath)
+ putExtra(EXTRA_FILE_PATH, file.absolutePathString())
}
}
- fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender {
+ fun makeDecryptFileIntentSender(file: Path, context: Context): IntentSender {
val intent =
Intent(context, AutofillDecryptActivity::class.java).apply {
putExtra(EXTRA_SEARCH_ACTION, false)
- putExtra(EXTRA_FILE_PATH, file.absolutePath)
+ putExtra(EXTRA_FILE_PATH, file.absolutePathString())
}
return PendingIntent.getActivity(
context,
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
index 70dcd78d..d6efab11 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
@@ -38,7 +38,7 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
import com.github.androidpasswordstore.autofillparser.FormOrigin
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
-import kotlinx.coroutines.flow.collect
+import kotlin.io.path.relativeTo
import kotlinx.coroutines.launch
import logcat.LogPriority.ERROR
import logcat.logcat
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
index c645ea04..dc54b78e 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
@@ -23,7 +23,8 @@ import com.github.androidpasswordstore.autofillparser.AutofillAction
import com.github.androidpasswordstore.autofillparser.Credentials
import com.github.androidpasswordstore.autofillparser.FormOrigin
import dagger.hilt.android.AndroidEntryPoint
-import java.io.File
+import java.nio.file.Paths
+import kotlin.io.path.absolutePathString
import logcat.LogPriority.ERROR
import logcat.logcat
@@ -109,8 +110,9 @@ class AutofillSaveActivity : AppCompatActivity() {
Intent(this, PasswordCreationActivity::class.java).apply {
putExtras(
bundleOf(
- "REPO_PATH" to repo.absolutePath,
- "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
+ "REPO_PATH" to repo.absolutePathString(),
+ "FILE_PATH" to
+ repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePathString(),
PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to
@@ -122,7 +124,7 @@ class AutofillSaveActivity : AppCompatActivity() {
val data = result.data
if (result.resultCode == RESULT_OK && data != null) {
val createdPath = data.getStringExtra("CREATED_FILE")!!
- formOrigin?.let { AutofillMatcher.addMatchFor(this, it, File(createdPath)) }
+ formOrigin?.let { AutofillMatcher.addMatchFor(this, it, Paths.get(createdPath)) }
val password = data.getStringExtra("PASSWORD")
val resultIntent =
if (password != null) {
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
index a0cf6f7c..dbeb86e4 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
@@ -34,8 +34,15 @@ import app.passwordstore.util.settings.PreferenceKeys
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
-import java.io.File
+import java.nio.file.Path
+import java.nio.file.Paths
import javax.inject.Inject
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.createFile
+import kotlin.io.path.exists
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.readLines
+import kotlin.io.path.readText
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -54,7 +61,7 @@ open class BasePGPActivity : AppCompatActivity() {
*
* Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org
*/
- val name: String by unsafeLazy { File(fullPath).nameWithoutExtension }
+ val name: String by unsafeLazy { Paths.get(fullPath).nameWithoutExtension }
/** Action to invoke if [keyImportAction] succeeds. */
private var onKeyImport: (() -> Unit)? = null
@@ -155,8 +162,8 @@ open class BasePGPActivity : AppCompatActivity() {
fun getPGPIdentifiers(subDir: String): List<PGPIdentifier>? {
val repoRoot = PasswordRepository.getRepositoryDirectory()
val gpgIdentifierFile =
- File(repoRoot, subDir).findTillRoot(".gpg-id", repoRoot)
- ?: File(repoRoot, ".gpg-id").apply { createNewFile() }
+ repoRoot.resolve(subDir).findTillRoot(".gpg-id", repoRoot)
+ ?: repoRoot.resolve(".gpg-id").createFile()
val gpgIdentifiers =
gpgIdentifierFile
.readLines()
@@ -185,15 +192,13 @@ open class BasePGPActivity : AppCompatActivity() {
return gpgIdentifiers
}
- @Suppress("ReturnCount")
- private fun File.findTillRoot(fileName: String, rootPath: File): File? {
- val gpgFile = File(this, fileName)
+ private fun Path.findTillRoot(fileName: String, rootPath: Path): Path? {
+ val gpgFile = this.resolve(fileName)
if (gpgFile.exists()) return gpgFile
- if (this.absolutePath == rootPath.absolutePath) {
+ if (this.absolutePathString() == rootPath.absolutePathString()) {
return null
}
- val parent = parentFile
return if (parent != null && parent.exists()) {
parent.findTillRoot(fileName, rootPath)
} else {
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
index c25b5d1a..e3ba7962 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
@@ -33,8 +33,9 @@ import app.passwordstore.util.settings.Constants
import app.passwordstore.util.settings.PreferenceKeys
import dagger.hilt.android.AndroidEntryPoint
import java.io.ByteArrayOutputStream
-import java.io.File
+import java.nio.file.Paths
import javax.inject.Inject
+import kotlin.io.path.readBytes
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
@@ -233,7 +234,8 @@ class DecryptActivity : BasePGPActivity() {
authResult: BiometricResult,
onSuccess: suspend () -> Unit = {},
) {
- val message = withContext(dispatcherProvider.io()) { File(fullPath).readBytes().inputStream() }
+ val message =
+ withContext(dispatcherProvider.io()) { Paths.get(fullPath).readBytes().inputStream() }
val outputStream = ByteArrayOutputStream()
val result = repository.decrypt(passphrase, identifiers, message, outputStream)
if (result.isOk) {
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
index 525d9f95..37a09e2f 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
@@ -387,7 +387,7 @@ class PasswordCreationActivity : BasePGPActivity() {
return@runCatching
}
- if (!passwordFile.toFile().isInsideRepository()) {
+ if (!passwordFile.isInsideRepository()) {
snackbar(message = getString(R.string.message_error_destination_outside_repo))
return@runCatching
}
@@ -414,8 +414,7 @@ class PasswordCreationActivity : BasePGPActivity() {
val directoryStructure = AutofillPreferences.directoryStructure(applicationContext)
val entry = passwordEntryFactory.create(content.encodeToByteArray())
returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
- val username =
- entry.username ?: directoryStructure.getUsernameFor(passwordFile.toFile())
+ val username = entry.username ?: directoryStructure.getUsernameFor(passwordFile)
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
}
diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt
index 3923a997..77567f4d 100644
--- a/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt
+++ b/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt
@@ -15,11 +15,15 @@ import app.passwordstore.ui.passwords.PasswordStore
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
-import java.io.File
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.io.path.createDirectories
+import kotlin.io.path.isDirectory
+import kotlin.io.path.isRegularFile
class FolderCreationDialogFragment : DialogFragment() {
- private lateinit var newFolder: File
+ private lateinit var newFolder: Path
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
@@ -41,15 +45,15 @@ class FolderCreationDialogFragment : DialogFragment() {
val dialog = requireDialog()
val folderNameView = dialog.findViewById<TextInputEditText>(R.id.folder_name_text)
val folderNameViewContainer = dialog.findViewById<TextInputLayout>(R.id.folder_name_container)
- newFolder = File("$currentDir/${folderNameView.text}")
+ newFolder = Paths.get("$currentDir/${folderNameView.text}")
folderNameViewContainer.error =
when {
- newFolder.isFile -> getString(R.string.folder_creation_err_file_exists)
- newFolder.isDirectory -> getString(R.string.folder_creation_err_folder_exists)
+ newFolder.isRegularFile() -> getString(R.string.folder_creation_err_file_exists)
+ newFolder.isDirectory() -> getString(R.string.folder_creation_err_folder_exists)
else -> null
}
if (folderNameViewContainer.error != null) return
- newFolder.mkdirs()
+ newFolder.createDirectories()
(requireActivity() as PasswordStore).refreshPasswordList(newFolder)
// TODO(msfjarvis): Restore this functionality
/*
diff --git a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt
index 9d4531f4..5410543b 100644
--- a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt
@@ -15,6 +15,7 @@ import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.passwords.PASSWORD_FRAGMENT_TAG
import app.passwordstore.ui.passwords.PasswordStore
import dagger.hilt.android.AndroidEntryPoint
+import kotlin.io.path.absolutePathString
@AndroidEntryPoint
class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
@@ -28,7 +29,7 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
val args = Bundle()
args.putString(
PasswordStore.REQUEST_ARG_PATH,
- PasswordRepository.getRepositoryDirectory().absolutePath,
+ PasswordRepository.getRepositoryDirectory().absolutePathString(),
)
passwordList.arguments = args
@@ -60,7 +61,7 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
}
private fun selectFolder() {
- intent.putExtra("SELECTED_FOLDER_PATH", passwordList.currentDir.absolutePath)
+ intent.putExtra("SELECTED_FOLDER_PATH", passwordList.currentDir.absolutePathString())
setResult(RESULT_OK, intent)
finish()
}
diff --git a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
index 9a8080a4..9cca7bbd 100644
--- a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
+++ b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
@@ -25,7 +25,8 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import dagger.hilt.android.AndroidEntryPoint
-import java.io.File
+import java.nio.file.Path
+import java.nio.file.Paths
import javax.inject.Inject
import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
@@ -60,7 +61,11 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) {
"Cannot navigate if ${PasswordStore.REQUEST_ARG_PATH} is not provided"
}
- model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false)
+ model.navigateTo(
+ Paths.get(path),
+ listMode = ListMode.DirectoriesOnly,
+ pushPreviousLocation = false,
+ )
lifecycleScope.launch {
model.searchResult.flowWithLifecycle(lifecycle).collect { result ->
recyclerAdapter.submitList(result.passwordItems)
@@ -88,7 +93,7 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
}
}
- val currentDir: File
+ val currentDir: Path
get() = model.currentDir.value
interface OnFragmentInteractionListener {
diff --git a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt b/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt
index e292cf4e..596c79d4 100644
--- a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt
@@ -30,6 +30,12 @@ import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
+import kotlin.io.path.ExperimentalPathApi
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteRecursively
+import kotlin.io.path.exists
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.name
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
@@ -220,17 +226,16 @@ class GitServerConfigActivity : BaseGitActivity() {
}
/** Clones the repository, the directory exists, deletes it */
+ @OptIn(ExperimentalPathApi::class)
private fun cloneRepository() {
val localDir =
requireNotNull(PasswordRepository.getRepositoryDirectory()) {
"Repository directory must be set before cloning"
}
- val localDirFiles = localDir.listFiles() ?: emptyArray()
+ val localDirFiles = if (localDir.exists()) localDir.listDirectoryEntries() else listOf()
// Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
if (
- localDir.exists() &&
- localDirFiles.isNotEmpty() &&
- !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
+ localDirFiles.isNotEmpty() && !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_delete_title)
@@ -246,7 +251,7 @@ class GitServerConfigActivity : BaseGitActivity() {
)
withContext(dispatcherProvider.io()) {
localDir.deleteRecursively()
- localDir.mkdirs()
+ localDir.createDirectories()
}
snackbar.dismiss()
launchGitOperation(GitOp.CLONE)
diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt
index 6702afc0..39a19f0d 100644
--- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt
+++ b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt
@@ -23,6 +23,10 @@ import app.passwordstore.util.extensions.viewBinding
import app.passwordstore.util.settings.PreferenceKeys
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
+import java.nio.file.LinkOption
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.notExists
import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
@@ -55,7 +59,9 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
private fun createRepository() {
val localDir = PasswordRepository.getRepositoryDirectory()
runCatching {
- check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" }
+ if (localDir.notExists(LinkOption.NOFOLLOW_LINKS)) {
+ localDir.createDirectories()
+ }
PasswordRepository.createRepository(localDir)
if (!PasswordRepository.isInitialized) {
PasswordRepository.initialize()
@@ -64,7 +70,7 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
}
.onFailure { e ->
logcat(ERROR) { e.asLog() }
- if (!localDir.delete()) {
+ if (!localDir.deleteIfExists()) {
logcat { "Failed to delete local repository: $localDir" }
}
finish()
diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt
index b5facaf3..fc370413 100644
--- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt
+++ b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt
@@ -26,8 +26,8 @@ import app.passwordstore.util.extensions.viewBinding
import app.passwordstore.util.settings.PreferenceKeys
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
-import java.io.File
import javax.inject.Inject
+import kotlin.io.path.writeText
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -46,7 +46,7 @@ class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
?: return@registerForActivityResult
lifecycleScope.launch {
withContext(dispatcherProvider.io()) {
- val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
+ val gpgIdentifierFile = PasswordRepository.getRepositoryDirectory().resolve(".gpg-id")
gpgIdentifierFile.writeText(selectedKey)
}
settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) }
diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt
index 5b6826bc..bfeccdc5 100644
--- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt
+++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt
@@ -48,8 +48,13 @@ import com.github.michaelbull.result.fold
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import dagger.hilt.android.AndroidEntryPoint
-import java.io.File
+import java.nio.file.Path
+import java.nio.file.Paths
import javax.inject.Inject
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
@@ -66,7 +71,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
private var recyclerViewStateToRestore: Parcelable? = null
private var actionMode: ActionMode? = null
- private var scrollTarget: File? = null
+ private var scrollTarget: Path? = null
private val model: SearchableRepositoryViewModel by activityViewModels()
private val binding by viewBinding(PasswordRecyclerViewBinding::bind)
@@ -76,7 +81,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
requireStore().refreshPasswordList()
}
- val currentDir: File
+ val currentDir: Path
get() = model.currentDir.value
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -97,9 +102,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
}
private fun initializePasswordList() {
- val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git")
+ val gitDir = PasswordRepository.getRepositoryDirectory().resolve(".git")
val hasGitDir =
- gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true)
+ gitDir.exists() && gitDir.isDirectory() && gitDir.listDirectoryEntries().isNotEmpty()
binding.swipeRefresher.setOnRefreshListener {
if (!hasGitDir) {
requireStore().refreshPasswordList()
@@ -179,7 +184,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) {
"Cannot navigate if ${PasswordStore.REQUEST_ARG_PATH} is not provided"
}
- model.navigateTo(File(path), pushPreviousLocation = false)
+ model.navigateTo(Paths.get(path), pushPreviousLocation = false)
lifecycleScope.launch {
model.searchResult.flowWithLifecycle(lifecycle).collect { result ->
// Only run animations when the new list is filtered, i.e., the user submitted a search,
@@ -317,7 +322,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
val preferences =
context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
preferences.edit {
- putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString())
+ putString(
+ item.file.absolutePathString().base64(),
+ System.currentTimeMillis().toString(),
+ )
}
}
@@ -368,7 +376,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
}
}
- fun navigateTo(file: File) {
+ fun navigateTo(file: Path) {
requireStore().clearSearch()
model.navigateTo(
file,
@@ -377,7 +385,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
- fun scrollToOnNextRefresh(file: File) {
+ fun scrollToOnNextRefresh(file: Path) {
scrollTarget = file
}
diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt
index c4dd631e..6625903d 100644
--- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt
+++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt
@@ -35,13 +35,12 @@ import app.passwordstore.ui.git.base.BaseGitActivity
import app.passwordstore.ui.onboarding.activity.OnboardingActivity
import app.passwordstore.ui.settings.SettingsActivity
import app.passwordstore.util.autofill.AutofillMatcher
+import app.passwordstore.util.extensions.asLog
import app.passwordstore.util.extensions.base64
import app.passwordstore.util.extensions.commitChange
-import app.passwordstore.util.extensions.contains
import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.isInsideRepository
import app.passwordstore.util.extensions.launchActivity
-import app.passwordstore.util.extensions.listFilesRecursively
import app.passwordstore.util.extensions.sharedPrefs
import app.passwordstore.util.settings.AuthMode
import app.passwordstore.util.settings.PreferenceKeys
@@ -49,13 +48,29 @@ import app.passwordstore.util.shortcuts.ShortcutHandler
import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
import com.github.michaelbull.result.fold
import com.github.michaelbull.result.onFailure
+import com.github.michaelbull.result.onSuccess
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 java.io.File
import java.lang.Character.UnicodeBlock
+import java.nio.file.Path
+import java.nio.file.Paths
import javax.inject.Inject
+import kotlin.io.path.ExperimentalPathApi
+import kotlin.io.path.absolute
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteRecursively
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.moveTo
+import kotlin.io.path.name
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.pathString
+import kotlin.io.path.relativeTo
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
@@ -88,13 +103,13 @@ class PasswordStore : BaseGitActivity() {
"'Files' intent extra must be set"
}
val target =
- File(
+ Paths.get(
requireNotNull(intentData.getStringExtra("SELECTED_FOLDER_PATH")) {
"'SELECTED_FOLDER_PATH' intent extra must be set"
}
)
- val repositoryPath = PasswordRepository.getRepositoryDirectory().absolutePath
- if (!target.isDirectory) {
+ val repositoryPath = PasswordRepository.getRepositoryDirectory().absolutePathString()
+ if (!target.isDirectory()) {
logcat(ERROR) { "Tried moving passwords to a non-existing folder." }
return@registerForActivityResult
}
@@ -104,20 +119,21 @@ class PasswordStore : BaseGitActivity() {
lifecycleScope.launch(dispatcherProvider.io()) {
for (file in filesToMove) {
- val source = File(file)
+ val source = Paths.get(file)
if (!source.exists()) {
logcat(ERROR) { "Tried moving something that appears non-existent." }
continue
}
- val destinationFile = File(target.absolutePath + "/" + source.name)
+ val destinationFile = Paths.get(target.absolutePathString(), source.name)
val basename = source.nameWithoutExtension
val sourceLongName =
getLongName(
- requireNotNull(source.parent) { "$file has no parent" },
+ requireNotNull(source.parent) { "$file has no parent" }.absolutePathString(),
repositoryPath,
basename,
)
- val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
+ val destinationLongName =
+ getLongName(target.absolutePathString(), repositoryPath, basename)
if (destinationFile.exists()) {
logcat(ERROR) { "Trying to move a file that already exists." }
withContext(dispatcherProvider.main()) {
@@ -142,15 +158,16 @@ class PasswordStore : BaseGitActivity() {
}
when (filesToMove.size) {
1 -> {
- val source = File(filesToMove[0])
+ val source = Paths.get(filesToMove[0])
val basename = source.nameWithoutExtension
val sourceLongName =
getLongName(
- requireNotNull(source.parent) { "$basename has no parent" },
+ requireNotNull(source.parent) { "$basename has no parent" }.pathString,
repositoryPath,
basename,
)
- val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
+ val destinationLongName =
+ getLongName(target.absolutePathString(), repositoryPath, basename)
withContext(dispatcherProvider.main()) {
commitChange(
resources.getString(
@@ -162,8 +179,8 @@ class PasswordStore : BaseGitActivity() {
}
}
else -> {
- val repoDir = PasswordRepository.getRepositoryDirectory().absolutePath
- val relativePath = getRelativePath("${target.absolutePath}/", repoDir)
+ val repoDir = PasswordRepository.getRepositoryDirectory().absolutePathString()
+ val relativePath = getRelativePath("${target.absolutePathString()}/", repoDir)
withContext(dispatcherProvider.main()) {
commitChange(
resources.getString(R.string.git_commit_move_multiple_text, relativePath)
@@ -203,7 +220,7 @@ class PasswordStore : BaseGitActivity() {
lifecycleScope.launch {
model.currentDir.flowWithLifecycle(lifecycle).collect { dir ->
- val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
+ val basePath = PasswordRepository.getRepositoryDirectory().absolute()
supportActionBar?.apply {
if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
}
@@ -348,7 +365,7 @@ class PasswordStore : BaseGitActivity() {
checkLocalRepository(PasswordRepository.getRepositoryDirectory())
}
- private fun checkLocalRepository(localDir: File?) {
+ private fun checkLocalRepository(localDir: Path?) {
if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) {
// do not push the fragment if we already have it
if (
@@ -356,7 +373,10 @@ class PasswordStore : BaseGitActivity() {
) {
settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) }
val args = Bundle()
- args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
+ args.putString(
+ REQUEST_ARG_PATH,
+ PasswordRepository.getRepositoryDirectory().absolutePathString(),
+ )
// if the activity was started from the autofill settings, the
// intent is to match a clicked pwd with app. pass this to fragment
@@ -406,25 +426,27 @@ class PasswordStore : BaseGitActivity() {
fun createPassword() {
if (!validateState()) return
val currentDir = currentDir
- logcat(INFO) { "Adding file to : ${currentDir.absolutePath}" }
+ logcat(INFO) { "Adding file to : ${currentDir.absolutePathString()}" }
val intent = Intent(this, PasswordCreationActivity::class.java)
- intent.putExtra(BasePGPActivity.EXTRA_FILE_PATH, currentDir.absolutePath)
+ intent.putExtra(BasePGPActivity.EXTRA_FILE_PATH, currentDir.absolutePathString())
intent.putExtra(
BasePGPActivity.EXTRA_REPO_PATH,
- PasswordRepository.getRepositoryDirectory().absolutePath,
+ PasswordRepository.getRepositoryDirectory().absolutePathString(),
)
listRefreshAction.launch(intent)
}
fun createFolder() {
if (!validateState()) return
- FolderCreationDialogFragment.newInstance(currentDir.path).show(supportFragmentManager, null)
+ FolderCreationDialogFragment.newInstance(currentDir.pathString)
+ .show(supportFragmentManager, null)
}
+ @OptIn(ExperimentalPathApi::class)
fun deletePasswords(selectedItems: List<PasswordItem>) {
var size = 0
selectedItems.forEach {
- if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size
+ if (it.file.isRegularFile()) size++ else size += it.file.listDirectoryEntries().size
}
if (size == 0) {
selectedItems.map { item -> item.file.deleteRecursively() }
@@ -434,9 +456,9 @@ class PasswordStore : BaseGitActivity() {
MaterialAlertDialogBuilder(this)
.setMessage(resources.getQuantityString(R.plurals.delete_dialog_text, size, size))
.setPositiveButton(resources.getString(R.string.dialog_yes)) { _, _ ->
- val filesToDelete = arrayListOf<File>()
+ val filesToDelete = arrayListOf<Path>()
selectedItems.forEach { item ->
- if (item.file.isDirectory) filesToDelete.addAll(item.file.listFilesRecursively())
+ if (item.file.isDirectory()) filesToDelete.addAll(item.file.listDirectoryEntries())
else filesToDelete.add(item.file)
}
selectedItems.map { item -> item.file.deleteRecursively() }
@@ -444,7 +466,7 @@ class PasswordStore : BaseGitActivity() {
AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete)
val fmt =
selectedItems.joinToString(separator = ", ") { item ->
- item.file.toRelativeString(PasswordRepository.getRepositoryDirectory())
+ item.file.relativeTo(PasswordRepository.getRepositoryDirectory()).absolutePathString()
}
lifecycleScope.launch {
commitChange(resources.getString(R.string.git_commit_remove_text, fmt))
@@ -456,7 +478,7 @@ class PasswordStore : BaseGitActivity() {
fun movePasswords(values: List<PasswordItem>) {
val intent = Intent(this, SelectFolderActivity::class.java)
- val fileLocations = values.map { it.file.absolutePath }.toTypedArray()
+ val fileLocations = values.map { it.file.absolutePathString() }.toTypedArray()
intent.putExtra("Files", fileLocations)
passwordMoveAction.launch(intent)
}
@@ -497,7 +519,7 @@ class PasswordStore : BaseGitActivity() {
.setView(view)
.setMessage(getString(R.string.message_rename_folder, oldCategory.name))
.setPositiveButton(R.string.dialog_ok) { _, _ ->
- val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}")
+ val newCategory = Paths.get("${oldCategory.file.parent}/${newCategoryEditText.text}")
when {
newCategoryEditText.text.isNullOrBlank() ->
renameCategory(oldCategory, CategoryRenameError.EmptyField)
@@ -512,11 +534,11 @@ class PasswordStore : BaseGitActivity() {
// history
val preference =
getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
- val timestamp = preference.getString(oldCategory.file.absolutePath.base64())
+ val timestamp = preference.getString(oldCategory.file.absolutePathString().base64())
if (timestamp != null) {
preference.edit {
- remove(oldCategory.file.absolutePath.base64())
- putString(newCategory.absolutePath.base64(), timestamp)
+ remove(oldCategory.file.absolutePathString().base64())
+ putString(newCategory.absolutePathString().base64(), timestamp)
}
}
@@ -552,14 +574,14 @@ class PasswordStore : BaseGitActivity() {
* 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) {
+ fun refreshPasswordList(target: Path? = null) {
val plist = getPasswordFragment()
- if (target?.isDirectory == true && model.currentDir.value.contains(target)) {
+ if (target?.isDirectory() == true && model.currentDir.value.contains(target)) {
plist?.navigateTo(target)
- } else if (target?.isFile == true && model.currentDir.value.contains(target)) {
+ } else if (target?.isRegularFile() == true && model.currentDir.value.contains(target)) {
// Creating new passwords is handled by an activity, so we will refresh in onStart.
plist?.scrollToOnNextRefresh(target)
- } else if (model.currentDir.value.isDirectory) {
+ } else if (model.currentDir.value.isDirectory()) {
model.forceRefresh()
} else {
model.reset()
@@ -567,40 +589,41 @@ class PasswordStore : BaseGitActivity() {
}
}
- private val currentDir: File
+ private val currentDir: Path
get() = getPasswordFragment()?.currentDir ?: PasswordRepository.getRepositoryDirectory()
- private suspend fun moveFile(source: File, destinationFile: File) {
+ private suspend fun moveFile(source: Path, destinationFile: Path) {
val sourceDestinationMap =
- if (source.isDirectory) {
- destinationFile.mkdirs()
+ if (source.isDirectory()) {
+ destinationFile.createDirectories()
// 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 {
+ source.listDirectoryEntries().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(dispatcherProvider.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()
+ runCatching { source.moveTo(destinationFile) }
+ .onFailure {
+ logcat(ERROR) { it.asLog("Something went wrong while moving $source to $destinationFile.") }
+ withContext(dispatcherProvider.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)
- }
+ .onSuccess { AutofillMatcher.updateMatches(this, sourceDestinationMap) }
}
fun matchPasswordWithApp(item: PasswordItem) {
val path =
- item.file.absolutePath
+ item.file
+ .absolutePathString()
.replace(PasswordRepository.getRepositoryDirectory().toString() + "/", "")
.replace(".gpg", "")
val data = Intent()
diff --git a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt
index c6c30b88..20aaa5aa 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt
@@ -41,6 +41,9 @@ import de.Maxr1998.modernpreferences.PreferenceScreen
import de.Maxr1998.modernpreferences.helpers.onClick
import de.Maxr1998.modernpreferences.helpers.pref
import de.Maxr1998.modernpreferences.helpers.switch
+import kotlin.io.path.ExperimentalPathApi
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteRecursively
class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider {
@@ -58,6 +61,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
private var showSshKeyPref: Preference? = null
+ @OptIn(ExperimentalPathApi::class)
override fun provideSettings(builder: PreferenceScreen.Builder) {
val encryptedPreferences = hiltEntryPoint.encryptedPreferences()
val gitSettings = hiltEntryPoint.gitSettings()
@@ -164,7 +168,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
PasswordRepository.closeRepository()
PasswordRepository.getRepositoryDirectory().let { dir ->
dir.deleteRecursively()
- dir.mkdirs()
+ dir.createDirectories()
}
}
.onFailure { it.message?.let { message -> activity.snackbar(message = message) } }
diff --git a/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt
index d1257f14..b6a2d410 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt
@@ -21,7 +21,7 @@ import com.github.androidpasswordstore.autofillparser.AutofillAction
import com.github.androidpasswordstore.autofillparser.FillableForm
import com.github.androidpasswordstore.autofillparser.fillWith
import com.github.michaelbull.result.fold
-import java.io.File
+import java.nio.file.Path
import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
@@ -58,7 +58,7 @@ class Api26AutofillResponseBuilder private constructor(form: FillableForm) :
}
}
- private fun makeMatchDataset(context: Context, file: File): Dataset? {
+ private fun makeMatchDataset(context: Context, file: Path): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
val metadata = makeFillMatchMetadata(context, file)
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
@@ -135,7 +135,7 @@ class Api26AutofillResponseBuilder private constructor(form: FillableForm) :
}
}
- private fun makeFillResponse(context: Context, matchedFiles: List<File>): FillResponse? {
+ private fun makeFillResponse(context: Context, matchedFiles: List<Path>): FillResponse? {
var datasetCount = 0
return FillResponse.Builder().run {
for (file in matchedFiles) {
diff --git a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt
index 743d6944..a68c8dbe 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt
@@ -26,7 +26,7 @@ import com.github.androidpasswordstore.autofillparser.AutofillAction
import com.github.androidpasswordstore.autofillparser.FillableForm
import com.github.androidpasswordstore.autofillparser.fillWith
import com.github.michaelbull.result.fold
-import java.io.File
+import java.nio.file.Path
import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
@@ -123,7 +123,7 @@ class Api30AutofillResponseBuilder private constructor(form: FillableForm) :
private fun makeMatchDataset(
context: Context,
- file: File,
+ file: Path,
imeSpec: InlinePresentationSpec?,
): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
@@ -199,7 +199,7 @@ class Api30AutofillResponseBuilder private constructor(form: FillableForm) :
private fun makeFillResponse(
context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
- matchedFiles: List<File>,
+ matchedFiles: List<Path>,
): FillResponse? {
var datasetCount = 0
val imeSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()
diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt
index 66edaafd..8a8f3c26 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt
@@ -14,7 +14,11 @@ import com.github.androidpasswordstore.autofillparser.computeCertificatesHash
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
-import java.io.File
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.io.path.absolute
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.exists
import logcat.LogPriority.ERROR
import logcat.LogPriority.WARN
import logcat.logcat
@@ -103,19 +107,22 @@ class AutofillMatcher {
fun getMatchesFor(
context: Context,
formOrigin: FormOrigin,
- ): Result<List<File>, AutofillPublisherChangedException> {
+ ): Result<List<Path>, AutofillPublisherChangedException> {
if (hasFormOriginHashChanged(context, formOrigin)) {
return Err(AutofillPublisherChangedException(formOrigin))
}
val matchPreferences = context.matchPreferences(formOrigin)
val matchedFiles =
- matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
+ matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { Paths.get(it) }
return Ok(
matchedFiles
.filter { it.exists() }
.also { validFiles ->
matchPreferences.edit {
- putStringSet(matchesKey(formOrigin), validFiles.map { it.absolutePath }.toSet())
+ putStringSet(
+ matchesKey(formOrigin),
+ validFiles.map { it.absolutePathString() }.toSet(),
+ )
}
}
)
@@ -135,7 +142,7 @@ class AutofillMatcher {
* The maximum number of matches is limited by [MAX_NUM_MATCHES] since older versions of Android
* may crash when too many datasets are offered.
*/
- fun addMatchFor(context: Context, formOrigin: FormOrigin, file: File) {
+ fun addMatchFor(context: Context, formOrigin: FormOrigin, file: Path) {
if (!file.exists()) return
if (hasFormOriginHashChanged(context, formOrigin)) {
// This should never happen since we already verified the publisher in
@@ -145,8 +152,8 @@ class AutofillMatcher {
}
val matchPreferences = context.matchPreferences(formOrigin)
val matchedFiles =
- matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
- val newFiles = setOf(file.absoluteFile).union(matchedFiles)
+ matchPreferences.getStringSet(matchesKey(formOrigin), emptySet()).orEmpty().map(Paths::get)
+ val newFiles = setOf(file.absolute()).union(matchedFiles)
if (newFiles.size > MAX_NUM_MATCHES) {
Toast.makeText(
context,
@@ -157,7 +164,7 @@ class AutofillMatcher {
return
}
matchPreferences.edit {
- putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet())
+ putStringSet(matchesKey(formOrigin), newFiles.map(Path::absolutePathString).toSet())
}
storeFormOriginHash(context, formOrigin)
logcat { "Stored match for $formOrigin" }
@@ -169,12 +176,14 @@ class AutofillMatcher {
*/
fun updateMatches(
context: Context,
- moveFromTo: Map<File, File> = emptyMap(),
- delete: Collection<File> = emptyList(),
+ moveFromTo: Map<Path, Path> = emptyMap(),
+ delete: Collection<Path> = emptyList(),
) {
- val deletePathList = delete.map { it.absolutePath }
+ val deletePathList = delete.map { it.absolutePathString() }
val oldNewPathMap =
- moveFromTo.mapValues { it.value.absolutePath }.mapKeys { it.key.absolutePath }
+ moveFromTo
+ .mapValues { it.value.absolutePathString() }
+ .mapKeys { it.key.absolutePathString() }
for (prefs in listOf(context.autofillAppMatches, context.autofillWebMatches)) {
for ((key, value) in prefs.all) {
if (!key.startsWith(PREFERENCE_PREFIX_MATCHES)) continue
@@ -190,8 +199,8 @@ class AutofillMatcher {
val newMatches =
oldMatches
.asSequence()
- .minus(deletePathList)
- .minus(oldNewPathMap.values)
+ .minus(deletePathList.toSet())
+ .minus(oldNewPathMap.values.toSet())
.map { match ->
val newPath = oldNewPathMap[match] ?: return@map match
logcat { "Updating match for $key: $match --> $newPath" }
diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt
index 70de5972..2e308d9d 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt
@@ -11,8 +11,11 @@ import app.passwordstore.util.extensions.sharedPrefs
import app.passwordstore.util.services.getDefaultUsername
import app.passwordstore.util.settings.PreferenceKeys
import com.github.androidpasswordstore.autofillparser.Credentials
-import java.io.File
+import java.nio.file.Path
import java.nio.file.Paths
+import kotlin.io.path.name
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.pathString
enum class DirectoryStructure(val value: String) {
EncryptedUsername("encrypted_username"),
@@ -29,11 +32,11 @@ enum class DirectoryStructure(val value: String) {
* - work/example.org/john@doe.org/password.gpg --> john@doe.org (DirectoryBased)
* - Temporary PIN.gpg --> Temporary PIN (DirectoryBased, fallback)
*/
- fun getUsernameFor(file: File): String? =
+ fun getUsernameFor(file: Path): String? =
when (this) {
EncryptedUsername -> null
FileBased -> file.nameWithoutExtension
- DirectoryBased -> file.parentFile?.name ?: file.nameWithoutExtension
+ DirectoryBased -> file.parent?.name ?: file.nameWithoutExtension
}
/**
@@ -50,11 +53,11 @@ enum class DirectoryStructure(val value: String) {
* - work/example.org/john@doe.org/password.gpg --> example.org (DirectoryBased)
* - Temporary PIN.gpg --> null (DirectoryBased)
*/
- fun getIdentifierFor(file: File): String? =
+ fun getIdentifierFor(file: Path): String? =
when (this) {
EncryptedUsername -> file.nameWithoutExtension
- FileBased -> file.parentFile?.name ?: file.nameWithoutExtension
- DirectoryBased -> file.parentFile?.parent
+ FileBased -> file.parent?.name ?: file.nameWithoutExtension
+ DirectoryBased -> file.parent?.parent?.pathString
}
/**
@@ -69,11 +72,11 @@ enum class DirectoryStructure(val value: String) {
* - work/example.org/john@doe.org/password.gpg --> work (DirectoryBased)
* - example.org/john@doe.org/password.gpg --> null (DirectoryBased)
*/
- fun getPathToIdentifierFor(file: File): String? =
+ fun getPathToIdentifierFor(file: Path): String? =
when (this) {
- EncryptedUsername -> file.parent
- FileBased -> file.parentFile?.parent
- DirectoryBased -> file.parentFile?.parentFile?.parent
+ EncryptedUsername -> file.parent.pathString
+ FileBased -> file.parent?.parent?.pathString
+ DirectoryBased -> file.parent?.parent?.parent?.pathString
}
/**
@@ -90,12 +93,12 @@ enum class DirectoryStructure(val value: String) {
* - work/example.org/john@doe.org/password.gpg --> john@doe.org/password (DirectoryBased)
* - Temporary PIN.gpg --> Temporary PIN (DirectoryBased, fallback)
*/
- fun getAccountPartFor(file: File): String? =
+ fun getAccountPartFor(file: Path): String? =
when (this) {
EncryptedUsername -> null
- FileBased -> file.nameWithoutExtension.takeIf { file.parentFile != null }
+ FileBased -> file.nameWithoutExtension.takeIf { file.parent != null }
DirectoryBased ->
- file.parentFile?.let { parentFile -> "${parentFile.name}/${file.nameWithoutExtension}" }
+ file.parent?.let { parent -> "${parent.name}/${file.nameWithoutExtension}" }
?: file.nameWithoutExtension
}
@@ -132,13 +135,13 @@ object AutofillPreferences {
fun credentialsFromStoreEntry(
context: Context,
- file: File,
+ path: Path,
entry: PasswordEntry,
directoryStructure: DirectoryStructure,
): Credentials {
// Always give priority to a username stored in the encrypted extras
val username =
- entry.username ?: directoryStructure.getUsernameFor(file) ?: context.getDefaultUsername()
+ entry.username ?: directoryStructure.getUsernameFor(path) ?: context.getDefaultUsername()
val totp = if (entry.hasTotp()) entry.currentOtp else null
return Credentials(username, entry.password, totp)
}
diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt
index 639f1073..ca894f8f 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt
@@ -21,7 +21,8 @@ import androidx.autofill.inline.v1.InlineSuggestionUi
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.passwords.PasswordStore
-import java.io.File
+import java.nio.file.Path
+import kotlin.io.path.relativeTo
data class DatasetMetadata(val title: String, val subtitle: String?, @DrawableRes val iconRes: Int)
@@ -77,7 +78,7 @@ fun makeInlinePresentation(
return InlinePresentation(slice, imeSpec, false)
}
-fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata {
+fun makeFillMatchMetadata(context: Context, file: Path): DatasetMetadata {
val directoryStructure = AutofillPreferences.directoryStructure(context)
val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory())
val title =
diff --git a/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt b/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt
index a921f8d6..e1fbe1f0 100644
--- a/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt
+++ b/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt
@@ -5,10 +5,9 @@
package app.passwordstore.util.extensions
import app.passwordstore.data.repo.PasswordRepository
-import com.github.michaelbull.result.getOrElse
-import com.github.michaelbull.result.runCatching
-import java.io.File
+import java.nio.file.Path
import java.time.Instant
+import kotlin.io.path.absolutePathString
import logcat.asLog
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
@@ -18,30 +17,15 @@ infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag
}
-/** Checks whether this [File] is a directory that contains [other]. */
-fun File.contains(other: File): Boolean {
- if (!isDirectory) return false
- if (!other.exists()) return false
- val relativePath =
- runCatching { other.relativeTo(this) }
- .getOrElse {
- return false
- }
- // Direct containment is equivalent to the relative path being equal to the filename.
- return relativePath.path == other.name
-}
-
/**
- * Checks if this [File] is in the password repository directory as given by
+ * Checks if this [Path] is in the password repository directory as given by
* [PasswordRepository.getRepositoryDirectory]
*/
-fun File.isInsideRepository(): Boolean {
- return canonicalPath.contains(PasswordRepository.getRepositoryDirectory().canonicalPath)
+fun Path.isInsideRepository(): Boolean {
+ return absolutePathString()
+ .contains(PasswordRepository.getRepositoryDirectory().absolutePathString())
}
-/** Recursively lists the files in this [File], skipping any directories it encounters. */
-fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
-
/**
* Unique SHA-1 hash of this commit as hexadecimal string.
*
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt
index 863721a5..0fe95ee9 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt
@@ -65,7 +65,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
/** Whether the operation requires authentication or not. */
open val requiresAuth: Boolean = true
- private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
+ private val hostKeyFile = callingActivity.filesDir.resolve(".host_key").toPath()
private var sshSessionFactory: SshjSessionFactory? = null
private val hiltEntryPoint =
EntryPointAccessors.fromApplication<GitOperationEntryPoint>(callingActivity)
diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt
index 3f1b58d0..8b9376c0 100644
--- a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt
+++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt
@@ -25,7 +25,6 @@ import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.settings.PreferenceKeys
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
-import java.io.File
import java.io.IOException
import java.security.KeyFactory
import java.security.KeyPairGenerator
@@ -34,6 +33,12 @@ import java.security.PrivateKey
import java.security.PublicKey
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.exists
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -113,10 +118,10 @@ object SshKey {
get() = Application.instance.applicationContext
private val privateKeyFile
- get() = File(context.filesDir, ".ssh_key")
+ get() = context.filesDir.toPath().resolve(".ssh_key")
private val publicKeyFile
- get() = File(context.filesDir, ".ssh_key.pub")
+ get() = context.filesDir.toPath().resolve(".ssh_key.pub")
private var type: Type?
get() = Type.fromValue(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_KEY_TYPE))
@@ -178,11 +183,11 @@ object SshKey {
context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit {
clear()
}
- if (privateKeyFile.isFile) {
- privateKeyFile.delete()
+ if (privateKeyFile.isRegularFile()) {
+ privateKeyFile.deleteIfExists()
}
- if (publicKeyFile.isFile) {
- publicKeyFile.delete()
+ if (publicKeyFile.isRegularFile()) {
+ publicKeyFile.deleteIfExists()
}
context.getEncryptedGitPrefs().edit { remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) }
type = null
@@ -247,7 +252,7 @@ object SshKey {
withContext(Dispatchers.IO) {
EncryptedFile.Builder(
context,
- privateKeyFile,
+ privateKeyFile.toFile(),
getOrCreateWrappingMasterKey(requireAuthentication),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
)
@@ -304,7 +309,7 @@ object SshKey {
fun provide(client: SSHClient, passphraseFinder: InteractivePasswordFinder): KeyProvider? =
when (type) {
Type.LegacyGenerated,
- Type.Imported -> client.loadKeys(privateKeyFile.absolutePath, passphraseFinder)
+ Type.Imported -> client.loadKeys(privateKeyFile.absolutePathString(), passphraseFinder)
Type.KeystoreNative -> KeystoreNativeKeyProvider
Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider
null -> null
diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt
index c03575d4..06089369 100644
--- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt
+++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt
@@ -11,15 +11,18 @@ import app.passwordstore.util.git.operation.CredentialFinder
import app.passwordstore.util.settings.AuthMode
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
-import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
+import java.nio.file.Path
import java.security.PublicKey
import java.util.Collections
import java.util.concurrent.TimeUnit
import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine
+import kotlin.io.path.exists
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
import kotlinx.coroutines.runBlocking
import logcat.LogPriority.WARN
import logcat.logcat
@@ -69,7 +72,7 @@ abstract class InteractivePasswordFinder(private val dispatcherProvider: Dispatc
class SshjSessionFactory(
private val authMethod: SshAuthMethod,
- private val hostKeyFile: File,
+ private val hostKeyFile: Path,
private val dispatcherProvider: DispatcherProvider,
) : SshSessionFactory() {
@@ -93,7 +96,7 @@ class SshjSessionFactory(
}
}
-private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
+private fun makeTofuHostKeyVerifier(hostKeyFile: Path): HostKeyVerifier {
if (!hostKeyFile.exists()) {
return object : HostKeyVerifier {
override fun verify(hostname: String?, port: Int, key: PublicKey?): Boolean {
@@ -125,7 +128,7 @@ private class SshjSession(
uri: URIish,
private val username: String,
private val authMethod: SshAuthMethod,
- private val hostKeyFile: File,
+ private val hostKeyFile: Path,
private val dispatcherProvider: DispatcherProvider,
) : RemoteSession {
diff --git a/app/src/main/java/app/passwordstore/util/services/PasswordExportService.kt b/app/src/main/java/app/passwordstore/util/services/PasswordExportService.kt
index ad70062a..e76eb7e9 100644
--- a/app/src/main/java/app/passwordstore/util/services/PasswordExportService.kt
+++ b/app/src/main/java/app/passwordstore/util/services/PasswordExportService.kt
@@ -19,6 +19,7 @@ import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
+import kotlin.io.path.pathString
import logcat.logcat
class PasswordExportService : Service() {
@@ -61,9 +62,9 @@ class PasswordExportService : Service() {
requireNotNull(PasswordRepository.getRepositoryDirectory()) {
"Password directory must be set to export them"
}
- val sourcePassDir = DocumentFile.fromFile(repositoryDirectory)
+ val sourcePassDir = DocumentFile.fromFile(repositoryDirectory.toFile())
- logcat { "Copying ${repositoryDirectory.path} to $targetDirectory" }
+ logcat { "Copying ${repositoryDirectory.pathString} to $targetDirectory" }
val dateString = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val passDir = targetDirectory.createDirectory("password_store_$dateString")
diff --git a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt
index d15269be..7f3de73c 100644
--- a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt
+++ b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt
@@ -14,9 +14,11 @@ import app.passwordstore.injection.prefs.SettingsPreferences
import app.passwordstore.util.extensions.getString
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
-import java.io.File
+import java.nio.file.Paths
import javax.inject.Inject
import javax.inject.Singleton
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.exists
import org.eclipse.jgit.transport.URIish
enum class Protocol(val pref: String) {
@@ -174,9 +176,9 @@ constructor(
/** Deletes a previously saved SSH host key */
fun clearSavedHostKey() {
- File(hostKeyPath).delete()
+ Paths.get(hostKeyPath).deleteIfExists()
}
/** Returns true if a host key was previously saved */
- fun hasSavedHostKey(): Boolean = File(hostKeyPath).exists()
+ fun hasSavedHostKey(): Boolean = Paths.get(hostKeyPath).exists()
}
diff --git a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt
index 47187080..acbcf36d 100644
--- a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt
+++ b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt
@@ -12,8 +12,9 @@ import app.passwordstore.util.extensions.getString
import app.passwordstore.util.git.sshj.SshKey
import com.github.michaelbull.result.get
import com.github.michaelbull.result.runCatching
-import java.io.File
import java.net.URI
+import java.nio.file.Paths
+import kotlin.io.path.exists
import logcat.LogPriority.ERROR
import logcat.LogPriority.INFO
import logcat.logcat
@@ -108,7 +109,7 @@ private fun migrateToHideAll(sharedPrefs: SharedPreferences) {
}
private fun migrateToSshKey(filesDirPath: String, sharedPrefs: SharedPreferences) {
- val privateKeyFile = File(filesDirPath, ".ssh_key")
+ val privateKeyFile = Paths.get(filesDirPath, ".ssh_key")
if (
sharedPrefs.contains(PreferenceKeys.USE_GENERATED_KEY) &&
!SshKey.exists &&
diff --git a/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt b/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt
index 61e4b118..3adc87cf 100644
--- a/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt
+++ b/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt
@@ -11,6 +11,7 @@ import app.passwordstore.Application
import app.passwordstore.data.password.PasswordItem
import app.passwordstore.util.extensions.base64
import app.passwordstore.util.extensions.getString
+import kotlin.io.path.absolutePathString
enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>) {
FOLDER_FIRST(
@@ -27,8 +28,8 @@ enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>)
Comparator { p1: PasswordItem, p2: PasswordItem ->
val recentHistory =
Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
- val timeP1 = recentHistory.getString(p1.file.absolutePath.base64())
- val timeP2 = recentHistory.getString(p2.file.absolutePath.base64())
+ val timeP1 = recentHistory.getString(p1.file.absolutePathString().base64())
+ val timeP2 = recentHistory.getString(p2.file.absolutePathString().base64())
when {
timeP1 != null && timeP2 != null -> timeP2.compareTo(timeP1)
timeP1 != null && timeP2 == null -> return@Comparator -1
diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
index 3c3db292..2a1be5a7 100644
--- a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
+++ b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
@@ -32,10 +32,21 @@ import app.passwordstore.util.settings.PasswordSortOrder
import app.passwordstore.util.settings.PreferenceKeys
import com.github.androidpasswordstore.sublimefuzzy.Fuzzy
import dagger.hilt.android.lifecycle.HiltViewModel
-import java.io.File
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
import java.text.Collator
import java.util.Locale
import javax.inject.Inject
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.exists
+import kotlin.io.path.extension
+import kotlin.io.path.isDirectory
+import kotlin.io.path.isHidden
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.name
+import kotlin.io.path.pathString
+import kotlin.io.path.relativeTo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -43,7 +54,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -54,9 +64,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import me.zhanghai.android.fastscroll.PopupTextProvider
-private fun File.toPasswordItem() =
- if (isFile) PasswordItem.newPassword(name, this, PasswordRepository.getRepositoryDirectory())
- else PasswordItem.newCategory(name, this, PasswordRepository.getRepositoryDirectory())
+private fun Path.toPasswordItem() =
+ if (isRegularFile()) PasswordItem.newPassword(this, PasswordRepository.getRepositoryDirectory())
+ else PasswordItem.newCategory(this, PasswordRepository.getRepositoryDirectory())
private fun PasswordItem.fuzzyMatch(filter: String): Int {
val (_, score) = Fuzzy.fuzzyMatch(filter, longName)
@@ -89,7 +99,7 @@ private fun PasswordItem.Companion.makeComparator(
}
val PasswordItem.stableId: String
- get() = file.absolutePath
+ get() = file.absolutePathString()
enum class FilterMode {
NoFilter,
@@ -151,7 +161,7 @@ constructor(
get() = PasswordItem.makeComparator(typeSortOrder, directoryStructure)
private data class SearchAction(
- val baseDirectory: File,
+ val baseDirectory: Path,
val filter: String,
val filterMode: FilterMode,
val searchMode: SearchMode,
@@ -162,7 +172,7 @@ constructor(
)
private fun makeSearchAction(
- baseDirectory: File,
+ baseDirectory: Path,
filter: String,
filterMode: FilterMode,
searchMode: SearchMode,
@@ -206,9 +216,9 @@ constructor(
val prefilteredResultFlow =
when (searchAction.listMode) {
ListMode.FilesOnly ->
- listResultFlow.filter { it.isFile }.flowOn(dispatcherProvider.io())
+ listResultFlow.filter { it.isRegularFile() }.flowOn(dispatcherProvider.io())
ListMode.DirectoriesOnly ->
- listResultFlow.filter { it.isDirectory }.flowOn(dispatcherProvider.io())
+ listResultFlow.filter { it.isDirectory() }.flowOn(dispatcherProvider.io())
ListMode.AllEntries -> listResultFlow
}
val passwordList =
@@ -223,7 +233,7 @@ constructor(
FilterMode.Exact -> {
prefilteredResultFlow
.filter { absoluteFile ->
- absoluteFile.relativeTo(root).path.contains(searchAction.filter)
+ absoluteFile.relativeTo(root).pathString.contains(searchAction.filter)
}
.map { it.toPasswordItem() }
.flowOn(dispatcherProvider.io())
@@ -238,7 +248,7 @@ constructor(
if (regex != null) {
prefilteredResultFlow
.filter { absoluteFile ->
- regex.containsMatchIn(absoluteFile.relativeTo(root).path)
+ regex.containsMatchIn(absoluteFile.relativeTo(root).pathString)
}
.map { it.toPasswordItem() }
.flowOn(dispatcherProvider.io())
@@ -268,27 +278,28 @@ constructor(
}
.flowOn(dispatcherProvider.io())
- private fun shouldTake(file: File) =
+ private fun shouldTake(file: Path) =
with(file) {
if (showHiddenContents) {
return !file.name.startsWith(".git")
}
- if (isDirectory) {
- !isHidden
+ if (isDirectory()) {
+ !isHidden()
} else {
- !isHidden && file.extension == "gpg"
+ !isHidden() && file.extension == "gpg"
}
}
- private fun listFiles(dir: File): Flow<File> {
- return dir.listFiles(::shouldTake)?.asFlow() ?: emptyFlow()
+ private fun listFiles(dir: Path): Flow<Path> {
+ return Files.newDirectoryStream(dir, ::shouldTake).asFlow()
}
- private fun listFilesRecursively(dir: File): Flow<File> {
+ private fun listFilesRecursively(dir: Path): Flow<Path> {
return dir
+ .toFile()
// Take top directory even if it is hidden.
.walkTopDown()
- .onEnter { file -> file == dir || shouldTake(file) }
+ .onEnter { file -> file.toPath() == dir || shouldTake(file.toPath()) }
.asFlow()
// Skip the root directory
.drop(1)
@@ -296,24 +307,25 @@ constructor(
yield()
it
}
+ .map { it.toPath() }
.filter(::shouldTake)
}
private val _currentDir = MutableStateFlow(root)
val currentDir = _currentDir.asStateFlow()
- data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?)
+ data class NavigationStackEntry(val dir: Path, val recyclerViewState: Parcelable?)
private val navigationStack = ArrayDeque<NavigationStackEntry>()
fun navigateTo(
- newDirectory: File = root,
+ newDirectory: Path = root,
listMode: ListMode = ListMode.AllEntries,
recyclerViewState: Parcelable? = null,
pushPreviousLocation: Boolean = true,
) {
if (!newDirectory.exists()) return
- require(newDirectory.isDirectory) { "Can only navigate to a directory" }
+ require(newDirectory.isDirectory()) { "Can only navigate to a directory" }
if (pushPreviousLocation) {
navigationStack.addFirst(NavigationStackEntry(_currentDir.value, recyclerViewState))
}
@@ -353,12 +365,12 @@ constructor(
fun search(
filter: String,
- baseDirectory: File? = null,
+ baseDirectory: Path? = null,
filterMode: FilterMode = FilterMode.Fuzzy,
searchMode: SearchMode? = null,
listMode: ListMode = ListMode.AllEntries,
) {
- require(baseDirectory?.isDirectory != false) { "Can only search in a directory" }
+ require(baseDirectory?.isDirectory() != false) { "Can only search in a directory" }
searchActionFlow.update {
makeSearchAction(
filter = filter,
@@ -401,7 +413,7 @@ constructor(
private object PasswordItemDiffCallback : DiffUtil.ItemCallback<PasswordItem>() {
override fun areItemsTheSame(oldItem: PasswordItem, newItem: PasswordItem) =
- oldItem.file.absolutePath == newItem.file.absolutePath
+ oldItem.file.absolutePathString() == newItem.file.absolutePathString()
override fun areContentsTheSame(oldItem: PasswordItem, newItem: PasswordItem) = oldItem == newItem
}
@@ -479,11 +491,11 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
fun requireSelectionTracker() = selectionTracker!!
private val selectedFiles
- get() = requireSelectionTracker().selection.map { File(it) }
+ get() = requireSelectionTracker().selection.map { Paths.get(it) }
fun getSelectedItems() = selectedFiles.map { it.toPasswordItem() }
- fun getPositionForFile(file: File) = itemKeyProvider.getPosition(file.absolutePath)
+ fun getPositionForFile(file: Path) = itemKeyProvider.getPosition(file.absolutePathString())
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): T {
val view = LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)