diff --git a/app/build.gradle b/app/build.gradle index 0936a6ebc..b07623847 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,6 +149,7 @@ dependencies { implementation libs.networkresult.calladapter implementation libs.bundles.okhttp + implementation libs.okio implementation libs.conscrypt.android diff --git a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt index ca81d244a..02e9b1a51 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -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() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index b5ad6c622..4f32d8dc0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -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") diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index b0e856fd1..7cfed4ea8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/UriRequestBody.kt b/app/src/main/java/com/keylesspalace/tusky/network/UriRequestBody.kt index c751f2fc3..bdd5a6e61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/UriRequestBody.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/UriRequestBody.kt @@ -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 . */ + * see . + */ 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) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CompositeWithOpaqueBackground.kt b/app/src/main/java/com/keylesspalace/tusky/util/CompositeWithOpaqueBackground.kt index c08c61178..ded746e8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CompositeWithOpaqueBackground.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CompositeWithOpaqueBackground.kt @@ -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 { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt index ed4e71811..27c4f549b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt @@ -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?) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a2b34c80..9b0da4bc8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }