Merge pull request #1661 from vector-im/feature/create_file_intent

Feature/create file intent
This commit is contained in:
Benoit Marty 2020-07-11 12:28:15 +02:00 committed by GitHub
commit 25bbe9c3d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 103 additions and 111 deletions

View File

@ -13,6 +13,7 @@ Improvements 🙌:
Bugfix 🐛:
- Regression | Share action menu do not work (#1647)
- verification issues on transition (#1555)
- Fix issue when restoring keys backup using recovery key
Translations 🗣:
-
@ -25,6 +26,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
- Use `Context#withStyledAttributes` extension function (#1546)
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

@ -49,7 +49,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
val recoveryKey = recoveryCode.value!!
try {
sharedViewModel.recoverUsingBackupPass(recoveryKey)
sharedViewModel.recoverUsingBackupRecoveryKey(recoveryKey)
} catch (failure: Throwable) {
recoveryCodeErrorText.postValue(stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt))
}

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,34 +167,33 @@ 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()
}
}
?: throw IOException("Unable to write the file")
}
.fold(
{ throwable ->
context?.let {
activity?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
.setMessage(throwable.localizedMessage)
.setMessage(errorFormatter.toHumanReadable(throwable))
}
},
{ path ->
{
viewModel.copyHasBeenMade = true
context?.let {
activity?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
.setTitle(R.string.dialog_title_success)
.setMessage(R.string.recovery_key_export_saved)
}
}
)
@ -200,11 +203,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

@ -126,6 +126,7 @@
<string name="dialog_title_confirmation">Confirmation</string>
<string name="dialog_title_warning">Warning</string>
<string name="dialog_title_error">Error</string>
<string name="dialog_title_success">Success</string>
<!-- Bottom navigation buttons -->
<string name="bottom_action_home">Home</string>
@ -1413,6 +1414,7 @@ Why choose Riot.im?
<string name="keys_backup_setup_step3_share_recovery_file">Share</string>
<string name="keys_backup_setup_step3_save_button_title">Save as File</string>
<string name="recovery_key_export_saved_as_warning">The recovery key has been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.</string>
<string name="recovery_key_export_saved">The recovery key has been saved.</string>
<string name="keys_backup_setup_override_backup_prompt_tile">A backup already exist on your HomeServer</string>
<string name="keys_backup_setup_override_backup_prompt_description">It looks like you already have setup key backup from another session. Do you want to replace it with the one youre creating?</string>
@ -2526,4 +2528,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>