summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <msfjarvis@gmail.com>2020-07-09 14:00:24 +0530
committerGitHub <noreply@github.com>2020-07-09 14:00:24 +0530
commitfc00de61dcc53f8378e332f7db1d5c58e3308aa7 (patch)
treee303a6b77f72ae4cbe7d0d16141fe07f822b97a9
parent0ead6b2a4dfc409ffdffd5ef34c860aaa7f378ae (diff)
Move password export to the IO dispatcher (#918)
* Move password export to the IO dispatcher Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Simplify snackbars and disable exit operations during export Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Move export password logic to service Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Reformat Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Use explicit null check Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Remove unneeded hack Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Fixup strings Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Don't use coroutines in a service Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Update notification icon Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Rollback unwanted formatting Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Aditya Wasan <adityawasan55@gmail.com>
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/src/main/AndroidManifest.xml3
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordExportService.kt152
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt95
-rw-r--r--app/src/main/res/drawable/ic_round_import_export.xml10
-rw-r--r--app/src/main/res/values/strings.xml1
6 files changed, 185 insertions, 77 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebf699d3..f9691e18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file.
- Top-level password names had inconsistent top margin making them look askew
- Autofill can now be made more reliable in Chrome by enabling an accessibility service that works around known Chrome limitations
- Password Store no longer ignores the selected OpenKeychain key
+- Password export now happens in a separate process, preventing possible freezes
### Added
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1a49b017..4e6ea102 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -116,6 +116,9 @@
android:name=".ClipboardService"
android:process=":clipboard_service_process" />
<service
+ android:name=".PasswordExportService"
+ android:process=":password_export_service_process" />
+ <service
android:name=".autofill.oreo.OreoAutofillService"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordExportService.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordExportService.kt
new file mode 100644
index 00000000..e877a77a
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordExportService.kt
@@ -0,0 +1,152 @@
+package com.zeapo.pwdstore
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.content.getSystemService
+import androidx.documentfile.provider.DocumentFile
+import com.github.ajalt.timberkt.d
+import com.zeapo.pwdstore.utils.PasswordRepository
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.Calendar
+import java.util.TimeZone
+
+class PasswordExportService : Service() {
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent != null) {
+ when (intent.action) {
+ ACTION_EXPORT_PASSWORD -> {
+ val uri = intent.getParcelableExtra<Uri>("uri")
+ if (uri != null) {
+ val targetDirectory = DocumentFile.fromTreeUri(applicationContext, uri)
+
+ if (targetDirectory != null) {
+ createNotification()
+ exportPasswords(targetDirectory)
+ stopSelf()
+ return START_NOT_STICKY
+ }
+ }
+ }
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ /**
+ * Exports passwords to the given directory.
+ *
+ * Recursively copies the existing password store to an external directory.
+ *
+ * @param targetDirectory directory to copy password directory to.
+ */
+ private fun exportPasswords(targetDirectory: DocumentFile) {
+
+ val repositoryDirectory = requireNotNull(PasswordRepository.getRepositoryDirectory(applicationContext))
+ val sourcePassDir = DocumentFile.fromFile(repositoryDirectory)
+
+ d { "Copying ${repositoryDirectory.path} to $targetDirectory" }
+
+ val dateString = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ LocalDateTime
+ .now()
+ .format(DateTimeFormatter.ISO_DATE_TIME)
+ } else {
+ String.format("%tFT%<tRZ", Calendar.getInstance(TimeZone.getTimeZone("Z")))
+ }
+
+ val passDir = targetDirectory.createDirectory("password_store_$dateString")
+
+ if (passDir != null) {
+ copyDirToDir(sourcePassDir, passDir)
+ }
+ }
+
+ /**
+ * Copies a password file to a given directory.
+ *
+ * Note: this does not preserve last modified time.
+ *
+ * @param passwordFile password file to copy.
+ * @param targetDirectory target directory to copy password.
+ */
+ private fun copyFileToDir(passwordFile: DocumentFile, targetDirectory: DocumentFile) {
+ val sourceInputStream = contentResolver.openInputStream(passwordFile.uri)
+ val name = passwordFile.name
+ val targetPasswordFile = targetDirectory.createFile("application/octet-stream", name!!)
+ if (targetPasswordFile?.exists() == true) {
+ val destOutputStream = contentResolver.openOutputStream(targetPasswordFile.uri)
+
+ if (destOutputStream != null && sourceInputStream != null) {
+ sourceInputStream.copyTo(destOutputStream, 1024)
+
+ sourceInputStream.close()
+ destOutputStream.close()
+ }
+ }
+ }
+
+ /**
+ * Recursively copies a directory to a destination.
+ *
+ * @param sourceDirectory directory to copy from.
+ * @param targetDirectory directory to copy to.
+ */
+ private fun copyDirToDir(sourceDirectory: DocumentFile, targetDirectory: DocumentFile) {
+ sourceDirectory.listFiles().forEach { file ->
+ if (file.isDirectory) {
+ // Create new directory and recurse
+ val newDir = targetDirectory.createDirectory(file.name!!)
+ copyDirToDir(file, newDir!!)
+ } else {
+ copyFileToDir(file, targetDirectory)
+ }
+ }
+ }
+
+ private fun createNotification() {
+ createNotificationChannel()
+
+ val notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.exporting_passwords))
+ .setSmallIcon(R.drawable.ic_round_import_export)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .build()
+
+ startForeground(2, notification)
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val serviceChannel = NotificationChannel(
+ CHANNEL_ID,
+ getString(R.string.app_name),
+ NotificationManager.IMPORTANCE_LOW
+ )
+ val manager = getSystemService<NotificationManager>()
+ if (manager != null) {
+ manager.createNotificationChannel(serviceChannel)
+ } else {
+ d { "Failed to create notification channel" }
+ }
+ }
+ }
+
+ companion object {
+
+ const val ACTION_EXPORT_PASSWORD = "ACTION_EXPORT_PASSWORD"
+ private const val CHANNEL_ID = "NotificationService"
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
index 9f444d45..950b6523 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
@@ -59,11 +59,7 @@ import com.zeapo.pwdstore.utils.autofillManager
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import java.io.File
import java.io.IOException
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
-import java.util.Calendar
import java.util.HashSet
-import java.util.TimeZone
import me.msfjarvis.openpgpktx.util.OpenPgpUtils
typealias ClickListener = Preference.OnPreferenceClickListener
@@ -643,6 +639,13 @@ class UserPreference : AppCompatActivity() {
* Exports the passwords
*/
private fun exportPasswords() {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
+ flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
+ Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
+ }
+
registerForActivityResult(StartActivityForResult()) { result ->
if (!validateResult(result)) return@registerForActivityResult
val uri = result.data?.data
@@ -651,10 +654,19 @@ class UserPreference : AppCompatActivity() {
val targetDirectory = DocumentFile.fromTreeUri(applicationContext, uri)
if (targetDirectory != null) {
- exportPasswords(targetDirectory)
+ val service = Intent(applicationContext, PasswordExportService::class.java).apply {
+ action = PasswordExportService.ACTION_EXPORT_PASSWORD
+ putExtra("uri", uri)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(service)
+ } else {
+ startService(service)
+ }
}
}
- }.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
+ }.launch(intent)
}
/**
@@ -772,77 +784,6 @@ class UserPreference : AppCompatActivity() {
return autofillManager?.hasEnabledAutofillServices() == true
}
- /**
- * Exports passwords to the given directory.
- *
- * Recursively copies the existing password store to an external directory.
- *
- * @param targetDirectory directory to copy password directory to.
- */
- private fun exportPasswords(targetDirectory: DocumentFile) {
-
- val repositoryDirectory = requireNotNull(PasswordRepository.getRepositoryDirectory(applicationContext))
- val sourcePassDir = DocumentFile.fromFile(repositoryDirectory)
-
- tag(TAG).d { "Copying ${repositoryDirectory.path} to $targetDirectory" }
-
- val dateString = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- LocalDateTime
- .now()
- .format(DateTimeFormatter.ISO_DATE_TIME)
- } else {
- String.format("%tFT%<tRZ", Calendar.getInstance(TimeZone.getTimeZone("Z")))
- }
-
- val passDir = targetDirectory.createDirectory("password_store_$dateString")
-
- if (passDir != null) {
- copyDirToDir(sourcePassDir, passDir)
- }
- }
-
- /**
- * Copies a password file to a given directory.
- *
- * Note: this does not preserve last modified time.
- *
- * @param passwordFile password file to copy.
- * @param targetDirectory target directory to copy password.
- */
- private fun copyFileToDir(passwordFile: DocumentFile, targetDirectory: DocumentFile) {
- val sourceInputStream = contentResolver.openInputStream(passwordFile.uri)
- val name = passwordFile.name
- val targetPasswordFile = targetDirectory.createFile("application/octet-stream", name!!)
- if (targetPasswordFile?.exists() == true) {
- val destOutputStream = contentResolver.openOutputStream(targetPasswordFile.uri)
-
- if (destOutputStream != null && sourceInputStream != null) {
- sourceInputStream.copyTo(destOutputStream, 1024)
-
- sourceInputStream.close()
- destOutputStream.close()
- }
- }
- }
-
- /**
- * Recursively copies a directory to a destination.
- *
- * @param sourceDirectory directory to copy from.
- * @param sourceDirectory directory to copy to.
- */
- private fun copyDirToDir(sourceDirectory: DocumentFile, targetDirectory: DocumentFile) {
- sourceDirectory.listFiles().forEach { file ->
- if (file.isDirectory) {
- // Create new directory and recurse
- val newDir = targetDirectory.createDirectory(file.name!!)
- copyDirToDir(file, newDir!!)
- } else {
- copyFileToDir(file, targetDirectory)
- }
- }
- }
-
companion object {
private const val TAG = "UserPreference"
diff --git a/app/src/main/res/drawable/ic_round_import_export.xml b/app/src/main/res/drawable/ic_round_import_export.xml
new file mode 100644
index 00000000..d732ca2f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_round_import_export.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M8.65,3.35L5.86,6.14c-0.32,0.31 -0.1,0.85 0.35,0.85H8V13c0,0.55 0.45,1 1,1s1,-0.45 1,-1V6.99h1.79c0.45,0 0.67,-0.54 0.35,-0.85L9.35,3.35c-0.19,-0.19 -0.51,-0.19 -0.7,0zM16,17.01V11c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v6.01h-1.79c-0.45,0 -0.67,0.54 -0.35,0.85l2.79,2.78c0.2,0.19 0.51,0.19 0.71,0l2.79,-2.78c0.32,-0.31 0.09,-0.85 -0.35,-0.85H16z"/>
+</vector>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 01b63579..625cf440 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -400,4 +400,5 @@
<string name="otp_import_failure">Failed to import TOTP configuration</string>
<string name="oreo_autofill_chrome_compat_fix_preference_title">Improve reliability in Chrome</string>
<string name="oreo_autofill_chrome_compat_fix_preference_summary">Requires activating an accessibility service and may affect overall Chrome performance</string>
+ <string name="exporting_passwords">Exporting passwords…</string>
</resources>