diff options
Diffstat (limited to 'app/src/main/java')
-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 |
2 files changed, 170 insertions, 77 deletions
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" |