Create extension function `Context.safeOpenOutputStream`
This commit is contained in:
parent
b9b5cab772
commit
29c7ea11bd
|
@ -18,6 +18,7 @@ package im.vector.app.core.extensions
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ImageSpan
|
||||
|
@ -31,6 +32,7 @@ import androidx.datastore.preferences.core.Preferences
|
|||
import dagger.hilt.EntryPoints
|
||||
import im.vector.app.core.datastore.dataStoreProvider
|
||||
import im.vector.app.core.di.SingletonEntryPoint
|
||||
import java.io.OutputStream
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Context.singletonEntryPoint(): SingletonEntryPoint {
|
||||
|
@ -68,3 +70,10 @@ private fun Float.toAndroidAlpha(): Int {
|
|||
}
|
||||
|
||||
val Context.dataStoreProvider: (String) -> DataStore<Preferences> by dataStoreProvider()
|
||||
|
||||
/**
|
||||
* Open Uri in truncate mode to make sure we don't partially overwrite content when we get passed a Uri to an existing file.
|
||||
*/
|
||||
fun Context.safeOpenOutputStream(uri: Uri): OutputStream? {
|
||||
return contentResolver.openOutputStream(uri, "wt")
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.crypto.keys
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import im.vector.app.core.dispatchers.CoroutineDispatchers
|
||||
import im.vector.app.core.extensions.safeOpenOutputStream
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
@ -34,7 +35,7 @@ class KeysExporter @Inject constructor(
|
|||
suspend fun export(password: String, uri: Uri) {
|
||||
withContext(dispatchers.io) {
|
||||
val data = session.cryptoService().exportRoomKeys(password)
|
||||
context.contentResolver.openOutputStream(uri, "wt")
|
||||
context.safeOpenOutputStream(uri)
|
||||
?.use { it.write(data) }
|
||||
?: throw IllegalStateException("Unable to open file for writing")
|
||||
verifyExportedKeysOutputFileSize(uri, expectedSize = data.size.toLong())
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.extensions.safeOpenOutputStream
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.LiveEvent
|
||||
import im.vector.app.core.utils.copyToClipboard
|
||||
|
@ -165,7 +166,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment<Fr
|
|||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
Try {
|
||||
withContext(Dispatchers.IO) {
|
||||
requireContext().contentResolver.openOutputStream(uri, "wt")
|
||||
requireContext().safeOpenOutputStream(uri)
|
||||
?.use { os ->
|
||||
os.write(data.toByteArray())
|
||||
os.flush()
|
||||
|
|
|
@ -29,6 +29,7 @@ import com.airbnb.mvrx.parentFragmentViewModel
|
|||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.extensions.safeOpenOutputStream
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.utils.startSharePlainTextIntent
|
||||
|
@ -81,7 +82,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor(
|
|||
val uri = activityResult.data?.data ?: return@registerStartForActivityResult
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
sharedViewModel.handle(BootstrapActions.SaveKeyToUri(requireContext().contentResolver!!.openOutputStream(uri, "wt")!!))
|
||||
sharedViewModel.handle(BootstrapActions.SaveKeyToUri(requireContext().safeOpenOutputStream(uri)!!))
|
||||
} catch (failure: Throwable) {
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqFailed)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.airbnb.mvrx.withState
|
|||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.extensions.safeOpenOutputStream
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.selectTxtFileToWrite
|
||||
import im.vector.app.databinding.FragmentDevtoolKeyrequestsBinding
|
||||
|
@ -106,7 +107,7 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment<FragmentDev
|
|||
when (it) {
|
||||
is KeyRequestEvents.SaveAudit -> {
|
||||
tryOrNull {
|
||||
requireContext().contentResolver?.openOutputStream(it.uri, "wt")
|
||||
requireContext().safeOpenOutputStream(it.uri)
|
||||
?.use { os -> os.write(it.raw.toByteArray()) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ class KeysExporterTest {
|
|||
@Test
|
||||
fun `when exporting then writes exported keys to context output stream`() {
|
||||
givenFileDescriptorWithSize(size = A_ROOM_KEYS_EXPORT.size.toLong())
|
||||
val outputStream = context.givenOutputStreamFor(A_URI, mode = "wt")
|
||||
val outputStream = context.givenSafeOutputStreamFor(A_URI)
|
||||
|
||||
runTest { keysExporter.export(A_PASSWORD, A_URI) }
|
||||
|
||||
|
@ -63,7 +63,7 @@ class KeysExporterTest {
|
|||
@Test
|
||||
fun `given different file size returned for export when exporting then throws UnexpectedExportKeysFileSizeException`() {
|
||||
givenFileDescriptorWithSize(size = 110)
|
||||
context.givenOutputStreamFor(A_URI, mode = "wt")
|
||||
context.givenSafeOutputStreamFor(A_URI)
|
||||
|
||||
assertFailsWith<UnexpectedExportKeysFileSizeException> {
|
||||
runTest { keysExporter.export(A_PASSWORD, A_URI) }
|
||||
|
@ -72,7 +72,7 @@ class KeysExporterTest {
|
|||
|
||||
@Test
|
||||
fun `given output stream is unavailable for exporting to when exporting then throws IllegalStateException`() {
|
||||
context.givenMissingOutputStreamFor(A_URI, mode = "wt")
|
||||
context.givenMissingSafeOutputStreamFor(A_URI)
|
||||
|
||||
assertFailsWith<IllegalStateException>(message = "Unable to open file for writing") {
|
||||
runTest { keysExporter.export(A_PASSWORD, A_URI) }
|
||||
|
@ -82,7 +82,7 @@ class KeysExporterTest {
|
|||
@Test
|
||||
fun `given exported file is missing after export when exporting then throws IllegalStateException`() {
|
||||
context.givenFileDescriptor(A_URI, mode = "r") { null }
|
||||
context.givenOutputStreamFor(A_URI, mode = "wt")
|
||||
context.givenSafeOutputStreamFor(A_URI)
|
||||
|
||||
assertFailsWith<IllegalStateException>(message = "Exported file not found") {
|
||||
runTest { keysExporter.export(A_PASSWORD, A_URI) }
|
||||
|
|
|
@ -39,13 +39,13 @@ class FakeContext(
|
|||
every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor
|
||||
}
|
||||
|
||||
fun givenOutputStreamFor(uri: Uri, mode: String): OutputStream {
|
||||
fun givenSafeOutputStreamFor(uri: Uri): OutputStream {
|
||||
val outputStream = mockk<OutputStream>(relaxed = true)
|
||||
every { contentResolver.openOutputStream(uri, mode) } returns outputStream
|
||||
every { contentResolver.openOutputStream(uri, "wt") } returns outputStream
|
||||
return outputStream
|
||||
}
|
||||
|
||||
fun givenMissingOutputStreamFor(uri: Uri, mode: String) {
|
||||
every { contentResolver.openOutputStream(uri, mode) } returns null
|
||||
fun givenMissingSafeOutputStreamFor(uri: Uri) {
|
||||
every { contentResolver.openOutputStream(uri, "wt") } returns null
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue