mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
latest changes
This commit is contained in:
parent
a482a5a535
commit
d8f3627beb
@ -155,16 +155,16 @@
|
||||
android:resource="@xml/widget" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.MemoryReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget" />
|
||||
</receiver>
|
||||
<!-- <receiver-->
|
||||
<!-- android:name=".widget.MemoryReceiver"-->
|
||||
<!-- android:exported="true">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="android.appwidget.provider"-->
|
||||
<!-- android:resource="@xml/widget" />-->
|
||||
<!-- </receiver>-->
|
||||
</application>
|
||||
|
||||
|
||||
|
@ -0,0 +1,33 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import java.io.File
|
||||
|
||||
fun loadScaledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap? {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
||||
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
|
||||
options.inJustDecodeBounds = false
|
||||
|
||||
return BitmapFactory.decodeFile(file.absolutePath, options)
|
||||
}
|
||||
|
||||
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
||||
var inSampleSize = 1
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
val halfHeight: Int = height / 2
|
||||
val halfWidth: Int = width / 2
|
||||
|
||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize
|
||||
}
|
@ -16,23 +16,31 @@ package app.alextran.immich.widget
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import HomeWidgetGlanceState
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
import android.content.pm.PackageManager
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.FileProvider.getUriForFile
|
||||
import androidx.glance.*
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.glance.state.GlanceStateDefinition
|
||||
import androidx.work.*
|
||||
import com.google.gson.Gson
|
||||
import es.antonborri.home_widget.HomeWidgetPlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.random.Random
|
||||
|
||||
class ImageDownloadWorker(
|
||||
@ -44,53 +52,74 @@ class ImageDownloadWorker(
|
||||
|
||||
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
|
||||
|
||||
fun enqueue(context: Context, glanceId: GlanceId, config: WidgetConfig) {
|
||||
fun enqueue(context: Context, appWidgetId: Int, config: WidgetConfig) {
|
||||
val manager = WorkManager.getInstance(context)
|
||||
val requestBuilder = OneTimeWorkRequestBuilder<ImageDownloadWorker>().apply {
|
||||
addTag(glanceId.toString())
|
||||
setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
setInputData(
|
||||
Data.Builder()
|
||||
.putString("config", Gson().toJson(config))
|
||||
.putInt("glanceId", glanceId.hashCode())
|
||||
|
||||
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
|
||||
20, TimeUnit.MINUTES
|
||||
)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.setInputData(
|
||||
Data.Builder()
|
||||
.putString("config", Gson().toJson(config))
|
||||
.putInt("widgetId", appWidgetId)
|
||||
.build()
|
||||
)
|
||||
.addTag(appWidgetId.toString())
|
||||
.build()
|
||||
|
||||
manager.enqueueUniqueWork(
|
||||
uniqueWorkName + glanceId.hashCode(),
|
||||
ExistingWorkPolicy.KEEP,
|
||||
requestBuilder.build()
|
||||
manager.enqueueUniquePeriodicWork(
|
||||
"$uniqueWorkName-$appWidgetId",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any ongoing worker
|
||||
*/
|
||||
fun cancel(context: Context, glanceId: GlanceId) {
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(glanceId.toString())
|
||||
val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(glanceId)
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(appWidgetId.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getServerConfig(): ServerConfig? {
|
||||
val prefs = HomeWidgetPlugin.getData(context)
|
||||
|
||||
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
||||
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
||||
|
||||
if (serverURL == "" || sessionKey == "") {
|
||||
return null
|
||||
}
|
||||
|
||||
return ServerConfig(
|
||||
serverURL,
|
||||
sessionKey
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
val configString = inputData.getString("config")
|
||||
val config = Gson().fromJson(configString, WidgetConfig::class.java)
|
||||
val glanceId = inputData.getInt("glanceId", -1)
|
||||
val widgetId = inputData.getInt("widgetId", -1)
|
||||
|
||||
if (glanceId == -1) {
|
||||
Result.failure()
|
||||
val serverConfig = getServerConfig() ?: return Result.success()
|
||||
|
||||
val newBitmap = when (config.widgetType) {
|
||||
WidgetType.RANDOM -> fetchRandom(serverConfig)
|
||||
}
|
||||
|
||||
fetchImage(config, glanceId)
|
||||
updateWidget(config, glanceId)
|
||||
saveImage(newBitmap, widgetId)
|
||||
updateWidget(config, widgetId)
|
||||
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(uniqueWorkName, "Error while loading image", e)
|
||||
if (runAttemptCount < 10) {
|
||||
// Exponential backoff strategy will avoid the request to repeat
|
||||
// too fast in case of failures.
|
||||
Result.retry()
|
||||
} else {
|
||||
Result.failure()
|
||||
@ -98,29 +127,26 @@ class ImageDownloadWorker(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWidget(config: WidgetConfig, glanceId: Int) {
|
||||
private suspend fun updateWidget(config: WidgetConfig, widgetID: Int) {
|
||||
val manager = GlanceAppWidgetManager(context)
|
||||
val glanceIds = manager.getGlanceIds(config.widgetType.widgetClass)
|
||||
val glanceId = manager.getGlanceIdBy(widgetID)
|
||||
|
||||
for (id in glanceIds) {
|
||||
if (id.hashCode() == glanceId) {
|
||||
config.widgetType.widgetClass.getDeclaredConstructor().newInstance().updateAll(context)
|
||||
break
|
||||
}
|
||||
}
|
||||
RandomWidget().update(context, glanceId)
|
||||
|
||||
Log.w("WIDGET_BG", "SENT THE UPDATE COMMAND: $widgetID")
|
||||
}
|
||||
|
||||
private suspend fun fetchImage(config: WidgetConfig, glanceId: Int) {
|
||||
val api = ImmichAPI(config.credentials)
|
||||
private suspend fun fetchRandom(serverConfig: ServerConfig): Bitmap {
|
||||
val api = ImmichAPI(serverConfig)
|
||||
|
||||
val random = api.fetchSearchResults(SearchFilters(AssetType.IMAGE, size=1))
|
||||
val image = api.fetchImage(random[0])
|
||||
|
||||
saveImage(image, glanceId)
|
||||
return image
|
||||
}
|
||||
|
||||
private suspend fun saveImage(bitmap: Bitmap, glanceId: Int) = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, "widget_image_$glanceId.jpg")
|
||||
private suspend fun saveImage(bitmap: Bitmap, widgetId: Int) = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, "widget_image_$widgetId.jpg")
|
||||
FileOutputStream(file).use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ class ImmichAPI(cfg: ServerConfig) {
|
||||
private val serverConfig = cfg
|
||||
|
||||
private fun buildRequestURL(endpoint: String, params: List<Pair<String, String>> = emptyList()): URL {
|
||||
val baseUrl = URL(serverConfig.serverEndpoint)
|
||||
val urlString = StringBuilder("${baseUrl.protocol}://${baseUrl.host}:${baseUrl.port}$endpoint?sessionKey=${URLEncoder.encode(serverConfig.sessionKey, "UTF-8")}")
|
||||
val urlString = StringBuilder("${serverConfig.serverEndpoint}$endpoint?sessionKey=${serverConfig.sessionKey}")
|
||||
|
||||
for ((key, value) in params) {
|
||||
urlString.append("&${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}")
|
||||
|
@ -1,14 +1,8 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
|
||||
sealed class WidgetError : Exception() {
|
||||
data object NoLogin : WidgetError()
|
||||
data object FetchFailed : WidgetError()
|
||||
data object Unknown : WidgetError()
|
||||
data object AlbumNotFound : WidgetError()
|
||||
}
|
||||
|
||||
enum class AssetType {
|
||||
IMAGE, VIDEO, AUDIO, OTHER
|
||||
}
|
||||
@ -50,7 +44,12 @@ enum class WidgetType {
|
||||
data class WidgetConfig(
|
||||
val widgetType: WidgetType,
|
||||
val params: Map<String, String>,
|
||||
val credentials: ServerConfig
|
||||
)
|
||||
|
||||
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
||||
|
||||
// this value is in HomeWidgetPlugin.PREFERENCES but is marked internal
|
||||
const val WIDGET_PREFERENCES = "HomeWidgetPreferences"
|
||||
|
||||
val loggedInKey = stringPreferencesKey("isLoggedIn")
|
||||
val lastUpdatedKey = stringPreferencesKey("lastUpdated")
|
||||
|
@ -10,6 +10,7 @@ import androidx.glance.background
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.text.Text
|
||||
import app.alextran.immich.R
|
||||
|
||||
@Composable
|
||||
@ -23,15 +24,17 @@ fun PhotoWidget(image: Bitmap?, error: String?, subtitle: String?) {
|
||||
if (image != null) {
|
||||
Image(
|
||||
provider = ImageProvider(image),
|
||||
contentDescription = "Widget Image"
|
||||
contentDescription = "Widget Image",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = GlanceModifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.splash),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = GlanceModifier.fillMaxSize()
|
||||
)
|
||||
Text(subtitle ?: "NOPERS")
|
||||
Text(error ?: "NOPERS")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,36 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import HomeWidgetGlanceWidgetReceiver
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RandomReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
|
||||
override val glanceAppWidget = RandomWidget()
|
||||
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
|
||||
val cfg = WidgetConfig(WidgetType.RANDOM, HashMap())
|
||||
|
||||
appWidgetIds.forEach { widgetID ->
|
||||
ImageDownloadWorker.enqueue(context, widgetID, cfg)
|
||||
Log.w("WIDGET_UPDATE", "WORKER ENQUEUE CALLED: $widgetID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,53 +3,38 @@ package app.alextran.immich.widget
|
||||
import HomeWidgetGlanceState
|
||||
import HomeWidgetGlanceStateDefinition
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.glance.appwidget.*
|
||||
import androidx.glance.*
|
||||
import androidx.glance.action.ActionParameters
|
||||
import androidx.glance.appwidget.action.ActionCallback
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.state.GlanceStateDefinition
|
||||
|
||||
import java.io.File
|
||||
|
||||
class RandomWidget : GlanceAppWidget() {
|
||||
override val stateDefinition: GlanceStateDefinition<HomeWidgetGlanceState>
|
||||
get() = HomeWidgetGlanceStateDefinition()
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
Log.w("WIDGET_UPDATE", "PROVIDED GLANCE")
|
||||
// fetch a random photo from server
|
||||
val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
|
||||
val file = File(context.cacheDir, "widget_image_$appWidgetId.jpg")
|
||||
|
||||
var bitmap: Bitmap? = null
|
||||
|
||||
if (file.exists()) {
|
||||
bitmap = loadScaledBitmap(file, 500, 500)
|
||||
}
|
||||
|
||||
provideContent {
|
||||
|
||||
val prefs = currentState<HomeWidgetGlanceState>().preferences
|
||||
|
||||
val serverURL = prefs.getString("widget_auth_token", "")
|
||||
val sessionKey = prefs.getString("widget_auth_token", "")
|
||||
|
||||
|
||||
|
||||
PhotoWidget(image = null, error = null, subtitle = id.hashCode().toString())
|
||||
PhotoWidget(image = bitmap, error = null, subtitle = "hello")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun onDelete(context: Context, glanceId: GlanceId) {
|
||||
super.onDelete(context, glanceId)
|
||||
ImageDownloadWorker.cancel(context, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
class RefreshAction : ActionCallback {
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
// Clear the state to show loading screen
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
prefs.clear()
|
||||
}
|
||||
ImageGlanceWidget().update(context, glanceId)
|
||||
|
||||
parameters.
|
||||
|
||||
// Enqueue a job for each size the widget can be shown in the current state
|
||||
// (i.e landscape/portrait)
|
||||
GlanceAppWidgetManager(context).getAppWidgetSizes(glanceId).forEach { size ->
|
||||
ImageWorker.enqueue(context, size, glanceId, force = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
|
||||
data class WidgetEntry(
|
||||
val imageURI: String? = null,
|
||||
val error: WidgetError? = null,
|
||||
val subtitle: String? = null
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user