kopia lustrzana https://github.com/vitorpamplona/amethyst
214 wiersze
7.5 KiB
Kotlin
214 wiersze
7.5 KiB
Kotlin
/**
|
|
* Copyright (c) 2024 Vitor Pamplona
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
package com.vitorpamplona.amethyst.ui.actions
|
|
|
|
import android.content.ContentResolver
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.media.MediaScannerConnection
|
|
import android.os.Build
|
|
import android.os.Environment
|
|
import android.provider.MediaStore
|
|
import android.webkit.MimeTypeMap
|
|
import androidx.annotation.RequiresApi
|
|
import com.vitorpamplona.amethyst.BuildConfig
|
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
|
import kotlinx.coroutines.CancellationException
|
|
import okhttp3.Call
|
|
import okhttp3.Callback
|
|
import okhttp3.Request
|
|
import okhttp3.Response
|
|
import okio.BufferedSource
|
|
import okio.IOException
|
|
import okio.buffer
|
|
import okio.sink
|
|
import okio.source
|
|
import java.io.File
|
|
import java.util.UUID
|
|
|
|
object ImageSaver {
|
|
/**
|
|
* Saves the image to the gallery. May require a storage permission.
|
|
*
|
|
* @see PICTURES_SUBDIRECTORY
|
|
*/
|
|
fun saveImage(
|
|
url: String,
|
|
context: Context,
|
|
onSuccess: () -> Any?,
|
|
onError: (Throwable) -> Any?,
|
|
) {
|
|
val client = HttpClientManager.getHttpClient()
|
|
|
|
val request =
|
|
Request.Builder()
|
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
|
.get()
|
|
.url(url)
|
|
.build()
|
|
|
|
client
|
|
.newCall(request)
|
|
.enqueue(
|
|
object : Callback {
|
|
override fun onFailure(
|
|
call: Call,
|
|
e: IOException,
|
|
) {
|
|
e.printStackTrace()
|
|
onError(e)
|
|
}
|
|
|
|
override fun onResponse(
|
|
call: Call,
|
|
response: Response,
|
|
) {
|
|
try {
|
|
check(response.isSuccessful)
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
val contentType = response.header("Content-Type")
|
|
checkNotNull(contentType) { "Can't find out the content type" }
|
|
|
|
saveContentQ(
|
|
displayName = File(url).nameWithoutExtension,
|
|
contentType = contentType,
|
|
contentSource = response.body.source(),
|
|
contentResolver = context.contentResolver,
|
|
)
|
|
} else {
|
|
saveContentDefault(
|
|
fileName = File(url).name,
|
|
contentSource = response.body.source(),
|
|
context = context,
|
|
)
|
|
}
|
|
onSuccess()
|
|
} catch (e: Exception) {
|
|
if (e is CancellationException) throw e
|
|
e.printStackTrace()
|
|
onError(e)
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
fun saveImage(
|
|
localFile: File,
|
|
mimeType: String?,
|
|
context: Context,
|
|
onSuccess: () -> Any?,
|
|
onError: (Throwable) -> Any?,
|
|
) {
|
|
try {
|
|
val extension =
|
|
mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
|
val buffer = localFile.inputStream().source().buffer()
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
saveContentQ(
|
|
displayName = UUID.randomUUID().toString(),
|
|
contentType = mimeType ?: "",
|
|
contentSource = buffer,
|
|
contentResolver = context.contentResolver,
|
|
)
|
|
} else {
|
|
saveContentDefault(
|
|
fileName = UUID.randomUUID().toString() + ".$extension",
|
|
contentSource = buffer,
|
|
context = context,
|
|
)
|
|
}
|
|
onSuccess()
|
|
} catch (e: Exception) {
|
|
if (e is CancellationException) throw e
|
|
e.printStackTrace()
|
|
onError(e)
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.Q)
|
|
private fun saveContentQ(
|
|
displayName: String,
|
|
contentType: String,
|
|
contentSource: BufferedSource,
|
|
contentResolver: ContentResolver,
|
|
) {
|
|
val contentValues =
|
|
ContentValues().apply {
|
|
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
|
put(MediaStore.MediaColumns.MIME_TYPE, contentType)
|
|
put(
|
|
MediaStore.MediaColumns.RELATIVE_PATH,
|
|
Environment.DIRECTORY_PICTURES + File.separatorChar + PICTURES_SUBDIRECTORY,
|
|
)
|
|
}
|
|
|
|
val masterUri =
|
|
if (contentType.startsWith("image")) {
|
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
} else {
|
|
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
}
|
|
|
|
val uri = contentResolver.insert(masterUri, contentValues)
|
|
checkNotNull(uri) { "Can't insert the new content" }
|
|
|
|
try {
|
|
val outputStream = contentResolver.openOutputStream(uri)
|
|
checkNotNull(outputStream) { "Can't open the content output stream" }
|
|
|
|
outputStream.use { contentSource.readAll(it.sink()) }
|
|
} catch (e: Exception) {
|
|
if (e is CancellationException) throw e
|
|
contentResolver.delete(uri, null, null)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
private fun saveContentDefault(
|
|
fileName: String,
|
|
contentSource: BufferedSource,
|
|
context: Context,
|
|
) {
|
|
val subdirectory =
|
|
File(
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
|
PICTURES_SUBDIRECTORY,
|
|
)
|
|
|
|
if (!subdirectory.exists()) {
|
|
subdirectory.mkdirs()
|
|
}
|
|
|
|
val outputFile = File(subdirectory, fileName)
|
|
|
|
outputFile.outputStream().use { contentSource.readAll(it.sink()) }
|
|
|
|
// Call the media scanner manually, so the image
|
|
// appears in the gallery faster.
|
|
MediaScannerConnection.scanFile(context, arrayOf(outputFile.toString()), null, null)
|
|
}
|
|
|
|
private const val PICTURES_SUBDIRECTORY = "Amethyst"
|
|
}
|