We do not need write storage permission to create a Txt file with the intent Intent.ACTION_CREATE_DOCUMENT

This commit is contained in:
Benoit Marty 2020-07-11 12:25:31 +02:00
parent 4741169cc7
commit 4387fd3327
9 changed files with 94 additions and 105 deletions

View file

@ -25,7 +25,7 @@ Build 🧱:
- Revert to build-tools 3.5.3
Other changes:
-
- Use Intent.ACTION_CREATE_DOCUMENT to save megolm key or recovery key in a txt file
Changes in Riot.imX 0.91.4 (2020-07-06)
===================================================

View file

@ -16,13 +16,12 @@
package im.vector.riotx.core.extensions
import android.content.ActivityNotFoundException
import android.content.Intent
import android.app.Activity
import android.os.Parcelable
import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast
import im.vector.riotx.core.utils.selectTxtFileToWrite
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -98,27 +97,25 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
const val POP_BACK_STACK_EXCLUSIVE = 0
fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
// We need WRITE_EXTERNAL permission
// if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
// this,
// PERMISSION_REQUEST_CODE_EXPORT_KEYS,
// R.string.permissions_rationale_msg_keys_backup_export)) {
// WRITE permissions are not needed
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let {
it.format(Date())
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(
Intent.EXTRA_TITLE,
"riot-megolm-export-$userId-$timestamp.txt"
)
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
try {
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode)
} catch (activityNotFoundException: ActivityNotFoundException) {
activity?.toast(R.string.error_no_external_application_found)
}
// }
selectTxtFileToWrite(
activity = requireActivity(),
fragment = this,
defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
requestCode = requestCode
)
}
fun Activity.queryExportKeys(userId: String, requestCode: Int) {
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
selectTxtFileToWrite(
activity = this,
fragment = null,
defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
requestCode = requestCode
)
}

View file

@ -424,6 +424,33 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
}
}
/**
* Ask the user to select a location and a file name to write in
*/
fun selectTxtFileToWrite(
activity: Activity,
fragment: Fragment?,
defaultFileName: String,
chooserHint: String,
requestCode: Int
) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, defaultFileName)
try {
val chooserIntent = Intent.createChooser(intent, chooserHint)
if (fragment != null) {
fragment.startActivityForResult(chooserIntent, requestCode)
} else {
activity.startActivityForResult(chooserIntent, requestCode)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
activity.toast(R.string.error_no_external_application_found)
}
}
// ==============================================================================================================
// Media utils
// ==============================================================================================================

View file

@ -63,7 +63,6 @@ const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA = 569
const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA = 570
const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571
const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576

View file

@ -16,7 +16,6 @@
package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
@ -27,12 +26,9 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.queryExportKeys
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter
@ -97,7 +93,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
.show()
}
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
exportKeysManually()
queryExportKeys(session.myUserId, REQUEST_CODE_SAVE_MEGOLM_EXPORT)
}
}
}
@ -129,38 +125,6 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
})
}
private fun exportKeysManually() {
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export)) {
try {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
startActivityForResult(
Intent.createChooser(
intent,
getString(R.string.keys_backup_setup_step1_manual_export)
),
REQUEST_CODE_SAVE_MEGOLM_EXPORT
)
} catch (activityNotFoundException: ActivityNotFoundException) {
toast(R.string.error_no_external_application_found)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
exportKeysManually()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
val uri = data?.data

View file

@ -48,6 +48,9 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
lateinit var session: Session
val userId: String
get() = session.myUserId
var showManualExport: MutableLiveData<Boolean> = MutableLiveData()
var navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()

View file

@ -15,8 +15,10 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.widget.Button
import android.widget.TextView
@ -29,25 +31,27 @@ import butterknife.BindView
import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotx.R
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.files.writeToFile
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.selectTxtFileToWrite
import im.vector.riotx.core.utils.startSharePlainTextIntent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() {
companion object {
private const val SAVE_RECOVERY_KEY_REQUEST_CODE = 2754
}
override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3
@BindView(R.id.keys_backup_setup_step3_button)
@ -130,15 +134,15 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
val permissionsChecked = checkPermissions(
PERMISSIONS_FOR_WRITING_FILES,
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export
val userId = viewModel.userId
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
selectTxtFileToWrite(
activity = requireActivity(),
fragment = this,
defaultFileName = "recovery-key-$userId-$timestamp.txt",
chooserHint = getString(R.string.save_recovery_key_chooser_hint),
requestCode = SAVE_RECOVERY_KEY_REQUEST_CODE
)
if (permissionsChecked) {
exportRecoveryKeyToFile(recoveryKey)
}
dialog.dismiss()
}
@ -163,19 +167,19 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
}
private fun exportRecoveryKeyToFile(data: String) {
private fun exportRecoveryKeyToFile(uri: Uri, data: String) {
GlobalScope.launch(Dispatchers.Main) {
Try {
withContext(Dispatchers.IO) {
val parentDir = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
writeToFile(data, file)
addEntryToDownloadManager(requireContext(), file, "text/plain")
file.absolutePath
requireContext().contentResolver.openOutputStream(uri)
?.use { os ->
os.write(data.toByteArray())
os.flush()
}
}?.let {
uri.toString()
}
?: throw IOException()
}
.fold(
{ throwable ->
@ -200,11 +204,14 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
viewModel.recoveryKey.value?.let {
exportRecoveryKeyToFile(it)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
SAVE_RECOVERY_KEY_REQUEST_CODE -> {
val uri = data?.data
if (resultCode == Activity.RESULT_OK && uri != null) {
viewModel.recoveryKey.value?.let {
exportRecoveryKeyToFile(uri, it)
}
}
}
}

View file

@ -42,8 +42,6 @@ import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.openFileSelection
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter
@ -142,14 +140,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
mCrossSigningStatePreference.isVisible = true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {

View file

@ -2526,4 +2526,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="crypto_error_withheld_unverified">You cannot access this message because your session is not trusted by the sender</string>
<string name="crypto_error_withheld_generic">You cannot access this message because the sender purposely did not send the keys</string>
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
<string name="save_recovery_key_chooser_hint">Save recovery key in</string>
</resources>