Skip to content

Commit

Permalink
Clean up old backup file after restore to prevent permission issues
Browse files Browse the repository at this point in the history
  • Loading branch information
dpad85 committed Apr 30, 2024
1 parent 3cec344 commit 9fa1634
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

package fr.acinq.phoenix.android.initwallet.restore

import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
Expand Down Expand Up @@ -52,7 +57,16 @@ fun RestorePaymentsBackupView(
BackHandler { /* Disable back button */ }

val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
contract = object : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY))
} else {
intent
}
}
},
onResult = { uri ->
if (uri != null) {
vm.restorePaymentsBackup(context, words = words, uri = uri, onBackupRestoreDone = onBackupRestoreDone)
Expand All @@ -67,10 +81,10 @@ fun RestorePaymentsBackupView(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(text = "You can manually restore your payments history by importing a Phoenix backup file, if one exists.")
Text(text = "Look for a phoenix.bak file in your Documents folder. Note that older versions of Phoenix (before v2.3.0) did not generate backups.")
Text(text = "Look for a phoenix.bak file in your Documents folder.")
}

Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(32.dp))

Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package fr.acinq.phoenix.android.initwallet.restore

import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
Expand Down Expand Up @@ -163,6 +164,10 @@ class RestoreWalletViewModel: InitWalletViewModel() {
LocalBackupHelper.restoreDbFile(context, paymentsDbEntry.key, paymentsDbEntry.value)
log.info("payments db has been restored")
restoreBackupState = RestoreBackupState.Done.BackupRestored
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
LocalBackupHelper.cleanUpOldBackupFile(context, keyManager, encryptedBackup, uri)
log.debug("old backup file cleaned up")
}
delay(1000)
viewModelScope.launch(Dispatchers.Main) {
onBackupRestoreDone()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ import kotlinx.coroutines.flow.first
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration

class LocalBackupWorker(val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {

Expand All @@ -49,6 +47,7 @@ class LocalBackupWorker(val context: Context, workerParams: WorkerParameters) :
val keyManager = business.walletManager.keyManager.filterNotNull().first()
LocalBackupHelper.saveBackupToDisk(context, keyManager)
log.info("successfully saved backup file to disk")

Result.success()
} catch (e: Exception) {
log.error("error when processing local-backup job: ", e)
Expand All @@ -71,7 +70,7 @@ class LocalBackupWorker(val context: Context, workerParams: WorkerParameters) :
/** Schedule a local-backup-worker job to run once. Existing schedules are replaced. */
fun scheduleOnce(context: Context) {
log.info("scheduling local-backup once")
val work = OneTimeWorkRequestBuilder<LocalBackupWorker>().setInitialDelay(10.seconds.toJavaDuration()).build()
val work = OneTimeWorkRequestBuilder<LocalBackupWorker>().build()
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, work)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ package fr.acinq.phoenix.android.utils.backup
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import fr.acinq.bitcoin.ByteVector
Expand Down Expand Up @@ -75,12 +77,12 @@ object LocalBackupHelper {

val bos = ByteArrayOutputStream()
ZipOutputStream(bos).use { zos ->
log.info("zipping channels db...")
log.debug("zipping channels db...")
FileInputStream(channelsDbFile).use { fis ->
zos.putNextEntry(ZipEntry(channelsDbFile.name))
zos.write(fis.readBytes())
}
log.info("zipping payments db file...")
log.debug("zipping payments db file...")
FileInputStream(paymentsDbFile).use { fis ->
zos.putNextEntry(ZipEntry(paymentsDbFile.name))
zos.write(fis.readBytes())
Expand All @@ -98,21 +100,21 @@ object LocalBackupHelper {
put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream")
put(MediaStore.MediaColumns.RELATIVE_PATH, backupDir)
}
return context.contentResolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), values)
return context.contentResolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values)
?: throw RuntimeException("failed to insert uri record for backup file")
}

@RequiresApi(Build.VERSION_CODES.Q)
private fun getBackupFileUri(context: Context, fileName: String): Pair<Long, Uri>? {
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
// columns to return -- we want the name & modified timestamp
val projection = arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DISPLAY_NAME,
MediaStore.Files.FileColumns.DATE_MODIFIED,
)
// filter on the file's name
val selection = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? AND ${MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME}"
val selection = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?"
val selectionArgs = arrayOf(fileName)
val resolver = context.contentResolver

Expand All @@ -130,7 +132,7 @@ object LocalBackupHelper {
val fileId = cursor.getLong(idColumn)
val actualFileName = cursor.getString(nameColumn)
val modifiedAt = cursor.getLong(modifiedAtColumn) * 1000
log.info("found backup file with name=$actualFileName modified_at=${modifiedAt.toAbsoluteDateTimeString()}")
log.debug("found backup file with name=$actualFileName modified_at=${modifiedAt.toAbsoluteDateTimeString()}")
modifiedAt to ContentUris.withAppendedId(contentUri, fileId)
} else {
log.info("no backup file found for name=$fileName")
Expand Down Expand Up @@ -162,17 +164,22 @@ object LocalBackupHelper {
val fileName = getBackupFileName(keyManager)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
log.debug("saving encrypted backup to public dir through mediastore api...")
val resolver = context.contentResolver

val uri = getBackupFileUri(context, fileName)?.second ?: createBackupFileUri(context, fileName)
resolver.openOutputStream(uri, "w")?.use { outputStream ->
val array = encryptedBackup.write()
outputStream.write(array)
log.debug("encrypted backup successfully saved to public dir ($uri)")
} ?: run {
log.error("public backup failed: cannot open output stream for uri=$uri")
}
saveBackupThroughMediastore(context, encryptedBackup, fileName)
}
}

@RequiresApi(Build.VERSION_CODES.Q)
private fun saveBackupThroughMediastore(context: Context, encryptedBackup: EncryptedBackup, fileName: String) {
log.debug("saving encrypted backup to public dir through mediastore api...")
val resolver = context.contentResolver

val uri = getBackupFileUri(context, fileName)?.second ?: createBackupFileUri(context, fileName)
resolver.openOutputStream(uri, "w")?.use { outputStream ->
val array = encryptedBackup.write()
outputStream.write(array)
log.debug("encrypted backup successfully saved to public dir ($uri)")
} ?: run {
log.error("public backup failed: cannot open output stream for uri=$uri")
}
}

Expand All @@ -188,12 +195,24 @@ object LocalBackupHelper {

fun resolveUriContent(context: Context, uri: Uri): EncryptedBackup? {
val resolver = context.contentResolver
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
val data = resolver.openInputStream(uri)?.use {
it.readBytes()
}
return data?.let { EncryptedBackup.read(it) }
}

@RequiresApi(Build.VERSION_CODES.Q)
fun cleanUpOldBackupFile(context: Context, keyManager: LocalKeyManager, encryptedBackup: EncryptedBackup, oldBackupUri: Uri) {
val fileName = getBackupFileName(keyManager)
val resolver = context.contentResolver
// old backup file needs to be renamed otherwise it will prevent new file from being written -- and it cannot be moved/deleted
// later since the file is not attributed to this app installation
DocumentsContract.renameDocument(resolver, oldBackupUri, "$fileName.old")
// write a new file through the mediastore API so that it's attributed to this app installation
saveBackupThroughMediastore(context, encryptedBackup, fileName)
}

/** Extracts files from zip - folders are unhandled. */
fun unzipData(data: ByteVector): Map<String, ByteArray> {
ByteArrayInputStream(data.toByteArray()).use { bis ->
Expand Down

0 comments on commit 9fa1634

Please sign in to comment.