Tusky-App-Android/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt

205 lines
6.3 KiB
Kotlin
Raw Normal View History

/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* 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>. */
package com.keylesspalace.tusky.util
import android.content.ContentResolver
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
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
Replace RxJava3 code with coroutines (#4290) This pull request removes the remaining RxJava code and replaces it with coroutine-equivalent implementations. - Remove all duplicate methods in `MastodonApi`: - Methods returning a RxJava `Single` have been replaced by suspending methods returning a `NetworkResult` in order to be consistent with the new code. - _sync_/_async_ method variants are replaced with the _async_ version only (suspending method), and `runBlocking{}` is used to make the async variant synchronous. - Create a custom coroutine-based implementation of `Single` for usage in Java code where launching a coroutine is not possible. This class can be deleted after remaining Java code has been converted to Kotlin. - `NotificationsFragment.java` can subscribe to `EventHub` events by calling the new lifecycle-aware `EventHub.subscribe()` method. This allows using the `SharedFlow` as single source of truth for all events. - Rx Autodispose is replaced by `lifecycleScope.launch()` which will automatically cancel the coroutine when the Fragment view/Activity is destroyed. - Background work is launched in the existing injectable `externalScope`, since using `GlobalScope` is discouraged. `externalScope` has been changed to be a `@Singleton` and to use the main dispatcher by default. - Transform `ShareShortcutHelper` to an injectable utility class so it can use the application `Context` and `externalScope` as provided dependencies to launch a background coroutine. - Implement a custom Glide extension method `RequestBuilder.submitAsync()` to do the same thing as `RequestBuilder.submit().get()` in a non-blocking way. This way there is no need to switch to a background dispatcher and block a background thread, and cancellation is supported out-of-the-box. - An utility method `Fragment.updateRelativeTimePeriodically()` has been added to remove duplicate logic in `TimelineFragment` and `NotificationsFragment`, and the logic is now implemented using a simple coroutine instead of `Observable.interval()`. Note that the periodic update now happens between onStart and onStop instead of between onResume and onPause, since the Fragment is not interactive but is still visible in the started state. - Rewrite `BottomSheetActivityTest` using coroutines tests. - Remove all RxJava library dependencies.
2024-02-29 15:28:48 +01:00
import kotlin.time.Duration.Companion.hours
/**
* Helper methods for obtaining and resizing media files
*/
private const val TAG = "MediaUtils"
private const val MEDIA_TEMP_PREFIX = "Tusky_Share_Media"
const val MEDIA_SIZE_UNKNOWN = -1L
/**
* Fetches the size of the media represented by the given URI, assuming it is openable and
* the ContentResolver is able to resolve it.
*
* @return the size of the media in bytes or {@link MediaUtils#MEDIA_SIZE_UNKNOWN}
*/
fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long {
if (uri == null) {
return MEDIA_SIZE_UNKNOWN
}
var mediaSize = MEDIA_SIZE_UNKNOWN
val cursor: Cursor?
try {
cursor = contentResolver.query(uri, null, null, null, null)
} catch (e: SecurityException) {
return MEDIA_SIZE_UNKNOWN
}
if (cursor != null) {
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
mediaSize = cursor.getLong(sizeIndex)
cursor.close()
}
return mediaSize
}
@Throws(FileNotFoundException::class)
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long {
val input = contentResolver.openInputStream(uri) ?: throw FileNotFoundException("Unavailable ContentProvider")
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(input, null, options)
input.closeQuietly()
return (options.outWidth * options.outHeight).toLong()
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
fun reorientBitmap(bitmap: Bitmap?, orientation: Int): Bitmap? {
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_NORMAL -> return bitmap
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1.0f, 1.0f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180.0f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
matrix.setRotate(180.0f)
matrix.postScale(-1.0f, 1.0f)
}
ExifInterface.ORIENTATION_TRANSPOSE -> {
matrix.setRotate(90.0f)
matrix.postScale(-1.0f, 1.0f)
}
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90.0f)
ExifInterface.ORIENTATION_TRANSVERSE -> {
matrix.setRotate(-90.0f)
matrix.postScale(-1.0f, 1.0f)
}
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90.0f)
else -> return bitmap
}
if (bitmap == null) {
return null
}
return try {
val result = Bitmap.createBitmap(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
matrix,
true
)
if (!bitmap.sameAs(result)) {
bitmap.recycle()
}
result
} catch (e: OutOfMemoryError) {
null
}
}
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)
} 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?) {
if (mediaDirectory == null || !mediaDirectory.exists()) {
// Nothing to do
return
}
Replace RxJava3 code with coroutines (#4290) This pull request removes the remaining RxJava code and replaces it with coroutine-equivalent implementations. - Remove all duplicate methods in `MastodonApi`: - Methods returning a RxJava `Single` have been replaced by suspending methods returning a `NetworkResult` in order to be consistent with the new code. - _sync_/_async_ method variants are replaced with the _async_ version only (suspending method), and `runBlocking{}` is used to make the async variant synchronous. - Create a custom coroutine-based implementation of `Single` for usage in Java code where launching a coroutine is not possible. This class can be deleted after remaining Java code has been converted to Kotlin. - `NotificationsFragment.java` can subscribe to `EventHub` events by calling the new lifecycle-aware `EventHub.subscribe()` method. This allows using the `SharedFlow` as single source of truth for all events. - Rx Autodispose is replaced by `lifecycleScope.launch()` which will automatically cancel the coroutine when the Fragment view/Activity is destroyed. - Background work is launched in the existing injectable `externalScope`, since using `GlobalScope` is discouraged. `externalScope` has been changed to be a `@Singleton` and to use the main dispatcher by default. - Transform `ShareShortcutHelper` to an injectable utility class so it can use the application `Context` and `externalScope` as provided dependencies to launch a background coroutine. - Implement a custom Glide extension method `RequestBuilder.submitAsync()` to do the same thing as `RequestBuilder.submit().get()` in a non-blocking way. This way there is no need to switch to a background dispatcher and block a background thread, and cancellation is supported out-of-the-box. - An utility method `Fragment.updateRelativeTimePeriodically()` has been added to remove duplicate logic in `TimelineFragment` and `NotificationsFragment`, and the logic is now implemented using a simple coroutine instead of `Observable.interval()`. Note that the periodic update now happens between onStart and onStop instead of between onResume and onPause, since the Fragment is not interactive but is still visible in the started state. - Rewrite `BottomSheetActivityTest` using coroutines tests. - Remove all RxJava library dependencies.
2024-02-29 15:28:48 +01:00
val unixTime = System.currentTimeMillis() - 24.hours.inWholeMilliseconds
val files = mediaDirectory.listFiles { file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) }
Replace RxJava3 code with coroutines (#4290) This pull request removes the remaining RxJava code and replaces it with coroutine-equivalent implementations. - Remove all duplicate methods in `MastodonApi`: - Methods returning a RxJava `Single` have been replaced by suspending methods returning a `NetworkResult` in order to be consistent with the new code. - _sync_/_async_ method variants are replaced with the _async_ version only (suspending method), and `runBlocking{}` is used to make the async variant synchronous. - Create a custom coroutine-based implementation of `Single` for usage in Java code where launching a coroutine is not possible. This class can be deleted after remaining Java code has been converted to Kotlin. - `NotificationsFragment.java` can subscribe to `EventHub` events by calling the new lifecycle-aware `EventHub.subscribe()` method. This allows using the `SharedFlow` as single source of truth for all events. - Rx Autodispose is replaced by `lifecycleScope.launch()` which will automatically cancel the coroutine when the Fragment view/Activity is destroyed. - Background work is launched in the existing injectable `externalScope`, since using `GlobalScope` is discouraged. `externalScope` has been changed to be a `@Singleton` and to use the main dispatcher by default. - Transform `ShareShortcutHelper` to an injectable utility class so it can use the application `Context` and `externalScope` as provided dependencies to launch a background coroutine. - Implement a custom Glide extension method `RequestBuilder.submitAsync()` to do the same thing as `RequestBuilder.submit().get()` in a non-blocking way. This way there is no need to switch to a background dispatcher and block a background thread, and cancellation is supported out-of-the-box. - An utility method `Fragment.updateRelativeTimePeriodically()` has been added to remove duplicate logic in `TimelineFragment` and `NotificationsFragment`, and the logic is now implemented using a simple coroutine instead of `Observable.interval()`. Note that the periodic update now happens between onStart and onStop instead of between onResume and onPause, since the Fragment is not interactive but is still visible in the started state. - Rewrite `BottomSheetActivityTest` using coroutines tests. - Remove all RxJava library dependencies.
2024-02-29 15:28:48 +01:00
if (files.isNullOrEmpty()) {
// Nothing to do
return
}
for (file in files) {
try {
file.delete()
} catch (se: SecurityException) {
Log.e(TAG, "Error removing stale cached media")
}
}
}
fun getTemporaryMediaFilename(extension: String): String {
return "${MEDIA_TEMP_PREFIX}_${SimpleDateFormat(
"yyyyMMdd_HHmmss",
Locale.US
).format(Date())}.$extension"
}