From c5bc0267916596902b5e69b07162e3dfbfdb6775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Tue, 21 Nov 2023 23:46:54 +0100 Subject: [PATCH] we can now store the file where the user wants to --- .../duckduckgo/sync/impl/RecoveryCodePDF.kt | 49 +++++++++++++++++++ .../com/duckduckgo/sync/impl/ShareAction.kt | 24 +++++++++ .../duckduckgo/sync/impl/ui/SyncActivity.kt | 11 ++--- .../sync/impl/ui/SyncActivityViewModel.kt | 11 +++-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/RecoveryCodePDF.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/RecoveryCodePDF.kt index c589c0b81c85..c9744616518e 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/RecoveryCodePDF.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/RecoveryCodePDF.kt @@ -19,6 +19,7 @@ package com.duckduckgo.sync.impl import android.content.Context import android.graphics.pdf.PdfDocument import android.graphics.pdf.PdfDocument.PageInfo.Builder +import android.net.Uri import android.os.Environment import android.view.LayoutInflater import android.view.View @@ -28,11 +29,22 @@ import com.duckduckgo.common.utils.checkMainThread import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.sync.impl.databinding.ViewRecoveryCodeBinding import com.squareup.anvil.annotations.ContributesBinding +import timber.log.Timber import java.io.File +import java.io.FileNotFoundException import java.io.FileOutputStream +import java.io.IOException import javax.inject.* interface RecoveryCodePDF { + + @WorkerThread + fun storeRecoveryCodePDF( + viewContext: Context, + recoveryCodeB64: String, + uri: Uri, + ): Uri + @WorkerThread fun generateAndStoreRecoveryCodePDF( viewContext: Context, @@ -44,6 +56,43 @@ interface RecoveryCodePDF { class RecoveryCodePDFImpl @Inject constructor( private val qrEncoder: QREncoder, ) : RecoveryCodePDF { + override fun storeRecoveryCodePDF( + viewContext: Context, + recoveryCodeB64: String, + uri: Uri + ): Uri { + checkMainThread() + + val bitmapQR = qrEncoder.encodeAsBitmap(recoveryCodeB64, R.dimen.qrSizeLarge, R.dimen.qrSizeLarge) + val pdfDocument = PdfDocument() + val inflater = LayoutInflater.from(viewContext) + val page = pdfDocument.startPage(Builder(a4PageWidth.toPx(), a4PageHeight.toPx(), 1).create()) + ViewRecoveryCodeBinding.inflate(inflater, null, false).apply { + this.qrCodeImageView.setImageBitmap(bitmapQR) + this.recoveryCodeText.text = recoveryCodeB64 + val measureWidth: Int = View.MeasureSpec.makeMeasureSpec(page.canvas.width, View.MeasureSpec.EXACTLY) + val measuredHeight: Int = View.MeasureSpec.makeMeasureSpec(page.canvas.height, View.MeasureSpec.EXACTLY) + this.root.measure(measureWidth, measuredHeight) + this.root.layout(0, 0, page.canvas.width, page.canvas.height) + this.root.draw(page.canvas) + } + pdfDocument.finishPage(page) + + try { + viewContext.contentResolver.openFileDescriptor(uri, "w")?.use { parcelFileDescriptor -> + FileOutputStream(parcelFileDescriptor.fileDescriptor).use { fileOutputStream -> + pdfDocument.writeTo(fileOutputStream) + pdfDocument.close() + } + } + } catch (e: FileNotFoundException) { + Timber.d("Sync: Pdf FileNotFoundException $e") + } catch (e: IOException) { + Timber.d("Sync: Pdf IOException $e") + } + + return uri + } override fun generateAndStoreRecoveryCodePDF( viewContext: Context, diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ShareAction.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ShareAction.kt index ddd96d995333..940f1fa616dc 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ShareAction.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ShareAction.kt @@ -30,7 +30,31 @@ import timber.log.Timber class ShareAction @Inject constructor(private val appBuildConfig: AppBuildConfig) { + fun shareFileUri(applicationContext: Context, uri: Uri): Boolean { + val intent = createShareUriIntent(applicationContext, uri) + return if (intent != null) startActivity(applicationContext, intent) else false + } + private fun createShareUriIntent(applicationContext: Context, uri: Uri): Intent? { + val intent = + Intent().apply { + setDataAndType(uri, applicationContext.contentResolver?.getType(uri)) + action = Intent.ACTION_SEND + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + putExtra(Intent.EXTRA_STREAM, uri) + } + return Intent.createChooser( + intent, + applicationContext.getString(R.string.sync_share_title), + ) + .apply { + if (appBuildConfig.sdkInt >= Build.VERSION_CODES.Q) { + // Show a thumbnail preview of the file to be shared on Android Q and above. + clipData = ClipData.newRawUri(uri.toString(), uri) + } + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } + } fun shareFile(applicationContext: Context, file: File): Boolean { val intent = createShareIntent(applicationContext, file) diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt index e2652b638e17..97aa7ef00a5c 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt @@ -18,9 +18,7 @@ package com.duckduckgo.sync.impl.ui import android.app.Activity import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.provider.DocumentsContract import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle @@ -130,9 +128,8 @@ class SyncActivity : DuckDuckGoActivity() { private val savePDFLauncher = registerForActivityResult(StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { - Timber.d("Sync: Pdf location chosen") result.data?.let { - viewModel.onPdfLocationChosen(it.data!!) + viewModel.onPdfLocationChosen(this@SyncActivity, it.data!!) } } } @@ -231,7 +228,7 @@ class SyncActivity : DuckDuckGoActivity() { is AskTurnOffSync -> askTurnOffsync(it.device) is AskDeleteAccount -> askDeleteAccount() is RecoveryCodePDFSuccess -> { - shareAction.shareFile(this@SyncActivity, it.recoveryCodePDFFile) + shareAction.shareFileUri(this@SyncActivity, it.recoveryCodePDFUri) } is CheckIfUserHasStoragePermission -> { @@ -243,11 +240,11 @@ class SyncActivity : DuckDuckGoActivity() { is AskRemoveDevice -> askRemoveDevice(it.device) is AskEditDevice -> askEditDevice(it.device) is ShowTextCode -> startActivity(ShowCodeActivity.intent(this)) - is AskPDFLocation -> createFile() + is AskPDFLocation -> askForPdfLocation() } } - private fun createFile() { + private fun askForPdfLocation() { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivityViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivityViewModel.kt index eda99b4d75e0..a05eeb70deac 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivityViewModel.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivityViewModel.kt @@ -140,7 +140,7 @@ class SyncActivityViewModel @Inject constructor( data class AskTurnOffSync(val device: ConnectedDevice) : Command() object AskDeleteAccount : Command() object CheckIfUserHasStoragePermission : Command() - data class RecoveryCodePDFSuccess(val recoveryCodePDFFile: File) : Command() + data class RecoveryCodePDFSuccess(val recoveryCodePDFUri: Uri) : Command() object AskPDFLocation : Command() data class AskRemoveDevice(val device: ConnectedDevice) : Command() data class AskEditDevice(val device: ConnectedDevice) : Command() @@ -229,9 +229,14 @@ class SyncActivityViewModel @Inject constructor( showAccountDetailsIfNeeded() } - fun onPdfLocationChosen(fileLocation: Uri) { + fun onPdfLocationChosen(viewContext: Context, fileLocation: Uri) { Timber.d("Sync: Pdf location chosen $fileLocation") - + viewModelScope.launch(dispatchers.io()) { + val recoveryCodeB64 = syncAccountRepository.getRecoveryCode() ?: return@launch + val generateRecoveryCodePDF = recoveryCodePDF.storeRecoveryCodePDF(viewContext, recoveryCodeB64, fileLocation) + // should return a result (error, Uri) + command.send(RecoveryCodePDFSuccess(generateRecoveryCodePDF)) + } } fun onDeleteAccountClicked() {