diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2020-07-09 14:00:24 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-09 14:00:24 +0530 |
commit | fc00de61dcc53f8378e332f7db1d5c58e3308aa7 (patch) | |
tree | e303a6b77f72ae4cbe7d0d16141fe07f822b97a9 | |
parent | 0ead6b2a4dfc409ffdffd5ef34c860aaa7f378ae (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.md | 1 | ||||
-rw-r--r-- | app/src/main/AndroidManifest.xml | 3 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordExportService.kt | 152 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/UserPreference.kt | 95 | ||||
-rw-r--r-- | app/src/main/res/drawable/ic_round_import_export.xml | 10 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 1 |
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> |