Optimize I/O code using Okio - part 2 (#4372)

- Read license resource using Okio inside a coroutine (instead of the
main thread) in `LicenseActivity`
- Use Okio and its buffer system to copy ContentProvider streams and
files to a temporary file in `MediaUploader.prepareMedia()`
- Properly close the input file after copying it to a temporary file in
`MediaUploader.prepareMedia()`
- Properly close sink in case of null body source during file copy in
`Uri.copyToFolder()` in `DraftHelper.kt`
- Add comment explaining the current value of `DEFAULT_CHUNK_SIZE` in
`UriRequestBody.kt` and indent the file properly
- Replace hardcoded `Charset` and `Int` byte size with the proper
constants, and align the `hashCode()` implementation with other
`BitmapTransformation` implementations in
`CompositeWithOpaqueBackground`
- Properly close `InputStream` in case of error during Bitmap size
decoding in `getImageSquarePixels()`
- return `Int` instead of `Long` in `getImageSquarePixels()`, since the
current code simply converts the `Int` result to a `Long` _after_
multiplication and not before (and `Int.MAX_VALUE` is already way above
the maximum number of pixels a decoded Bitmap could return)
- Simplify `getImageOrientation()`
- Add explicit dependency to the Okio library and upgrade it to its
latest version.
This commit is contained in:
Christophe Beyls 2024-04-14 16:39:29 +02:00 committed by GitHub
parent 2504f42f7b
commit f69cae2315
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 77 additions and 76 deletions

View File

@ -149,6 +149,7 @@ dependencies {
implementation libs.networkresult.calladapter
implementation libs.bundles.okhttp
implementation libs.okio
implementation libs.conscrypt.android

View File

@ -19,11 +19,14 @@ import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.annotation.RawRes
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
import com.keylesspalace.tusky.util.closeQuietly
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.buffer
import okio.source
class LicenseActivity : BaseActivity() {
@ -44,23 +47,15 @@ class LicenseActivity : BaseActivity() {
}
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
val sb = StringBuilder()
val br = BufferedReader(InputStreamReader(resources.openRawResource(fileId)))
try {
var line: String? = br.readLine()
while (line != null) {
sb.append(line)
sb.append('\n')
line = br.readLine()
lifecycleScope.launch {
textView.text = withContext(Dispatchers.IO) {
try {
resources.openRawResource(fileId).source().buffer().use { it.readUtf8() }
} catch (e: IOException) {
Log.w("LicenseActivity", e)
""
}
}
} catch (e: IOException) {
Log.w("LicenseActivity", e)
}
br.closeQuietly()
textView.text = sb.toString()
}
}

View File

@ -37,8 +37,6 @@ import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.getServerErrorMessage
import com.keylesspalace.tusky.util.randomAlphanumericString
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@ -58,6 +56,9 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.shareIn
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okio.buffer
import okio.sink
import okio.source
import retrofit2.HttpException
sealed interface FinalUploadEvent
@ -161,22 +162,22 @@ class MediaUploader @Inject constructor(
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
contentResolver.openInputStream(inUri).use { input ->
contentResolver.openInputStream(inUri)?.source().use { input ->
if (input == null) {
Log.w(TAG, "Media input is null")
uri = inUri
return@use
}
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out)
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
file.absoluteFile.sink().buffer().use { out ->
out.writeAll(input)
}
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
}
}
ContentResolver.SCHEME_FILE -> {
@ -189,17 +190,18 @@ class MediaUploader @Inject constructor(
val suffix = inputFile.name.substringAfterLast('.', "tmp")
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
val input = FileInputStream(inputFile)
FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out)
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
inputFile.source().use { input ->
file.absoluteFile.sink().buffer().use { out ->
out.writeAll(input)
}
}
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
}
else -> {
Log.w(TAG, "Unknown uri scheme $uri")

View File

@ -187,10 +187,8 @@ class DraftHelper @Inject constructor(
val response = okHttpClient.newCall(request).execute()
val sink = file.sink().buffer()
response.body?.source()?.use { input ->
sink.use { output ->
file.sink().buffer().use { output ->
response.body?.source()?.use { input ->
output.writeAll(input)
}
}

View File

@ -1,4 +1,5 @@
/* Copyright 2024 Tusky Contributors
/*
* Copyright 2024 Tusky Contributors
*
* This file is a part of Tusky.
*
@ -11,7 +12,8 @@
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.network
import android.content.ContentResolver
@ -23,13 +25,19 @@ import okio.Buffer
import okio.BufferedSink
import okio.source
// Align with Okio Segment size for better performance
private const val DEFAULT_CHUNK_SIZE = 8192L
fun interface UploadCallback {
fun onProgressUpdate(percentage: Int)
}
fun Uri.asRequestBody(contentResolver: ContentResolver, contentType: MediaType? = null, contentLength: Long = -1L, uploadListener: UploadCallback? = null): RequestBody {
fun Uri.asRequestBody(
contentResolver: ContentResolver,
contentType: MediaType? = null,
contentLength: Long = -1L,
uploadListener: UploadCallback? = null
): RequestBody {
return object : RequestBody() {
override fun contentType(): MediaType? = contentType
@ -38,7 +46,8 @@ fun Uri.asRequestBody(contentResolver: ContentResolver, contentType: MediaType?
override fun writeTo(sink: BufferedSink) {
val buffer = Buffer()
var uploaded: Long = 0
val inputStream = contentResolver.openInputStream(this@asRequestBody) ?: throw FileNotFoundException("Unavailable ContentProvider")
val inputStream = contentResolver.openInputStream(this@asRequestBody)
?: throw FileNotFoundException("Unavailable ContentProvider")
inputStream.source().use { source ->
while (true) {

View File

@ -24,11 +24,11 @@ import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.graphics.Shader
import com.bumptech.glide.load.Key
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.bumptech.glide.util.Util
import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.security.MessageDigest
/**
@ -57,11 +57,11 @@ class CompositeWithOpaqueBackground(val backgroundColor: Int) : BitmapTransforma
return false
}
override fun hashCode() = Util.hashCode(ID.hashCode(), backgroundColor.hashCode())
override fun hashCode() = Util.hashCode(ID.hashCode(), Util.hashCode(backgroundColor))
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(ID_BYTES)
messageDigest.update(ByteBuffer.allocate(4).putInt(backgroundColor.hashCode()).array())
messageDigest.update(ByteBuffer.allocate(Int.SIZE_BYTES).putInt(backgroundColor).array())
}
override fun transform(
@ -111,7 +111,7 @@ class CompositeWithOpaqueBackground(val backgroundColor: Int) : BitmapTransforma
@Suppress("unused")
private const val TAG = "CompositeWithOpaqueBackground"
private val ID = CompositeWithOpaqueBackground::class.qualifiedName!!
private val ID_BYTES = ID.toByteArray(Charset.forName("UTF-8"))
private val ID_BYTES = ID.toByteArray(Key.CHARSET)
/** Paint with a color filter that converts 8bpp alpha images to a 1bpp mask */
private val EXTRACT_MASK_PAINT = Paint().apply {

View File

@ -27,7 +27,6 @@ import androidx.exifinterface.media.ExifInterface
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -68,16 +67,18 @@ fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long {
}
@Throws(FileNotFoundException::class)
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long {
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Int {
val input = contentResolver.openInputStream(uri) ?: throw FileNotFoundException("Unavailable ContentProvider")
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(input, null, options)
try {
BitmapFactory.decodeStream(input, null, options)
} finally {
input.closeQuietly()
}
input.closeQuietly()
return (options.outWidth * options.outHeight).toLong()
return options.outWidth * options.outHeight
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
@ -147,30 +148,23 @@ fun reorientBitmap(bitmap: Bitmap?, orientation: Int): Bitmap? {
}
fun getImageOrientation(uri: Uri, contentResolver: ContentResolver): Int {
val inputStream: InputStream?
try {
inputStream = contentResolver.openInputStream(uri)
} catch (e: FileNotFoundException) {
Log.w(TAG, e)
return ExifInterface.ORIENTATION_UNDEFINED
}
if (inputStream == null) {
return ExifInterface.ORIENTATION_UNDEFINED
}
val exifInterface: ExifInterface
try {
exifInterface = ExifInterface(inputStream)
val inputStream = contentResolver.openInputStream(uri)
?: return ExifInterface.ORIENTATION_UNDEFINED
try {
val exifInterface = ExifInterface(inputStream)
return exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
} finally {
inputStream.closeQuietly()
}
} catch (e: IOException) {
Log.w(TAG, e)
inputStream.closeQuietly()
return ExifInterface.ORIENTATION_UNDEFINED
}
val orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
inputStream.closeQuietly()
return orientation
}
fun deleteStaleCachedMedia(mediaDirectory: File?) {

View File

@ -42,6 +42,7 @@ mockito-kotlin = "5.3.1"
moshi = "1.15.1"
networkresult-calladapter = "1.1.0"
okhttp = "4.12.0"
okio = "3.9.0"
retrofit = "2.11.0"
robolectric = "4.12.1"
sparkbutton = "4.2.0"
@ -122,6 +123,7 @@ moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", ver
networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" }
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }