From fc00de61dcc53f8378e332f7db1d5c58e3308aa7 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 9 Jul 2020 14:00:24 +0530 Subject: Move password export to the IO dispatcher (#918) * Move password export to the IO dispatcher Signed-off-by: Harsh Shandilya * Simplify snackbars and disable exit operations during export Signed-off-by: Harsh Shandilya * Move export password logic to service Signed-off-by: Aditya Wasan * Reformat Signed-off-by: Harsh Shandilya * Use explicit null check Signed-off-by: Harsh Shandilya * Remove unneeded hack Signed-off-by: Harsh Shandilya * Fixup strings Signed-off-by: Harsh Shandilya * Don't use coroutines in a service Signed-off-by: Aditya Wasan * Update notification icon Signed-off-by: Aditya Wasan * Rollback unwanted formatting Signed-off-by: Harsh Shandilya Co-authored-by: Aditya Wasan --- CHANGELOG.md | 1 + app/src/main/AndroidManifest.xml | 3 + .../com/zeapo/pwdstore/PasswordExportService.kt | 152 +++++++++++++++++++++ .../main/java/com/zeapo/pwdstore/UserPreference.kt | 95 +++---------- .../main/res/drawable/ic_round_import_export.xml | 10 ++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 185 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/com/zeapo/pwdstore/PasswordExportService.kt create mode 100644 app/src/main/res/drawable/ic_round_import_export.xml 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 @@ -115,6 +115,9 @@ + 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") + 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% + 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() + 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% - 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 @@ + + + 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 @@ Failed to import TOTP configuration Improve reliability in Chrome Requires activating an accessibility service and may affect overall Chrome performance + Exporting passwords… -- cgit v1.2.3