改包名com.aiosman.ravenow
This commit is contained in:
218
app/src/main/java/com/aiosman/ravenow/utils/BlurHashDecoder.kt
Normal file
218
app/src/main/java/com/aiosman/ravenow/utils/BlurHashDecoder.kt
Normal file
@@ -0,0 +1,218 @@
|
||||
package com.aiosman.ravenow.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.withSign
|
||||
|
||||
internal object BlurHashDecoder {
|
||||
|
||||
// cache Math.cos() calculations to improve performance.
|
||||
// The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps
|
||||
// the cache is enabled by default, it is recommended to disable it only when just a few images are displayed
|
||||
private val cacheCosinesX = SparseArrayCompat<DoubleArray>()
|
||||
private val cacheCosinesY = SparseArrayCompat<DoubleArray>()
|
||||
|
||||
/**
|
||||
* Clear calculations stored in memory cache.
|
||||
* The cache is not big, but will increase when many image sizes are used,
|
||||
* if the app needs memory it is recommended to clear it.
|
||||
*/
|
||||
private fun clearCache() {
|
||||
cacheCosinesX.clear()
|
||||
cacheCosinesY.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a blur hash into a new bitmap.
|
||||
*
|
||||
* @param useCache use in memory cache for the calculated math, reused by images with same size.
|
||||
* if the cache does not exist yet it will be created and populated with new calculations.
|
||||
* By default it is true.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
internal fun decode(
|
||||
blurHash: String?,
|
||||
width: Int,
|
||||
height: Int,
|
||||
punch: Float = 1f,
|
||||
useCache: Boolean = true
|
||||
): Bitmap? {
|
||||
if (blurHash == null || blurHash.length < 6) {
|
||||
return null
|
||||
}
|
||||
val numCompEnc = decode83(blurHash, 0, 1)
|
||||
val numCompX = (numCompEnc % 9) + 1
|
||||
val numCompY = (numCompEnc / 9) + 1
|
||||
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
|
||||
return null
|
||||
}
|
||||
val maxAcEnc = decode83(blurHash, 1, 2)
|
||||
val maxAc = (maxAcEnc + 1) / 166f
|
||||
val colors = Array(numCompX * numCompY) { i ->
|
||||
if (i == 0) {
|
||||
val colorEnc = decode83(blurHash, 2, 6)
|
||||
decodeDc(colorEnc)
|
||||
} else {
|
||||
val from = 4 + i * 2
|
||||
val colorEnc = decode83(blurHash, from, from + 2)
|
||||
decodeAc(colorEnc, maxAc * punch)
|
||||
}
|
||||
}
|
||||
return composeBitmap(width, height, numCompX, numCompY, colors, useCache)
|
||||
}
|
||||
|
||||
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
|
||||
var result = 0
|
||||
for (i in from until to) {
|
||||
val index = charMap[str[i]] ?: -1
|
||||
if (index != -1) {
|
||||
result = result * 83 + index
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun decodeDc(colorEnc: Int): FloatArray {
|
||||
val r = colorEnc shr 16
|
||||
val g = (colorEnc shr 8) and 255
|
||||
val b = colorEnc and 255
|
||||
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
|
||||
}
|
||||
|
||||
private fun srgbToLinear(colorEnc: Int): Float {
|
||||
val v = colorEnc / 255f
|
||||
return if (v <= 0.04045f) {
|
||||
(v / 12.92f)
|
||||
} else {
|
||||
((v + 0.055f) / 1.055f).pow(2.4f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
|
||||
val r = value / (19 * 19)
|
||||
val g = (value / 19) % 19
|
||||
val b = value % 19
|
||||
return floatArrayOf(
|
||||
signedPow2((r - 9) / 9.0f) * maxAc,
|
||||
signedPow2((g - 9) / 9.0f) * maxAc,
|
||||
signedPow2((b - 9) / 9.0f) * maxAc
|
||||
)
|
||||
}
|
||||
|
||||
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
|
||||
|
||||
private fun composeBitmap(
|
||||
width: Int,
|
||||
height: Int,
|
||||
numCompX: Int,
|
||||
numCompY: Int,
|
||||
colors: Array<FloatArray>,
|
||||
useCache: Boolean
|
||||
): Bitmap {
|
||||
// use an array for better performance when writing pixel colors
|
||||
val imageArray = IntArray(width * height)
|
||||
val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX)
|
||||
val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX)
|
||||
val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY)
|
||||
val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY)
|
||||
runBlocking {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
||||
val tasks = ArrayList<Deferred<Unit>>()
|
||||
tasks.add(
|
||||
async {
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
var r = 0f
|
||||
var g = 0f
|
||||
var b = 0f
|
||||
for (j in 0 until numCompY) {
|
||||
for (i in 0 until numCompX) {
|
||||
val cosX =
|
||||
cosinesX.getCos(calculateCosX, i, numCompX, x, width)
|
||||
val cosY =
|
||||
cosinesY.getCos(calculateCosY, j, numCompY, y, height)
|
||||
val basis = (cosX * cosY).toFloat()
|
||||
val color = colors[j * numCompX + i]
|
||||
r += color[0] * basis
|
||||
g += color[1] * basis
|
||||
b += color[2] * basis
|
||||
}
|
||||
}
|
||||
imageArray[x + width * y] =
|
||||
Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
|
||||
}
|
||||
}
|
||||
return@async
|
||||
}
|
||||
)
|
||||
tasks.forEach { it.await() }
|
||||
}.join()
|
||||
}
|
||||
|
||||
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
|
||||
private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when {
|
||||
calculate -> {
|
||||
DoubleArray(height * numCompY).also {
|
||||
cacheCosinesY.put(height * numCompY, it)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
cacheCosinesY.get(height * numCompY)!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when {
|
||||
calculate -> {
|
||||
DoubleArray(width * numCompX).also {
|
||||
cacheCosinesX.put(width * numCompX, it)
|
||||
}
|
||||
}
|
||||
|
||||
else -> cacheCosinesX.get(width * numCompX)!!
|
||||
}
|
||||
|
||||
private fun DoubleArray.getCos(
|
||||
calculate: Boolean,
|
||||
x: Int,
|
||||
numComp: Int,
|
||||
y: Int,
|
||||
size: Int
|
||||
): Double {
|
||||
if (calculate) {
|
||||
this[x + numComp * y] = cos(Math.PI * y * x / size)
|
||||
}
|
||||
return this[x + numComp * y]
|
||||
}
|
||||
|
||||
private fun linearToSrgb(value: Float): Int {
|
||||
val v = value.coerceIn(0f, 1f)
|
||||
return if (v <= 0.0031308f) {
|
||||
(v * 12.92f * 255f + 0.5f).toInt()
|
||||
} else {
|
||||
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private val charMap = listOf(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
||||
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
|
||||
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
|
||||
)
|
||||
.mapIndexed { i, c -> c to i }
|
||||
.toMap()
|
||||
}
|
||||
121
app/src/main/java/com/aiosman/ravenow/utils/FileUtil.kt
Normal file
121
app/src/main/java/com/aiosman/ravenow/utils/FileUtil.kt
Normal file
@@ -0,0 +1,121 @@
|
||||
package com.aiosman.ravenow.utils
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import com.aiosman.ravenow.utils.Utils.getImageLoader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
object FileUtil {
|
||||
suspend fun saveImageToGallery(context: Context, url: String) {
|
||||
val loader = getImageLoader(context)
|
||||
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(url)
|
||||
.allowHardware(false) // Disable hardware bitmaps.
|
||||
.build()
|
||||
|
||||
val result = (loader.execute(request) as SuccessResult).drawable
|
||||
val bitmap = (result as BitmapDrawable).bitmap
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, "image_${System.currentTimeMillis()}.jpg")
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
|
||||
}
|
||||
|
||||
val uri = context.contentResolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
contentValues
|
||||
)
|
||||
uri?.let {
|
||||
val outputStream: OutputStream? = context.contentResolver.openOutputStream(it)
|
||||
outputStream.use { stream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream!!)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Image saved to gallery", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImageToMediaStore(context: Context, displayName: String, bitmap: Bitmap): Uri? {
|
||||
val imageCollections = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
} else {
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
|
||||
val imageDetails = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||
}
|
||||
}
|
||||
|
||||
val resolver = context.applicationContext.contentResolver
|
||||
val imageContentUri = resolver.insert(imageCollections, imageDetails) ?: return null
|
||||
|
||||
return try {
|
||||
resolver.openOutputStream(imageContentUri, "w").use { os ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os!!)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
imageDetails.clear()
|
||||
imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||
resolver.update(imageContentUri, imageDetails, null, null)
|
||||
}
|
||||
|
||||
imageContentUri
|
||||
} catch (e: FileNotFoundException) {
|
||||
// Some legacy devices won't create directory for the Uri if dir not exist, resulting in
|
||||
// a FileNotFoundException. To resolve this issue, we should use the File API to save the
|
||||
// image, which allows us to create the directory ourselves.
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getRealPathFromUri(context: Context, uri: Uri): String? {
|
||||
var realPath: String? = null
|
||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||
val cursor: Cursor? = context.contentResolver.query(uri, projection, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||
realPath = it.getString(columnIndex)
|
||||
}
|
||||
}
|
||||
return realPath
|
||||
}
|
||||
|
||||
suspend fun bitmapToJPG(context: Context, bitmap: Bitmap, displayName: String): Uri? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val tempFile = File.createTempFile(displayName, ".jpg", context.cacheDir)
|
||||
FileOutputStream(tempFile).use { os ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os)
|
||||
}
|
||||
Uri.fromFile(tempFile)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/src/main/java/com/aiosman/ravenow/utils/GoogleLogin.kt
Normal file
37
app/src/main/java/com/aiosman/ravenow/utils/GoogleLogin.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.aiosman.ravenow.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.credentials.Credential
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.CustomCredential
|
||||
import androidx.credentials.GetCredentialRequest
|
||||
import androidx.credentials.GetCredentialResponse
|
||||
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
||||
|
||||
|
||||
fun handleGoogleSignIn(result: GetCredentialResponse, onLoginWithGoogle: (String) -> Unit) {
|
||||
val credential: Credential = result.credential
|
||||
|
||||
if (credential is CustomCredential) {
|
||||
if (GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL.equals(credential.type)) {
|
||||
val googleIdTokenCredential: GoogleIdTokenCredential =
|
||||
GoogleIdTokenCredential.createFrom(credential.data)
|
||||
onLoginWithGoogle(googleIdTokenCredential.idToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun GoogleLogin(context: Context, onLoginWithGoogle: (String) -> Unit) {
|
||||
val credentialManager = CredentialManager.create(context)
|
||||
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
|
||||
.setServerClientId("987156664714-3bpf58ldhhr0m474ep48l668ngdn7860.apps.googleusercontent.com")
|
||||
.setFilterByAuthorizedAccounts(false)
|
||||
.build()
|
||||
val request = GetCredentialRequest.Builder().addCredentialOption(googleIdOption)
|
||||
.build()
|
||||
|
||||
credentialManager.getCredential(context, request).let {
|
||||
handleGoogleSignIn(it, onLoginWithGoogle)
|
||||
}
|
||||
}
|
||||
22
app/src/main/java/com/aiosman/ravenow/utils/TrtcHelper.kt
Normal file
22
app/src/main/java/com/aiosman/ravenow/utils/TrtcHelper.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.aiosman.ravenow.utils
|
||||
|
||||
import com.tencent.imsdk.v2.V2TIMManager
|
||||
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
object TrtcHelper {
|
||||
suspend fun loadUnreadCount(): Long {
|
||||
return suspendCoroutine { continuation ->
|
||||
V2TIMManager.getConversationManager()
|
||||
.getTotalUnreadMessageCount(object : V2TIMValueCallback<Long> {
|
||||
override fun onSuccess(t: Long?) {
|
||||
continuation.resumeWith(Result.success(t ?: 0))
|
||||
}
|
||||
|
||||
override fun onError(code: Int, desc: String?) {
|
||||
continuation.resumeWith(Result.failure(Exception("Error $code: $desc")))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
96
app/src/main/java/com/aiosman/ravenow/utils/Utils.kt
Normal file
96
app/src/main/java/com/aiosman/ravenow/utils/Utils.kt
Normal file
@@ -0,0 +1,96 @@
|
||||
package com.aiosman.ravenow.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import coil.ImageLoader
|
||||
import coil.request.CachePolicy
|
||||
import com.aiosman.ravenow.data.api.AuthInterceptor
|
||||
import com.aiosman.ravenow.data.api.getUnsafeOkHttpClient
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object Utils {
|
||||
fun generateRandomString(length: Int): String {
|
||||
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||
return (1..length)
|
||||
.map { allowedChars.random() }
|
||||
.joinToString("")
|
||||
}
|
||||
|
||||
fun getImageLoader(context: Context): ImageLoader {
|
||||
val okHttpClient = getUnsafeOkHttpClient(authInterceptor = AuthInterceptor())
|
||||
return ImageLoader.Builder(context)
|
||||
.okHttpClient(okHttpClient)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
// .memoryCache {
|
||||
// MemoryCache.Builder(context)
|
||||
// .maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25%
|
||||
// .build()
|
||||
// }
|
||||
// .diskCache {
|
||||
// DiskCache.Builder()
|
||||
// .directory(context.cacheDir.resolve("image_cache"))
|
||||
// .maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2%
|
||||
// .build()
|
||||
// }
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getTimeAgo(date: Date): String {
|
||||
val now = Date()
|
||||
val diffInMillis = now.time - date.time
|
||||
|
||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(diffInMillis)
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(diffInMillis)
|
||||
val hours = TimeUnit.MILLISECONDS.toHours(diffInMillis)
|
||||
val days = TimeUnit.MILLISECONDS.toDays(diffInMillis)
|
||||
val years = days / 365
|
||||
|
||||
return when {
|
||||
seconds < 60 -> "$seconds seconds ago"
|
||||
minutes < 60 -> "$minutes minutes ago"
|
||||
hours < 24 -> "$hours hours ago"
|
||||
days < 365 -> "$days days ago"
|
||||
else -> "$years years ago"
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentLanguage(): String {
|
||||
return Locale.getDefault().language
|
||||
}
|
||||
|
||||
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
||||
val (width, height) = originalBitmap.width to originalBitmap.height
|
||||
|
||||
val (newWidth, newHeight) = if (width > height) {
|
||||
maxSize to (height * maxSize / width)
|
||||
} else {
|
||||
(width * maxSize / height) to maxSize
|
||||
}
|
||||
|
||||
val scaledBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true)
|
||||
val uuidImageName = UUID.randomUUID().toString().let { "$it.jpg" }
|
||||
val compressedFile = File(context.cacheDir, uuidImageName)
|
||||
val outputStream = FileOutputStream(compressedFile)
|
||||
if (quality > 100) {
|
||||
throw IllegalArgumentException("Quality must be less than 100")
|
||||
}
|
||||
if (quality < 0) {
|
||||
throw IllegalArgumentException("Quality must be greater than 0")
|
||||
}
|
||||
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
return compressedFile
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user