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
|
@ -149,6 +149,7 @@ dependencies {
|
||||||
implementation libs.networkresult.calladapter
|
implementation libs.networkresult.calladapter
|
||||||
|
|
||||||
implementation libs.bundles.okhttp
|
implementation libs.bundles.okhttp
|
||||||
|
implementation libs.okio
|
||||||
|
|
||||||
implementation libs.conscrypt.android
|
implementation libs.conscrypt.android
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,14 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RawRes
|
import androidx.annotation.RawRes
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
||||||
import com.keylesspalace.tusky.util.closeQuietly
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.IOException
|
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() {
|
class LicenseActivity : BaseActivity() {
|
||||||
|
|
||||||
|
@ -44,23 +47,15 @@ class LicenseActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
||||||
val sb = StringBuilder()
|
lifecycleScope.launch {
|
||||||
|
textView.text = withContext(Dispatchers.IO) {
|
||||||
val br = BufferedReader(InputStreamReader(resources.openRawResource(fileId)))
|
try {
|
||||||
|
resources.openRawResource(fileId).source().buffer().use { it.readUtf8() }
|
||||||
try {
|
} catch (e: IOException) {
|
||||||
var line: String? = br.readLine()
|
Log.w("LicenseActivity", e)
|
||||||
while (line != null) {
|
""
|
||||||
sb.append(line)
|
}
|
||||||
sb.append('\n')
|
|
||||||
line = br.readLine()
|
|
||||||
}
|
}
|
||||||
} 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.getServerErrorMessage
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -58,6 +56,9 @@ import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
sealed interface FinalUploadEvent
|
sealed interface FinalUploadEvent
|
||||||
|
@ -161,22 +162,22 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||||
|
|
||||||
contentResolver.openInputStream(inUri).use { input ->
|
contentResolver.openInputStream(inUri)?.source().use { input ->
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
Log.w(TAG, "Media input is null")
|
Log.w(TAG, "Media input is null")
|
||||||
uri = inUri
|
uri = inUri
|
||||||
return@use
|
return@use
|
||||||
}
|
}
|
||||||
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
file.absoluteFile.sink().buffer().use { out ->
|
||||||
input.copyTo(out)
|
out.writeAll(input)
|
||||||
uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
|
||||||
}
|
}
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentResolver.SCHEME_FILE -> {
|
ContentResolver.SCHEME_FILE -> {
|
||||||
|
@ -189,17 +190,18 @@ class MediaUploader @Inject constructor(
|
||||||
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
||||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
||||||
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
||||||
val input = FileInputStream(inputFile)
|
|
||||||
|
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
inputFile.source().use { input ->
|
||||||
input.copyTo(out)
|
file.absoluteFile.sink().buffer().use { out ->
|
||||||
uri = FileProvider.getUriForFile(
|
out.writeAll(input)
|
||||||
context,
|
}
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
|
||||||
}
|
}
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unknown uri scheme $uri")
|
Log.w(TAG, "Unknown uri scheme $uri")
|
||||||
|
|
|
@ -187,10 +187,8 @@ class DraftHelper @Inject constructor(
|
||||||
|
|
||||||
val response = okHttpClient.newCall(request).execute()
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
val sink = file.sink().buffer()
|
file.sink().buffer().use { output ->
|
||||||
|
response.body?.source()?.use { input ->
|
||||||
response.body?.source()?.use { input ->
|
|
||||||
sink.use { output ->
|
|
||||||
output.writeAll(input)
|
output.writeAll(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* Copyright 2024 Tusky Contributors
|
/*
|
||||||
|
* Copyright 2024 Tusky Contributors
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -11,7 +12,8 @@
|
||||||
* Public License for more details.
|
* Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* 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
|
package com.keylesspalace.tusky.network
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
@ -23,13 +25,19 @@ import okio.Buffer
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.source
|
import okio.source
|
||||||
|
|
||||||
|
// Align with Okio Segment size for better performance
|
||||||
private const val DEFAULT_CHUNK_SIZE = 8192L
|
private const val DEFAULT_CHUNK_SIZE = 8192L
|
||||||
|
|
||||||
fun interface UploadCallback {
|
fun interface UploadCallback {
|
||||||
fun onProgressUpdate(percentage: Int)
|
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() {
|
return object : RequestBody() {
|
||||||
override fun contentType(): MediaType? = contentType
|
override fun contentType(): MediaType? = contentType
|
||||||
|
|
||||||
|
@ -38,7 +46,8 @@ fun Uri.asRequestBody(contentResolver: ContentResolver, contentType: MediaType?
|
||||||
override fun writeTo(sink: BufferedSink) {
|
override fun writeTo(sink: BufferedSink) {
|
||||||
val buffer = Buffer()
|
val buffer = Buffer()
|
||||||
var uploaded: Long = 0
|
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 ->
|
inputStream.source().use { source ->
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
|
@ -24,11 +24,11 @@ import android.graphics.ColorMatrix
|
||||||
import android.graphics.ColorMatrixColorFilter
|
import android.graphics.ColorMatrixColorFilter
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.Shader
|
import android.graphics.Shader
|
||||||
|
import com.bumptech.glide.load.Key
|
||||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import com.bumptech.glide.util.Util
|
import com.bumptech.glide.util.Util
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,11 +57,11 @@ class CompositeWithOpaqueBackground(val backgroundColor: Int) : BitmapTransforma
|
||||||
return false
|
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) {
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
messageDigest.update(ID_BYTES)
|
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(
|
override fun transform(
|
||||||
|
@ -111,7 +111,7 @@ class CompositeWithOpaqueBackground(val backgroundColor: Int) : BitmapTransforma
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
private const val TAG = "CompositeWithOpaqueBackground"
|
private const val TAG = "CompositeWithOpaqueBackground"
|
||||||
private val ID = CompositeWithOpaqueBackground::class.qualifiedName!!
|
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 */
|
/** Paint with a color filter that converts 8bpp alpha images to a 1bpp mask */
|
||||||
private val EXTRACT_MASK_PAINT = Paint().apply {
|
private val EXTRACT_MASK_PAINT = Paint().apply {
|
||||||
|
|
|
@ -27,7 +27,6 @@ import androidx.exifinterface.media.ExifInterface
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -68,16 +67,18 @@ fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@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 input = contentResolver.openInputStream(uri) ?: throw FileNotFoundException("Unavailable ContentProvider")
|
||||||
|
|
||||||
val options = BitmapFactory.Options()
|
val options = BitmapFactory.Options()
|
||||||
options.inJustDecodeBounds = true
|
options.inJustDecodeBounds = true
|
||||||
BitmapFactory.decodeStream(input, null, options)
|
try {
|
||||||
|
BitmapFactory.decodeStream(input, null, options)
|
||||||
|
} finally {
|
||||||
|
input.closeQuietly()
|
||||||
|
}
|
||||||
|
|
||||||
input.closeQuietly()
|
return options.outWidth * options.outHeight
|
||||||
|
|
||||||
return (options.outWidth * options.outHeight).toLong()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
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 {
|
fun getImageOrientation(uri: Uri, contentResolver: ContentResolver): Int {
|
||||||
val inputStream: InputStream?
|
|
||||||
try {
|
try {
|
||||||
inputStream = contentResolver.openInputStream(uri)
|
val inputStream = contentResolver.openInputStream(uri)
|
||||||
} catch (e: FileNotFoundException) {
|
?: return ExifInterface.ORIENTATION_UNDEFINED
|
||||||
Log.w(TAG, e)
|
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED
|
try {
|
||||||
}
|
val exifInterface = ExifInterface(inputStream)
|
||||||
if (inputStream == null) {
|
return exifInterface.getAttributeInt(
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED
|
ExifInterface.TAG_ORIENTATION,
|
||||||
}
|
ExifInterface.ORIENTATION_NORMAL
|
||||||
val exifInterface: ExifInterface
|
)
|
||||||
try {
|
} finally {
|
||||||
exifInterface = ExifInterface(inputStream)
|
inputStream.closeQuietly()
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, e)
|
Log.w(TAG, e)
|
||||||
inputStream.closeQuietly()
|
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED
|
return ExifInterface.ORIENTATION_UNDEFINED
|
||||||
}
|
}
|
||||||
val orientation = exifInterface.getAttributeInt(
|
|
||||||
ExifInterface.TAG_ORIENTATION,
|
|
||||||
ExifInterface.ORIENTATION_NORMAL
|
|
||||||
)
|
|
||||||
inputStream.closeQuietly()
|
|
||||||
return orientation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteStaleCachedMedia(mediaDirectory: File?) {
|
fun deleteStaleCachedMedia(mediaDirectory: File?) {
|
||||||
|
|
|
@ -42,6 +42,7 @@ mockito-kotlin = "5.3.1"
|
||||||
moshi = "1.15.1"
|
moshi = "1.15.1"
|
||||||
networkresult-calladapter = "1.1.0"
|
networkresult-calladapter = "1.1.0"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
|
okio = "3.9.0"
|
||||||
retrofit = "2.11.0"
|
retrofit = "2.11.0"
|
||||||
robolectric = "4.12.1"
|
robolectric = "4.12.1"
|
||||||
sparkbutton = "4.2.0"
|
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" }
|
networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" }
|
||||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", 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-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
|
||||||
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||||
|
|
Loading…
Reference in New Issue