改包名com.aiosman.ravenow

This commit is contained in:
2024-11-17 20:07:42 +08:00
parent 914cfca6be
commit 074244c0f8
168 changed files with 897 additions and 970 deletions

View 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()
}

View 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
}
}
}
}

View 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)
}
}

View 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")))
}
});
}
}
}

View 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
}
}