Skip to content

Commit 218b0db

Browse files
authored
Merge pull request #461 from diasDominik/add-widget-configuration-support
Add Widget support for Upcoming Payments
2 parents 46a1ff4 + 9699545 commit 218b0db

File tree

20 files changed

+591
-36
lines changed

20 files changed

+591
-36
lines changed

app/build.gradle.kts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ kotlin {
5353

5454
implementation(libs.koin.android)
5555
implementation(libs.koin.androidx.compose)
56+
implementation(libs.androidx.glance.appwidget)
57+
implementation(libs.androidx.glance.material3)
5658
}
5759
commonMain.dependencies {
5860
implementation(compose.components.resources)
@@ -154,14 +156,19 @@ android {
154156
}
155157
}
156158

157-
compose.desktop {
158-
application {
159-
mainClass = "MainKt"
159+
compose {
160+
resources {
161+
publicResClass = true
162+
}
163+
desktop {
164+
application {
165+
mainClass = "MainKt"
160166

161-
nativeDistributions {
162-
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
163-
packageName = "de.dbauer.expensetracker"
164-
packageVersion = "1.0.0"
167+
nativeDistributions {
168+
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
169+
packageName = "de.dbauer.expensetracker"
170+
packageVersion = "1.0.0"
171+
}
165172
}
166173
}
167174
}

app/src/androidMain/AndroidManifest.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
<category android:name="android.intent.category.LAUNCHER" />
2727
</intent-filter>
2828
</activity>
29+
<activity android:name=".widget.ConfigureWidgetActivity"
30+
android:exported="true">
31+
<intent-filter>
32+
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
33+
</intent-filter>
34+
</activity>
2935
<receiver
3036
android:name="model.notification.NotificationLoopReceiver"
3137
android:exported="true">
@@ -37,6 +43,15 @@
3743
android:name="model.notification.DismissExpenseNotificationReceiver"
3844
android:exported="false">
3945
</receiver>
46+
<receiver android:name=".widget.UpcomingPaymentsWidgetReceiver"
47+
android:exported="true">
48+
<intent-filter>
49+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
50+
</intent-filter>
51+
<meta-data
52+
android:name="android.appwidget.provider"
53+
android:resource="@xml/my_app_widget_info" />
54+
</receiver>
4055
</application>
4156

4257
</manifest>

app/src/androidMain/kotlin/de/dbauer/expensetracker/MainActivity.kt

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue
3030
import androidx.compose.ui.Alignment
3131
import androidx.compose.ui.Modifier
3232
import androidx.core.content.IntentCompat
33+
import androidx.glance.appwidget.GlanceAppWidgetManager
3334
import androidx.lifecycle.lifecycleScope
3435
import asString
3536
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -39,6 +40,8 @@ import com.google.accompanist.permissions.rememberPermissionState
3940
import data.HomePane
4041
import data.SettingsPane
4142
import data.UpcomingPane
43+
import de.dbauer.expensetracker.widget.UpcomingPaymentsWidget
44+
import kotlinx.coroutines.Dispatchers
4245
import kotlinx.coroutines.flow.collectLatest
4346
import kotlinx.coroutines.launch
4447
import model.DatabaseBackupRestore
@@ -252,33 +255,42 @@ class MainActivity : AppCompatActivity() {
252255
}
253256
} else {
254257
MainContent(
255-
onClickBackup = {
256-
backupPathLauncher.launch(Constants.DEFAULT_BACKUP_NAME)
257-
},
258-
onClickRestore = {
259-
importPathLauncher.launch(arrayOf(Constants.BACKUP_MIME_TYPE))
260-
},
261258
isGridMode = isGridMode,
262259
biometricSecurity = biometricSecurity,
263-
onBiometricSecurityChange = {
260+
canUseBiometric = canUseBiometric,
261+
canUseNotifications = true,
262+
hasNotificationPermission = notificationPermissionGranted,
263+
toggleGridMode = {
264264
lifecycleScope.launch {
265-
userPreferencesRepository.biometricSecurity.save(it)
265+
userPreferencesRepository.gridMode.save(!isGridMode)
266266
}
267267
},
268-
toggleGridMode = {
268+
onBiometricSecurityChange = {
269269
lifecycleScope.launch {
270-
userPreferencesRepository.gridMode.save(!isGridMode)
270+
userPreferencesRepository.biometricSecurity.save(it)
271271
}
272272
},
273-
canUseBiometric = canUseBiometric,
274-
canUseNotifications = true,
275-
hasNotificationPermission = notificationPermissionGranted,
276273
requestNotificationPermission = {
277274
notificationPermissionState?.launchPermissionRequest()
278275
},
279276
navigateToPermissionsSettings = {
280277
navigateToNotificationPermissionSettings()
281278
},
279+
onClickBackup = {
280+
backupPathLauncher.launch(Constants.DEFAULT_BACKUP_NAME)
281+
},
282+
onClickRestore = {
283+
importPathLauncher.launch(arrayOf(Constants.BACKUP_MIME_TYPE))
284+
},
285+
updateWidget = {
286+
lifecycleScope.launch(Dispatchers.Main.immediate) {
287+
GlanceAppWidgetManager(
288+
context = this@MainActivity,
289+
).getGlanceIds(UpcomingPaymentsWidget::class.java).forEach {
290+
UpcomingPaymentsWidget().update(this@MainActivity, it)
291+
}
292+
}
293+
},
282294
startRoute = startRoute,
283295
)
284296
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package de.dbauer.expensetracker.widget
2+
3+
import android.appwidget.AppWidgetManager
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.activity.compose.setContent
7+
import androidx.activity.enableEdgeToEdge
8+
import androidx.appcompat.app.AppCompatActivity
9+
import androidx.compose.foundation.layout.Arrangement
10+
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.Row
12+
import androidx.compose.foundation.layout.Spacer
13+
import androidx.compose.foundation.layout.fillMaxSize
14+
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.foundation.layout.width
16+
import androidx.compose.material3.Button
17+
import androidx.compose.material3.ExperimentalMaterial3Api
18+
import androidx.compose.material3.Scaffold
19+
import androidx.compose.material3.Switch
20+
import androidx.compose.material3.Text
21+
import androidx.compose.material3.TopAppBar
22+
import androidx.compose.runtime.Composable
23+
import androidx.compose.ui.Alignment
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.tooling.preview.Preview
26+
import androidx.compose.ui.tooling.preview.PreviewLightDark
27+
import androidx.compose.ui.tooling.preview.PreviewParameter
28+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
29+
import androidx.compose.ui.unit.dp
30+
import androidx.lifecycle.lifecycleScope
31+
import kotlinx.coroutines.Dispatchers
32+
import kotlinx.coroutines.flow.collectLatest
33+
import kotlinx.coroutines.launch
34+
import model.database.UserPreferencesRepository
35+
import org.jetbrains.compose.resources.stringResource
36+
import org.koin.android.ext.android.get
37+
import recurringexpensetracker.app.generated.resources.Res
38+
import recurringexpensetracker.app.generated.resources.biometric_prompt_manager_title
39+
import recurringexpensetracker.app.generated.resources.cancel
40+
import recurringexpensetracker.app.generated.resources.dialog_ok
41+
import recurringexpensetracker.app.generated.resources.widget_configuration_biometric
42+
import recurringexpensetracker.app.generated.resources.widget_configuration_biometric_deactivate
43+
import recurringexpensetracker.app.generated.resources.widget_configuration_title
44+
import recurringexpensetracker.app.generated.resources.widget_grid_mode
45+
import security.BiometricPromptManager
46+
import security.BiometricPromptManager.BiometricResult
47+
import ui.theme.ExpenseTrackerTheme
48+
49+
class ConfigureWidgetActivity : AppCompatActivity() {
50+
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
51+
private val userPreferencesRepository = get<UserPreferencesRepository>()
52+
private val biometricPromptManager: BiometricPromptManager by lazy { BiometricPromptManager(this) }
53+
54+
override fun onCreate(savedInstanceState: Bundle?) {
55+
enableEdgeToEdge()
56+
super.onCreate(savedInstanceState)
57+
58+
appWidgetId = intent.extras?.getInt(
59+
AppWidgetManager.EXTRA_APPWIDGET_ID,
60+
AppWidgetManager.INVALID_APPWIDGET_ID,
61+
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
62+
63+
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
64+
setResult(RESULT_CANCELED, resultValue)
65+
66+
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
67+
finish()
68+
return
69+
}
70+
71+
lifecycleScope.launch {
72+
biometricPromptManager.promptResult.collectLatest {
73+
when (it) {
74+
is BiometricResult.AuthenticationSuccess -> {
75+
launch {
76+
userPreferencesRepository.biometricSecurity.save(false)
77+
}
78+
}
79+
else -> {
80+
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
81+
setResult(RESULT_CANCELED, resultValue)
82+
finish()
83+
return@collectLatest
84+
}
85+
}
86+
}
87+
}
88+
89+
setContent {
90+
val biometricPromptTitle = stringResource(Res.string.biometric_prompt_manager_title)
91+
val biometricCancel = stringResource(Res.string.cancel)
92+
ExpenseTrackerTheme {
93+
ConfigurationContent(
94+
isBiometricEnabled = userPreferencesRepository.biometricSecurity.collectAsState().value,
95+
isGridModeEnabled = userPreferencesRepository.gridMode.collectAsState().value,
96+
onShowBiometricPrompt = {
97+
biometricPromptManager.showBiometricPrompt(
98+
biometricPromptTitle,
99+
biometricCancel,
100+
)
101+
},
102+
onGridModeChange = {
103+
lifecycleScope.launch(Dispatchers.IO) {
104+
userPreferencesRepository.gridMode.save(it)
105+
}
106+
},
107+
onConfirmClick = {
108+
val resultValue =
109+
Intent().putExtra(
110+
AppWidgetManager.EXTRA_APPWIDGET_ID,
111+
appWidgetId,
112+
)
113+
setResult(RESULT_OK, resultValue)
114+
finish()
115+
},
116+
)
117+
}
118+
}
119+
}
120+
}
121+
122+
@OptIn(ExperimentalMaterial3Api::class)
123+
@Composable
124+
private fun ConfigurationContent(
125+
isBiometricEnabled: Boolean,
126+
isGridModeEnabled: Boolean,
127+
onShowBiometricPrompt: () -> Unit,
128+
onGridModeChange: (Boolean) -> Unit,
129+
onConfirmClick: () -> Unit,
130+
) {
131+
Scaffold(
132+
modifier = Modifier.fillMaxSize(),
133+
topBar = {
134+
TopAppBar(
135+
title = {
136+
Text(
137+
text = stringResource(Res.string.widget_configuration_title),
138+
)
139+
},
140+
)
141+
},
142+
) { innerPadding ->
143+
Column(
144+
modifier =
145+
Modifier
146+
.fillMaxSize()
147+
.padding(innerPadding),
148+
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
149+
horizontalAlignment = Alignment.CenterHorizontally,
150+
) {
151+
if (isBiometricEnabled) {
152+
BiometricSection(onShowBiometricPrompt)
153+
} else {
154+
GridModeSection(isGridModeEnabled, onGridModeChange)
155+
Button(onClick = onConfirmClick) {
156+
Text(text = stringResource(Res.string.dialog_ok))
157+
}
158+
}
159+
}
160+
}
161+
}
162+
163+
@Composable
164+
private fun BiometricSection(onShowBiometricPrompt: () -> Unit) {
165+
Column(
166+
modifier = Modifier.padding(16.dp),
167+
horizontalAlignment = Alignment.CenterHorizontally,
168+
verticalArrangement = Arrangement.spacedBy(8.dp),
169+
) {
170+
Text(text = stringResource(Res.string.widget_configuration_biometric))
171+
Button(
172+
onClick = onShowBiometricPrompt,
173+
) {
174+
Text(text = stringResource(Res.string.widget_configuration_biometric_deactivate))
175+
}
176+
}
177+
}
178+
179+
@Composable
180+
private fun GridModeSection(
181+
isGridModeEnabled: Boolean,
182+
onGridModeChange: (Boolean) -> Unit,
183+
) {
184+
Row(
185+
verticalAlignment = Alignment.CenterVertically,
186+
horizontalArrangement = Arrangement.Center,
187+
) {
188+
Text(text = stringResource(Res.string.widget_grid_mode))
189+
Spacer(modifier = Modifier.width(8.dp))
190+
Switch(
191+
checked = isGridModeEnabled,
192+
onCheckedChange = onGridModeChange,
193+
)
194+
}
195+
}
196+
197+
private class IsGridModePreviewParameterProvider : PreviewParameterProvider<Boolean> {
198+
override val values = sequenceOf(true, false)
199+
}
200+
201+
@Preview
202+
@Composable
203+
private fun ConfigurationContentPreview(
204+
@PreviewParameter(IsGridModePreviewParameterProvider::class) isGridModeEnabled: Boolean,
205+
) {
206+
ExpenseTrackerTheme {
207+
ConfigurationContent(
208+
isBiometricEnabled = false,
209+
isGridModeEnabled = isGridModeEnabled,
210+
onShowBiometricPrompt = {},
211+
onGridModeChange = {},
212+
onConfirmClick = {},
213+
)
214+
}
215+
}
216+
217+
@PreviewLightDark
218+
@Composable
219+
private fun ConfigurationContentPreview3() {
220+
ExpenseTrackerTheme {
221+
ConfigurationContent(
222+
isBiometricEnabled = true,
223+
isGridModeEnabled = true,
224+
onShowBiometricPrompt = {},
225+
onGridModeChange = {},
226+
onConfirmClick = {},
227+
)
228+
}
229+
}

0 commit comments

Comments
 (0)