diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index e4da00c13d..e9b8a3b6d2 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -155,16 +155,16 @@
android:resource="@xml/widget" />
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt
new file mode 100644
index 0000000000..9188df1700
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt
@@ -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
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt
index 615f42523a..b454fff677 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt
@@ -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().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(
+ 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)
}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt
index d22a8a8a30..ec35a710ce 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt
@@ -22,8 +22,7 @@ class ImmichAPI(cfg: ServerConfig) {
private val serverConfig = cfg
private fun buildRequestURL(endpoint: String, params: List> = 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")}")
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt
index ad2ba2ec8a..0a24f59001 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt
@@ -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,
- 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")
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt
index 614ba7458f..780e2b17a4 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt
@@ -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")
}
}
}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt
index 8e019c48e8..d2307bd3c6 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt
@@ -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() {
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")
+ }
+ }
}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomWidget.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomWidget.kt
index e3de9421d1..ba88cf47cb 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomWidget.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomWidget.kt
@@ -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
get() = HomeWidgetGlanceStateDefinition()
override suspend fun provideGlance(context: Context, id: GlanceId) {
- // fetch a random photo from server
- provideContent {
+ 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")
- val prefs = currentState().preferences
+ var bitmap: Bitmap? = null
- val serverURL = prefs.getString("widget_auth_token", "")
- val sessionKey = prefs.getString("widget_auth_token", "")
+ if (file.exists()) {
+ bitmap = loadScaledBitmap(file, 500, 500)
+ }
-
-
- PhotoWidget(image = null, error = null, subtitle = id.hashCode().toString())
- }
+ provideContent {
+ 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)
- }
- }
-}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/WidgetEntry.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/WidgetEntry.kt
deleted file mode 100644
index 94b8704650..0000000000
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/WidgetEntry.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package app.alextran.immich.widget
-
-
-data class WidgetEntry(
- val imageURI: String? = null,
- val error: WidgetError? = null,
- val subtitle: String? = null
-)