Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move password export to the IO dispatcher #918

Merged
merged 13 commits into from
Jul 9, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
<service
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">
Expand Down
152 changes: 152 additions & 0 deletions app/src/main/java/com/zeapo/pwdstore/PasswordExportService.kt
Original file line number Diff line number Diff line change
@@ -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"
}
}
95 changes: 18 additions & 77 deletions app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

package mozilla.components.lib.publicsuffixlist

import mozilla.components.lib.publicsuffixlist.ext.binarySearch
msfjarvis marked this conversation as resolved.
Show resolved Hide resolved
import java.net.IDN
import mozilla.components.lib.publicsuffixlist.ext.binarySearch

/**
* Class wrapping the public suffix list data and offering methods for accessing rules in it.
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_round_import_export.xml
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>