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:
parent
2504f42f7b
commit
f69cae2315
8 changed files with 77 additions and 76 deletions
|
@ -149,6 +149,7 @@ dependencies {
|
|||
implementation libs.networkresult.calladapter
|
||||
|
||||
implementation libs.bundles.okhttp
|
||||
implementation libs.okio
|
||||
|
||||
implementation libs.conscrypt.android
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in a new issue