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