We do not need write storage permission to create a Txt file with the intent Intent.ACTION_CREATE_DOCUMENT
This commit is contained in:
parent
4741169cc7
commit
4387fd3327
|
@ -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)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
// ==============================================================================================================
|
// ==============================================================================================================
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue