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 - Revert to build-tools 3.5.3
Other changes: 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) Changes in Riot.imX 0.91.4 (2020-07-06)
=================================================== ===================================================

View File

@ -16,13 +16,12 @@
package im.vector.riotx.core.extensions package im.vector.riotx.core.extensions
import android.content.ActivityNotFoundException import android.app.Activity
import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment 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.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -98,27 +97,25 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
const val POP_BACK_STACK_EXCLUSIVE = 0 const val POP_BACK_STACK_EXCLUSIVE = 0
fun Fragment.queryExportKeys(userId: String, requestCode: Int) { fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
// We need WRITE_EXTERNAL permission val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
// 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"
)
try { selectTxtFileToWrite(
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode) activity = requireActivity(),
} catch (activityNotFoundException: ActivityNotFoundException) { fragment = this,
activity?.toast(R.string.error_no_external_application_found) 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 // 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_LAUNCH_NATIVE_VIDEO_CAMERA = 570
const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571 const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571
const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572 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_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576 const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.crypto.keysbackup.setup package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AlertDialog 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.R
import im.vector.riotx.core.dialogs.ExportKeysDialog import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.extensions.observeEvent 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.extensions.replaceFragment
import im.vector.riotx.core.platform.SimpleFragmentActivity 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.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter import im.vector.riotx.features.crypto.keys.KeysExporter
@ -97,7 +93,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
.show() .show()
} }
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> { 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) { if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
val uri = data?.data val uri = data?.data

View File

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

View File

@ -15,8 +15,10 @@
*/ */
package im.vector.riotx.features.crypto.keysbackup.setup 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.Bundle
import android.os.Environment
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
@ -29,25 +31,27 @@ import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotx.R 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.platform.VectorBaseFragment
import im.vector.riotx.core.utils.LiveEvent 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.copyToClipboard
import im.vector.riotx.core.utils.selectTxtFileToWrite
import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.core.utils.startSharePlainTextIntent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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 import javax.inject.Inject
class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() { 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 override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3
@BindView(R.id.keys_backup_setup_step3_button) @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 { dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
val permissionsChecked = checkPermissions( val userId = viewModel.userId
PERMISSIONS_FOR_WRITING_FILES, val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
this, selectTxtFileToWrite(
PERMISSION_REQUEST_CODE_EXPORT_KEYS, activity = requireActivity(),
R.string.permissions_rationale_msg_keys_backup_export 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() 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) { GlobalScope.launch(Dispatchers.Main) {
Try { Try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val parentDir = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) requireContext().contentResolver.openOutputStream(uri)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt") ?.use { os ->
os.write(data.toByteArray())
writeToFile(data, file) os.flush()
}
addEntryToDownloadManager(requireContext(), file, "text/plain") }?.let {
uri.toString()
file.absolutePath
} }
?: throw IOException()
} }
.fold( .fold(
{ throwable -> { throwable ->
@ -200,11 +204,14 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (allGranted(grantResults)) { when (requestCode) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { SAVE_RECOVERY_KEY_REQUEST_CODE -> {
viewModel.recoveryKey.value?.let { val uri = data?.data
exportRecoveryKeyToFile(it) 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.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.VectorPreference 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.openFileSelection
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter import im.vector.riotx.features.crypto.keys.KeysExporter
@ -142,14 +140,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
mCrossSigningStatePreference.isVisible = true 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) { 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_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="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="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
<string name="save_recovery_key_chooser_hint">Save recovery key in</string>
</resources> </resources>