Skip to content

Commit 89f9d89

Browse files
committed
dialog up + android tv dialog integrated + cmts added + ui up + features++
1 parent f26f3f0 commit 89f9d89

File tree

15 files changed

+346
-141
lines changed

15 files changed

+346
-141
lines changed

directappupdate/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ dependencies {
5656
implementation(libs.androidx.ui.tooling.preview)
5757
implementation(libs.androidx.material3)
5858

59+
api(libs.androidx.tv.tv.foundation)
60+
api(libs.androidx.tv.tv.material)
61+
5962
implementation(platform(libs.okhttp.bom))
6063
implementation(libs.okhttp)
6164
implementation(libs.logging.interceptor)
@@ -69,6 +72,10 @@ dependencies {
6972
testImplementation(libs.junit)
7073
androidTestImplementation(libs.androidx.junit)
7174
androidTestImplementation(libs.androidx.espresso.core)
75+
androidTestImplementation(platform(libs.androidx.compose.bom))
76+
androidTestImplementation(libs.androidx.ui.test.junit4)
77+
debugImplementation(libs.androidx.ui.tooling)
78+
debugImplementation(libs.androidx.ui.test.manifest)
7279

7380
}
7481

directappupdate/src/main/java/com/micoder/directappupdate/DirectAppUpdate.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,75 +7,95 @@ import androidx.compose.runtime.LaunchedEffect
77
import androidx.compose.runtime.mutableStateOf
88
import androidx.compose.runtime.remember
99
import androidx.hilt.navigation.compose.hiltViewModel
10-
import com.micoder.directappupdate.dialogs.UpdateDialog
10+
import com.micoder.directappupdate.components.UpdateDialog
1111
import com.micoder.directappupdate.listeners.DirectUpdateListener
1212
import com.micoder.directappupdate.model.UpdateDialogState
1313
import com.micoder.directappupdate.model.UpdateType
1414
import com.micoder.directappupdate.viewmodel.NotificationViewModel
1515

16+
/**
17+
* [DirectAppUpdate] composable function that handles the direct app update process.
18+
*/
1619
@Composable
1720
fun DirectAppUpdate(activity: Activity, configUrl: String, notificationViewModel: NotificationViewModel = hiltViewModel(), appIcon: Int) {
1821

22+
/**
23+
* [UpdateDialogState] instance to hold the update dialog state.
24+
*/
1925
val updateDialogState = remember { mutableStateOf(UpdateDialogState()) }
2026

27+
/**
28+
* [DirectAppUpdateManager] instance to handle the direct app update process.
29+
*/
2130
val directAppUpdateManager = remember { DirectAppUpdateManager.Builder(activity) }
2231

32+
/**
33+
* [LaunchedEffect] that fetches the update config and listens for the update process and also to avoid recomposition.
34+
*/
2335
LaunchedEffect(key1 = true) {
2436
directAppUpdateManager.fetchUpdateConfig(
2537
configUrl = configUrl,
2638
onSuccess = { builder ->
39+
val appUpdateConfig = builder.appUpdateConfig
2740
builder.setDirectUpdateListener(object : DirectUpdateListener {
2841
override fun onImmediateUpdateAvailable() {
2942
updateDialogState.value = updateDialogState.value.copy(
3043
visible = true,
3144
updateType = UpdateType.Immediate,
32-
status = "Immediate Update Available",
33-
showUpdateButton = true
45+
status = "Update Available",
46+
showUpdateButton = true,
47+
config = appUpdateConfig
3448
)
3549
}
3650

3751
override fun onFlexibleUpdateAvailable() {
3852
updateDialogState.value = updateDialogState.value.copy(
3953
visible = true,
4054
updateType = UpdateType.Flexible,
41-
status = "Flexible Update Available",
42-
showUpdateButton = true
55+
status = "Update Available",
56+
showUpdateButton = true,
57+
config = appUpdateConfig
4358
)
4459
}
4560

4661
override fun onAlreadyUpToDate() {
4762
updateDialogState.value = updateDialogState.value.copy(
4863
status = "Already up to date",
49-
showUpdateButton = false
64+
showUpdateButton = false,
65+
config = appUpdateConfig
5066
)
5167
}
5268

5369
override fun onDownloadStart() {
5470
updateDialogState.value = updateDialogState.value.copy(
5571
status = "Download started",
56-
showUpdateButton = false
72+
showUpdateButton = false,
73+
config = appUpdateConfig
5774
)
5875
}
5976

6077
override fun onProgress(progress: Float) {
6178
notificationViewModel.showProgress(progress = progress.toInt(), icon = appIcon)
6279
updateDialogState.value = updateDialogState.value.copy(
6380
status = "Downloading: $progress%",
64-
progress = progress
81+
progress = progress,
82+
config = appUpdateConfig
6583
)
6684
}
6785

6886
override fun onDownloadComplete() {
6987
updateDialogState.value = updateDialogState.value.copy(
7088
status = "Download complete",
71-
showUpdateButton = false
89+
showUpdateButton = false,
90+
config = appUpdateConfig
7291
)
7392
}
7493

7594
override fun onDownloadFailed(error: String) {
7695
updateDialogState.value = updateDialogState.value.copy(
7796
status = "Download failed: $error",
78-
showUpdateButton = false
97+
showUpdateButton = false,
98+
config = appUpdateConfig
7999
)
80100
}
81101
}).build().checkForUpdate()
@@ -86,6 +106,9 @@ fun DirectAppUpdate(activity: Activity, configUrl: String, notificationViewModel
86106
)
87107
}
88108

109+
/**
110+
* [UpdateDialog] composable function that shows the update status and progress.
111+
*/
89112
UpdateDialog(
90113
dialogState = updateDialogState.value,
91114
onUpdateClick = { directAppUpdateManager.build().startUpdate() },

directappupdate/src/main/java/com/micoder/directappupdate/DirectAppUpdateManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class DirectAppUpdateManager private constructor(
3232
* [Builder] is a class that helps to build the [DirectAppUpdateManager] instance.
3333
*/
3434
class Builder(private val activity: Activity) {
35-
private lateinit var appUpdateConfig: AppUpdateConfig
35+
lateinit var appUpdateConfig: AppUpdateConfig
3636
private lateinit var directUpdateListener: DirectUpdateListener
3737

3838
/**
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
@file:Suppress("unused")
2+
3+
package com.micoder.directappupdate.components
4+
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.material3.Button
8+
import androidx.compose.material3.ButtonDefaults
9+
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.material3.Text
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.graphics.Color
14+
import androidx.compose.ui.unit.dp
15+
import androidx.tv.material3.ExperimentalTvMaterial3Api
16+
import androidx.tv.material3.Button as TvButton
17+
import androidx.tv.material3.Text as TvText
18+
19+
@OptIn(ExperimentalTvMaterial3Api::class)
20+
@Composable
21+
fun Button(
22+
text: String,
23+
modifier: Modifier = Modifier,
24+
enabled: Boolean = true,
25+
containerColor: Color = MaterialTheme.colorScheme.primary,
26+
contentColor: Color = MaterialTheme.colorScheme.onPrimary,
27+
disabledContainerColor: Color = containerColor.copy(alpha = 0.12f),
28+
disabledContentColor: Color = containerColor.copy(alpha = 0.38f),
29+
onClick: () -> Unit
30+
) {
31+
val spacing = LocalSpacing.current
32+
val tv = isTelevision()
33+
if (!tv) {
34+
Button(
35+
shape = RoundedCornerShape(8.dp),
36+
onClick = onClick,
37+
enabled = enabled,
38+
modifier = modifier,
39+
colors = ButtonDefaults.buttonColors(
40+
containerColor = containerColor,
41+
contentColor = contentColor,
42+
disabledContainerColor = disabledContainerColor,
43+
disabledContentColor = disabledContentColor
44+
)
45+
) {
46+
Text(
47+
text = text.uppercase()
48+
)
49+
}
50+
} else {
51+
TvButton(
52+
onClick = onClick,
53+
enabled = enabled,
54+
modifier = Modifier
55+
.padding(spacing.extraSmall)
56+
.then(modifier),
57+
colors = androidx.tv.material3.ButtonDefaults.colors(
58+
containerColor = containerColor,
59+
contentColor = contentColor,
60+
disabledContainerColor = disabledContainerColor,
61+
disabledContentColor = disabledContentColor
62+
)
63+
) {
64+
TvText(
65+
text = text.uppercase()
66+
)
67+
}
68+
}
69+
70+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package com.micoder.directappupdate.components
2+
3+
import androidx.compose.animation.animateContentSize
4+
import androidx.compose.foundation.BorderStroke
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.ColumnScope
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.layout.width
14+
import androidx.compose.foundation.layout.widthIn
15+
import androidx.compose.foundation.layout.wrapContentSize
16+
import androidx.compose.foundation.shape.RoundedCornerShape
17+
import androidx.compose.material3.LinearProgressIndicator
18+
import androidx.compose.material3.MaterialTheme
19+
import androidx.compose.material3.Surface
20+
import androidx.compose.material3.Text
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.tooling.preview.Preview
24+
import androidx.compose.ui.unit.dp
25+
import androidx.compose.ui.window.Dialog
26+
import androidx.compose.ui.window.DialogProperties
27+
import androidx.tv.material3.ExperimentalTvMaterial3Api
28+
import com.micoder.directappupdate.model.UpdateDialogState
29+
import com.micoder.directappupdate.model.UpdateType
30+
31+
/**
32+
* A root dialog component that wraps the content in a surface with a border and elevation.
33+
*/
34+
@Composable
35+
fun AppDialog(
36+
visible: Boolean,
37+
onDismiss: () -> Unit,
38+
modifier: Modifier = Modifier,
39+
border: BorderStroke = BorderStroke(
40+
2.dp,
41+
MaterialTheme.colorScheme.outline
42+
),
43+
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
44+
content: @Composable ColumnScope.() -> Unit,
45+
) {
46+
val theme = MaterialTheme.colorScheme
47+
val spacing = LocalSpacing.current
48+
49+
if (visible) {
50+
Dialog(
51+
onDismissRequest = onDismiss,
52+
properties = DialogProperties(
53+
usePlatformDefaultWidth = false
54+
)
55+
) {
56+
Surface(
57+
color = theme.background,
58+
contentColor = theme.onBackground,
59+
shape = RoundedCornerShape(LocalSpacing.current.medium),
60+
border = border,
61+
tonalElevation = spacing.medium,
62+
modifier = Modifier
63+
.padding(spacing.medium)
64+
.fillMaxWidth()
65+
.wrapContentSize()
66+
.animateContentSize()
67+
.then(modifier)
68+
) {
69+
Column(
70+
verticalArrangement = verticalArrangement,
71+
modifier = Modifier
72+
.fillMaxWidth()
73+
.padding(spacing.medium),
74+
content = content
75+
)
76+
}
77+
}
78+
}
79+
}
80+
81+
/**
82+
* A dialog that shows the update status and progress.
83+
*/
84+
@OptIn(ExperimentalTvMaterial3Api::class)
85+
@Composable
86+
fun UpdateDialog(
87+
dialogState: UpdateDialogState,
88+
onUpdateClick: () -> Unit,
89+
onCancelClick: () -> Unit
90+
) {
91+
92+
val tv = isTelevision()
93+
AppDialog(
94+
visible = dialogState.visible,
95+
onDismiss = onCancelClick,
96+
modifier = Modifier.widthIn(max = 500.dp),
97+
verticalArrangement = Arrangement.spacedBy(8.dp)
98+
) {
99+
if (!tv) {
100+
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
101+
Spacer(modifier = Modifier.height(8.dp))
102+
dialogState.config?.let {
103+
Text(text = it.appName, style = MaterialTheme.typography.headlineSmall)
104+
}
105+
Spacer(modifier = Modifier.height(8.dp))
106+
Text(text = dialogState.status, style = MaterialTheme.typography.bodyLarge)
107+
Spacer(modifier = Modifier.height(16.dp))
108+
if (dialogState.showUpdateButton) {
109+
Row {
110+
Button(text = "Update App", onClick = onUpdateClick)
111+
if (dialogState.updateType == UpdateType.Flexible) {
112+
Spacer(modifier = Modifier.width(8.dp))
113+
Button(text = "Cancel", onClick = onCancelClick)
114+
}
115+
}
116+
} else {
117+
Spacer(modifier = Modifier.height(8.dp))
118+
LinearProgressIndicator(
119+
progress = dialogState.progress / 100,
120+
modifier = Modifier.fillMaxWidth()
121+
)
122+
Spacer(modifier = Modifier.height(8.dp))
123+
}
124+
}
125+
} else {
126+
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
127+
Spacer(modifier = Modifier.height(8.dp))
128+
dialogState.config?.let {
129+
Text(text = it.appName, style = MaterialTheme.typography.headlineSmall)
130+
}
131+
Spacer(modifier = Modifier.height(8.dp))
132+
Text(text = dialogState.status, style = MaterialTheme.typography.bodyLarge)
133+
Spacer(modifier = Modifier.height(16.dp))
134+
if (dialogState.showUpdateButton) {
135+
Row {
136+
androidx.tv.material3.Button(onClick = onUpdateClick) {
137+
Text(text = "Update App")
138+
}
139+
if (dialogState.updateType == UpdateType.Flexible) {
140+
Spacer(modifier = Modifier.width(8.dp))
141+
androidx.tv.material3.Button(onClick = onCancelClick) {
142+
Text(text = "Cancel")
143+
}
144+
}
145+
}
146+
} else {
147+
Spacer(modifier = Modifier.height(8.dp))
148+
LinearProgressIndicator(
149+
progress = dialogState.progress / 100,
150+
modifier = Modifier.fillMaxWidth()
151+
)
152+
Spacer(modifier = Modifier.height(8.dp))
153+
}
154+
}
155+
}
156+
}
157+
158+
}
159+
160+
@Preview
161+
@Composable
162+
fun UpdateDialogPreview() {
163+
UpdateDialog(
164+
dialogState = UpdateDialogState(
165+
visible = true,
166+
status = "Downloading update",
167+
showUpdateButton = false,
168+
progress = 50f,
169+
updateType = UpdateType.Flexible
170+
),
171+
onUpdateClick = {},
172+
onCancelClick = {}
173+
)
174+
}

0 commit comments

Comments
 (0)