@glancekit/android-core 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/build.gradle.kts +51 -0
- package/consumer-rules.pro +1 -0
- package/index.js +1 -0
- package/package.json +34 -0
- package/src/main/java/dev/glancekit/androidcore/ProgressCardData.kt +29 -0
- package/src/main/java/dev/glancekit/androidcore/WidgetStateRepository.kt +76 -0
- package/src/main/java/dev/glancekit/androidcore/WidgetUpdateManager.kt +102 -0
- package/src/main/java/dev/glancekit/androidcore/widget/ProgressCardWidget.kt +185 -0
- package/src/main/java/dev/glancekit/androidcore/widget/ProgressCardWidgetReceiver.kt +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @glancekit/android-core
|
|
2
|
+
|
|
3
|
+
Pure Android core module for GlanceKit alpha.
|
|
4
|
+
|
|
5
|
+
This module owns the pure Android implementation:
|
|
6
|
+
|
|
7
|
+
- `ProgressCardData`
|
|
8
|
+
- `WidgetStateRepository`
|
|
9
|
+
- `WidgetUpdateManager`
|
|
10
|
+
- `ProgressCardWidget`
|
|
11
|
+
- `ProgressCardWidgetReceiver`
|
|
12
|
+
|
|
13
|
+
It intentionally does not depend on React Native.
|
|
14
|
+
|
|
15
|
+
## Intended Use
|
|
16
|
+
|
|
17
|
+
Most React Native and Expo consumers should install:
|
|
18
|
+
|
|
19
|
+
- `@glancekit/react-native`
|
|
20
|
+
- `@glancekit/expo-plugin` for Expo managed builds
|
|
21
|
+
|
|
22
|
+
This package exists so the Android implementation has a clean boundary and can be consumed directly by:
|
|
23
|
+
|
|
24
|
+
- the native Android demo in `examples/native-android-demo`
|
|
25
|
+
- `@glancekit/react-native` as a bridge dependency
|
|
26
|
+
|
|
27
|
+
## Alpha Notes
|
|
28
|
+
|
|
29
|
+
- Android only
|
|
30
|
+
- internal/power-user package for alpha
|
|
31
|
+
- `ProgressCardWidget` is the only widget template
|
|
32
|
+
- no React Native JS API is exposed from this package
|
package/build.gradle.kts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
|
2
|
+
|
|
3
|
+
plugins {
|
|
4
|
+
id("com.android.library")
|
|
5
|
+
id("org.jetbrains.kotlin.android")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
val composeExpectedKotlinVersion = "1.9.24"
|
|
9
|
+
val detectedKotlinVersion = KotlinCompilerVersion.VERSION
|
|
10
|
+
|
|
11
|
+
android {
|
|
12
|
+
namespace = "dev.glancekit.androidcore"
|
|
13
|
+
compileSdk = 34
|
|
14
|
+
|
|
15
|
+
defaultConfig {
|
|
16
|
+
minSdk = 26
|
|
17
|
+
consumerProguardFiles("consumer-rules.pro")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
buildFeatures {
|
|
21
|
+
compose = true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
compileOptions {
|
|
25
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
26
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
composeOptions {
|
|
30
|
+
kotlinCompilerExtensionVersion = "1.5.14"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
kotlinOptions {
|
|
34
|
+
jvmTarget = "17"
|
|
35
|
+
if (detectedKotlinVersion != composeExpectedKotlinVersion) {
|
|
36
|
+
freeCompilerArgs += listOf(
|
|
37
|
+
"-P",
|
|
38
|
+
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=$detectedKotlinVersion",
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dependencies {
|
|
45
|
+
implementation("androidx.core:core-ktx:1.13.1")
|
|
46
|
+
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
|
47
|
+
api("androidx.glance:glance-appwidget:1.1.1")
|
|
48
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
|
|
49
|
+
|
|
50
|
+
testImplementation("junit:junit:4.13.2")
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glancekit/android-core",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Pure Android Glance/DataStore core module for GlanceKit alpha.",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"README.md",
|
|
9
|
+
"package.json",
|
|
10
|
+
"index.js",
|
|
11
|
+
"build.gradle.kts",
|
|
12
|
+
"consumer-rules.pro",
|
|
13
|
+
"src/main"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"glancekit",
|
|
17
|
+
"android",
|
|
18
|
+
"jetpack-glance",
|
|
19
|
+
"android-widget",
|
|
20
|
+
"datastore"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "echo android-core build is driven by Gradle consumers",
|
|
28
|
+
"lint": "echo android-core lint pending",
|
|
29
|
+
"typecheck": "echo android-core typecheck pending"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package dev.glancekit.androidcore
|
|
2
|
+
|
|
3
|
+
data class ProgressCardData(
|
|
4
|
+
val title: String,
|
|
5
|
+
val subtitle: String,
|
|
6
|
+
val progress: Int,
|
|
7
|
+
val deepLink: String? = null,
|
|
8
|
+
) {
|
|
9
|
+
fun normalized(): ProgressCardData = copy(
|
|
10
|
+
title = title.trim(),
|
|
11
|
+
subtitle = subtitle.trim(),
|
|
12
|
+
deepLink = deepLink?.trim()?.takeIf { it.isNotEmpty() },
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
fun validate() {
|
|
16
|
+
require(title.isNotBlank()) { "Widget title cannot be empty." }
|
|
17
|
+
require(subtitle.isNotBlank()) { "Widget subtitle cannot be empty." }
|
|
18
|
+
require(progress in 0..100) { "Widget progress must be between 0 and 100." }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
companion object {
|
|
22
|
+
val DEFAULT = ProgressCardData(
|
|
23
|
+
title = "Daily Progress",
|
|
24
|
+
subtitle = "Tap the app to update this widget.",
|
|
25
|
+
progress = 0,
|
|
26
|
+
deepLink = null,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package dev.glancekit.androidcore
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.datastore.preferences.core.edit
|
|
6
|
+
import androidx.datastore.preferences.core.intPreferencesKey
|
|
7
|
+
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
8
|
+
import androidx.datastore.preferences.preferencesDataStore
|
|
9
|
+
import kotlinx.coroutines.flow.first
|
|
10
|
+
|
|
11
|
+
private val Context.widgetDataStore by preferencesDataStore(name = "glancekit_widgets")
|
|
12
|
+
|
|
13
|
+
internal object WidgetStateRepository {
|
|
14
|
+
suspend fun loadWidget(context: Context, widgetId: Int): ProgressCardData {
|
|
15
|
+
if (widgetId <= 0) {
|
|
16
|
+
Log.w(TAG, "loadWidget called with invalid widgetId=$widgetId, returning default state")
|
|
17
|
+
return ProgressCardData.DEFAULT
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
val preferences = context.widgetDataStore.data.first()
|
|
21
|
+
val data = ProgressCardData(
|
|
22
|
+
title = preferences[stringPreferencesKey(titleKey(widgetId))]
|
|
23
|
+
?: ProgressCardData.DEFAULT.title,
|
|
24
|
+
subtitle = preferences[stringPreferencesKey(subtitleKey(widgetId))]
|
|
25
|
+
?: ProgressCardData.DEFAULT.subtitle,
|
|
26
|
+
progress = preferences[intPreferencesKey(progressKey(widgetId))]
|
|
27
|
+
?: ProgressCardData.DEFAULT.progress,
|
|
28
|
+
deepLink = preferences[stringPreferencesKey(deepLinkKey(widgetId))],
|
|
29
|
+
)
|
|
30
|
+
Log.d(
|
|
31
|
+
TAG,
|
|
32
|
+
"loadWidget widgetId=$widgetId title=${data.title} progress=${data.progress} deepLink=${data.deepLink}",
|
|
33
|
+
)
|
|
34
|
+
return data
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
suspend fun saveWidget(context: Context, widgetId: Int, data: ProgressCardData) {
|
|
38
|
+
Log.d(
|
|
39
|
+
TAG,
|
|
40
|
+
"saveWidget widgetId=$widgetId title=${data.title} progress=${data.progress} deepLink=${data.deepLink}",
|
|
41
|
+
)
|
|
42
|
+
context.widgetDataStore.edit { preferences ->
|
|
43
|
+
preferences[stringPreferencesKey(titleKey(widgetId))] = data.title
|
|
44
|
+
preferences[stringPreferencesKey(subtitleKey(widgetId))] = data.subtitle
|
|
45
|
+
preferences[intPreferencesKey(progressKey(widgetId))] = data.progress
|
|
46
|
+
val deepLinkKey = stringPreferencesKey(deepLinkKey(widgetId))
|
|
47
|
+
if (data.deepLink == null) {
|
|
48
|
+
preferences.remove(deepLinkKey)
|
|
49
|
+
} else {
|
|
50
|
+
preferences[deepLinkKey] = data.deepLink
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
suspend fun deleteWidget(context: Context, widgetId: Int) {
|
|
56
|
+
if (widgetId <= 0) {
|
|
57
|
+
Log.w(TAG, "deleteWidget skipped for invalid widgetId=$widgetId")
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Log.d(TAG, "deleteWidget widgetId=$widgetId")
|
|
62
|
+
context.widgetDataStore.edit { preferences ->
|
|
63
|
+
preferences.remove(stringPreferencesKey(titleKey(widgetId)))
|
|
64
|
+
preferences.remove(stringPreferencesKey(subtitleKey(widgetId)))
|
|
65
|
+
preferences.remove(intPreferencesKey(progressKey(widgetId)))
|
|
66
|
+
preferences.remove(stringPreferencesKey(deepLinkKey(widgetId)))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun titleKey(widgetId: Int) = "widget_${widgetId}_title"
|
|
71
|
+
private fun subtitleKey(widgetId: Int) = "widget_${widgetId}_subtitle"
|
|
72
|
+
private fun progressKey(widgetId: Int) = "widget_${widgetId}_progress"
|
|
73
|
+
private fun deepLinkKey(widgetId: Int) = "widget_${widgetId}_deep_link"
|
|
74
|
+
|
|
75
|
+
private const val TAG = "GlanceKitState"
|
|
76
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package dev.glancekit.androidcore
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.glance.appwidget.GlanceAppWidgetManager
|
|
6
|
+
import androidx.glance.appwidget.updateAll
|
|
7
|
+
import dev.glancekit.androidcore.widget.ProgressCardWidget
|
|
8
|
+
|
|
9
|
+
object WidgetUpdateManager {
|
|
10
|
+
suspend fun updateProgressCardWidget(
|
|
11
|
+
context: Context,
|
|
12
|
+
widgetId: Int,
|
|
13
|
+
data: ProgressCardData,
|
|
14
|
+
widgetClass: Class<out ProgressCardWidget> = ProgressCardWidget::class.java,
|
|
15
|
+
): ProgressCardData {
|
|
16
|
+
require(widgetId > 0) { "Widget ID must be a positive integer." }
|
|
17
|
+
|
|
18
|
+
Log.d(TAG, "updateProgressCardWidget start widgetId=$widgetId widgetClass=${widgetClass.name} rawData=$data")
|
|
19
|
+
val normalized = data.normalized()
|
|
20
|
+
normalized.validate()
|
|
21
|
+
WidgetStateRepository.saveWidget(context, widgetId, normalized)
|
|
22
|
+
refreshProgressCardWidget(context, widgetId, widgetClass)
|
|
23
|
+
Log.d(TAG, "updateProgressCardWidget success widgetId=$widgetId normalized=$normalized")
|
|
24
|
+
return normalized
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
suspend fun loadProgressCardWidget(
|
|
28
|
+
context: Context,
|
|
29
|
+
widgetId: Int,
|
|
30
|
+
): ProgressCardData {
|
|
31
|
+
Log.d(TAG, "loadProgressCardWidget widgetId=$widgetId")
|
|
32
|
+
return WidgetStateRepository.loadWidget(context, widgetId)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
suspend fun refreshProgressCardWidget(
|
|
36
|
+
context: Context,
|
|
37
|
+
widgetId: Int,
|
|
38
|
+
widgetClass: Class<out ProgressCardWidget> = ProgressCardWidget::class.java,
|
|
39
|
+
) {
|
|
40
|
+
Log.d(TAG, "refreshProgressCardWidget widgetId=$widgetId widgetClass=${widgetClass.name}")
|
|
41
|
+
val manager = GlanceAppWidgetManager(context)
|
|
42
|
+
val glanceIds = manager.getGlanceIds(widgetClass)
|
|
43
|
+
Log.d(TAG, "refreshProgressCardWidget glanceIds count=${glanceIds.size} values=$glanceIds")
|
|
44
|
+
val widget = widgetClass.getDeclaredConstructor().newInstance()
|
|
45
|
+
glanceIds.forEach { glanceId ->
|
|
46
|
+
val appWidgetId = manager.getAppWidgetId(glanceId)
|
|
47
|
+
if (appWidgetId == widgetId) {
|
|
48
|
+
Log.d(TAG, "refreshProgressCardWidget matched glanceId=$glanceId appWidgetId=$appWidgetId")
|
|
49
|
+
widget.update(context, glanceId)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
suspend fun refreshAllProgressCardWidgets(
|
|
55
|
+
context: Context,
|
|
56
|
+
widgetClass: Class<out ProgressCardWidget> = ProgressCardWidget::class.java,
|
|
57
|
+
) {
|
|
58
|
+
Log.d(TAG, "refreshAllProgressCardWidgets widgetClass=${widgetClass.name}")
|
|
59
|
+
widgetClass.getDeclaredConstructor().newInstance().updateAll(context)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
suspend fun updateAllProgressCardWidgets(
|
|
63
|
+
context: Context,
|
|
64
|
+
data: ProgressCardData,
|
|
65
|
+
widgetClass: Class<out ProgressCardWidget> = ProgressCardWidget::class.java,
|
|
66
|
+
): Int {
|
|
67
|
+
val normalized = data.normalized()
|
|
68
|
+
normalized.validate()
|
|
69
|
+
|
|
70
|
+
val manager = GlanceAppWidgetManager(context)
|
|
71
|
+
val glanceIds = manager.getGlanceIds(widgetClass)
|
|
72
|
+
Log.d(
|
|
73
|
+
TAG,
|
|
74
|
+
"updateAllProgressCardWidgets widgetClass=${widgetClass.name} glanceIds count=${glanceIds.size} values=$glanceIds",
|
|
75
|
+
)
|
|
76
|
+
check(glanceIds.isNotEmpty()) {
|
|
77
|
+
"No ProgressCardWidget instances are currently added to the home screen. " +
|
|
78
|
+
"Searched for widgetClass=${widgetClass.name}."
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Log.d(
|
|
82
|
+
TAG,
|
|
83
|
+
"updateAllProgressCardWidgets count=${glanceIds.size} normalized=$normalized",
|
|
84
|
+
)
|
|
85
|
+
val widget = widgetClass.getDeclaredConstructor().newInstance()
|
|
86
|
+
glanceIds.forEach { glanceId ->
|
|
87
|
+
val appWidgetId = manager.getAppWidgetId(glanceId)
|
|
88
|
+
WidgetStateRepository.saveWidget(context, appWidgetId, normalized)
|
|
89
|
+
Log.d(TAG, "updateAllProgressCardWidgets updating glanceId=$glanceId appWidgetId=$appWidgetId")
|
|
90
|
+
widget.update(context, glanceId)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return glanceIds.size
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
suspend fun deleteProgressCardWidget(context: Context, widgetId: Int) {
|
|
97
|
+
Log.d(TAG, "deleteProgressCardWidget widgetId=$widgetId")
|
|
98
|
+
WidgetStateRepository.deleteWidget(context, widgetId)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private const val TAG = "GlanceKitUpdate"
|
|
102
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
package dev.glancekit.androidcore.widget
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.compose.runtime.Composable
|
|
8
|
+
import androidx.compose.ui.unit.dp
|
|
9
|
+
import androidx.glance.GlanceId
|
|
10
|
+
import androidx.glance.GlanceModifier
|
|
11
|
+
import androidx.glance.action.Action
|
|
12
|
+
import androidx.glance.action.clickable
|
|
13
|
+
import androidx.glance.appwidget.GlanceAppWidget
|
|
14
|
+
import androidx.glance.appwidget.GlanceAppWidgetManager
|
|
15
|
+
import androidx.glance.appwidget.action.actionStartActivity
|
|
16
|
+
import androidx.glance.appwidget.provideContent
|
|
17
|
+
import androidx.glance.layout.Column
|
|
18
|
+
import androidx.glance.layout.Spacer
|
|
19
|
+
import androidx.glance.layout.height
|
|
20
|
+
import androidx.glance.layout.padding
|
|
21
|
+
import androidx.glance.text.Text
|
|
22
|
+
import dev.glancekit.androidcore.ProgressCardData
|
|
23
|
+
import dev.glancekit.androidcore.WidgetUpdateManager
|
|
24
|
+
import kotlinx.coroutines.Dispatchers
|
|
25
|
+
import kotlinx.coroutines.withContext
|
|
26
|
+
|
|
27
|
+
open class ProgressCardWidget : GlanceAppWidget() {
|
|
28
|
+
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
|
29
|
+
Log.d(TAG, "provideGlance start glanceId=$id")
|
|
30
|
+
try {
|
|
31
|
+
val renderState = loadRenderState(context, id)
|
|
32
|
+
Log.d(TAG, "provideGlance before provideContent glanceId=$id")
|
|
33
|
+
provideContent {
|
|
34
|
+
Log.d(
|
|
35
|
+
TAG,
|
|
36
|
+
"provideContent entered glanceId=$id mode=${renderState.mode} widgetId=${renderState.widgetId}",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
when (renderState.mode) {
|
|
40
|
+
RenderMode.Fallback -> {
|
|
41
|
+
Log.d(TAG, "render static content fallback glanceId=$id")
|
|
42
|
+
MinimalFallbackContent()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
RenderMode.Data -> {
|
|
46
|
+
Log.d(
|
|
47
|
+
TAG,
|
|
48
|
+
"render data content widgetId=${renderState.widgetId} title=${renderState.data.title}",
|
|
49
|
+
)
|
|
50
|
+
ProgressCardTextContent(
|
|
51
|
+
data = renderState.data,
|
|
52
|
+
onClick = renderState.onClick,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error: Throwable) {
|
|
58
|
+
Log.e(TAG, "provideGlance failure catch glanceId=$id", error)
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private suspend fun loadRenderState(context: Context, id: GlanceId): RenderState {
|
|
64
|
+
return runCatching {
|
|
65
|
+
val manager = GlanceAppWidgetManager(context)
|
|
66
|
+
val widgetId = manager.getAppWidgetId(id)
|
|
67
|
+
Log.d(TAG, "state read start widgetId=$widgetId glanceId=$id")
|
|
68
|
+
|
|
69
|
+
val data = withContext(Dispatchers.IO) {
|
|
70
|
+
WidgetUpdateManager.loadProgressCardWidget(context, widgetId)
|
|
71
|
+
}.also { loadedData ->
|
|
72
|
+
Log.d(
|
|
73
|
+
TAG,
|
|
74
|
+
"state read success widgetId=$widgetId title=${loadedData.title} progress=${loadedData.progress}",
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
RenderState(
|
|
79
|
+
widgetId = widgetId,
|
|
80
|
+
data = data,
|
|
81
|
+
onClick = createClickAction(context, widgetId, data),
|
|
82
|
+
mode = RenderMode.Data,
|
|
83
|
+
)
|
|
84
|
+
}.getOrElse { error ->
|
|
85
|
+
Log.e(TAG, "state read failure glanceId=$id", error)
|
|
86
|
+
RenderState(
|
|
87
|
+
widgetId = -1,
|
|
88
|
+
data = ProgressCardData.DEFAULT,
|
|
89
|
+
onClick = null,
|
|
90
|
+
mode = RenderMode.Fallback,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private fun createClickAction(
|
|
96
|
+
context: Context,
|
|
97
|
+
widgetId: Int,
|
|
98
|
+
data: ProgressCardData,
|
|
99
|
+
): Action? {
|
|
100
|
+
Log.d(TAG, "click action creation start widgetId=$widgetId")
|
|
101
|
+
return runCatching {
|
|
102
|
+
val intent = createDeepLinkIntent(context, widgetId, data)
|
|
103
|
+
actionStartActivity(intent).also {
|
|
104
|
+
Log.d(TAG, "click action creation success widgetId=$widgetId uri=${intent.data}")
|
|
105
|
+
}
|
|
106
|
+
}.getOrElse { error ->
|
|
107
|
+
Log.e(TAG, "click action creation failure widgetId=$widgetId", error)
|
|
108
|
+
null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun createDeepLinkIntent(
|
|
113
|
+
context: Context,
|
|
114
|
+
widgetId: Int,
|
|
115
|
+
data: ProgressCardData,
|
|
116
|
+
): Intent {
|
|
117
|
+
val deepLinkUri = data.deepLink?.let(Uri::parse) ?: Uri.Builder()
|
|
118
|
+
.scheme("glancekit")
|
|
119
|
+
.authority("progress")
|
|
120
|
+
.appendPath(widgetId.toString())
|
|
121
|
+
.build()
|
|
122
|
+
|
|
123
|
+
return Intent(Intent.ACTION_VIEW, deepLinkUri).apply {
|
|
124
|
+
`package` = context.packageName
|
|
125
|
+
addFlags(
|
|
126
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
127
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
128
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
companion object {
|
|
134
|
+
private const val TAG = "GlanceKitWidget"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private data class RenderState(
|
|
139
|
+
val widgetId: Int,
|
|
140
|
+
val data: ProgressCardData,
|
|
141
|
+
val onClick: Action?,
|
|
142
|
+
val mode: RenderMode,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
private enum class RenderMode {
|
|
146
|
+
Fallback,
|
|
147
|
+
Data,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Composable
|
|
151
|
+
private fun MinimalFallbackContent() {
|
|
152
|
+
Column(modifier = GlanceModifier.padding(CONTENT_PADDING)) {
|
|
153
|
+
Text(text = "GlanceKit")
|
|
154
|
+
Spacer(modifier = GlanceModifier.height(TEXT_SPACING_SMALL))
|
|
155
|
+
Text(text = "Waiting for data")
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Composable
|
|
160
|
+
private fun ProgressCardTextContent(
|
|
161
|
+
data: ProgressCardData,
|
|
162
|
+
onClick: Action?,
|
|
163
|
+
) {
|
|
164
|
+
val contentModifier = GlanceModifier
|
|
165
|
+
.padding(CONTENT_PADDING)
|
|
166
|
+
.let { modifier ->
|
|
167
|
+
if (onClick == null) {
|
|
168
|
+
modifier
|
|
169
|
+
} else {
|
|
170
|
+
modifier.clickable(onClick)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Column(modifier = contentModifier) {
|
|
175
|
+
Text(text = data.title.ifBlank { ProgressCardData.DEFAULT.title })
|
|
176
|
+
Spacer(modifier = GlanceModifier.height(TEXT_SPACING_SMALL))
|
|
177
|
+
Text(text = data.subtitle.ifBlank { ProgressCardData.DEFAULT.subtitle })
|
|
178
|
+
Spacer(modifier = GlanceModifier.height(TEXT_SPACING_LARGE))
|
|
179
|
+
Text(text = "${data.progress.coerceIn(0, 100)}% complete")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private val CONTENT_PADDING = 16.dp
|
|
184
|
+
private val TEXT_SPACING_SMALL = 4.dp
|
|
185
|
+
private val TEXT_SPACING_LARGE = 10.dp
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package dev.glancekit.androidcore.widget
|
|
2
|
+
|
|
3
|
+
import android.appwidget.AppWidgetManager
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.glance.appwidget.GlanceAppWidget
|
|
8
|
+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
|
9
|
+
import dev.glancekit.androidcore.WidgetUpdateManager
|
|
10
|
+
import kotlinx.coroutines.runBlocking
|
|
11
|
+
|
|
12
|
+
open class ProgressCardWidgetReceiver : GlanceAppWidgetReceiver() {
|
|
13
|
+
override val glanceAppWidget: GlanceAppWidget = ProgressCardWidget()
|
|
14
|
+
|
|
15
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
16
|
+
Log.d(TAG, "onReceive action=${intent.action}")
|
|
17
|
+
super.onReceive(context, intent)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun onUpdate(
|
|
21
|
+
context: Context,
|
|
22
|
+
appWidgetManager: AppWidgetManager,
|
|
23
|
+
appWidgetIds: IntArray,
|
|
24
|
+
) {
|
|
25
|
+
Log.d(TAG, "onUpdate widgetIds=${appWidgetIds.joinToString()}")
|
|
26
|
+
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun onEnabled(context: Context) {
|
|
30
|
+
Log.d(TAG, "onEnabled")
|
|
31
|
+
super.onEnabled(context)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun onDisabled(context: Context) {
|
|
35
|
+
Log.d(TAG, "onDisabled")
|
|
36
|
+
super.onDisabled(context)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
|
40
|
+
Log.d(TAG, "onDeleted widgetIds=${appWidgetIds.joinToString()}")
|
|
41
|
+
super.onDeleted(context, appWidgetIds)
|
|
42
|
+
runBlocking {
|
|
43
|
+
appWidgetIds.forEach { widgetId ->
|
|
44
|
+
WidgetUpdateManager.deleteProgressCardWidget(context, widgetId)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
companion object {
|
|
50
|
+
private const val TAG = "GlanceKitReceiver"
|
|
51
|
+
}
|
|
52
|
+
}
|