Skip to content

Commit 0cf7655

Browse files
committed
Add Widget to see failing projects at a glance
1 parent 1d5d82d commit 0cf7655

File tree

23 files changed

+421
-44
lines changed

23 files changed

+421
-44
lines changed

app/build.gradle.kts

+6-1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ dependencies {
160160

161161
implementation(libs.accompanist)
162162

163+
implementation(libs.androidx.glance.appwidget)
164+
testImplementation(libs.androidx.glance.appwidget.testing)
165+
implementation(libs.androidx.glance.material3)
166+
163167
implementation(libs.androidx.hilt.navigation)
164168
implementation(libs.compose.destinations.core)
165169
ksp(libs.compose.destinations.ksp)
@@ -200,9 +204,10 @@ dependencies {
200204
implementation(libs.permissionFlow.android)
201205

202206
implementation(libs.dagger.hilt.android)
203-
implementation(libs.dagger.hilt.work)
204207
ksp(libs.dagger.hilt.compiler)
205208
ksp(libs.dagger.hilt.android.compiler)
209+
implementation(libs.androidx.hilt.work)
210+
ksp(libs.androidx.hilt.compiler)
206211
androidTestImplementation(libs.dagger.hilt.android.testing)
207212
kspAndroidTest(libs.dagger.hilt.compiler)
208213
kspAndroidTest(libs.dagger.hilt.android.compiler)

app/src/main/AndroidManifest.xml

+23-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
android:roundIcon="@mipmap/ic_launcher"
1414
android:supportsRtl="true"
1515
android:theme="@style/Theme.CCDroidX">
16+
1617
<activity
1718
android:name=".feature.MainActivity"
1819
android:exported="true"
@@ -36,11 +37,31 @@
3637
</intent-filter>
3738
</activity>
3839

39-
<!-- If you want to disable android.startup completely. -->
40+
<receiver
41+
android:name=".feature.widget.DashboardWidgetReceiver"
42+
android:exported="true"
43+
android:label="Dashboard">
44+
<intent-filter>
45+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
46+
</intent-filter>
47+
48+
<meta-data
49+
android:name="android.appwidget.provider"
50+
android:resource="@xml/dashboard_widget_info" />
51+
</receiver>
52+
53+
4054
<provider
4155
android:name="androidx.startup.InitializationProvider"
4256
android:authorities="${applicationId}.androidx-startup"
43-
tools:node="remove" />
57+
android:exported="false"
58+
tools:node="merge">
59+
<!-- If you are using androidx.startup to initialize other components -->
60+
<meta-data
61+
android:name="androidx.work.WorkManagerInitializer"
62+
android:value="androidx.startup"
63+
tools:node="remove" />
64+
</provider>
4465

4566
<meta-data
4667
android:name="firebase_crashlytics_collection_enabled"

app/src/main/java/dev/aungkyawpaing/ccdroidx/CCDroidXApp.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package dev.aungkyawpaing.ccdroidx
22

33
import android.app.Application
4+
import androidx.hilt.work.HiltWorkerFactory
45
import androidx.work.Configuration
56
import com.google.android.material.color.DynamicColors
67
import dagger.hilt.android.HiltAndroidApp
7-
import dev.aungkyawpaing.ccdroidx.work.MyWorkerFactory
88
import dev.shreyaspatil.permissionFlow.PermissionFlow
99
import timber.log.Timber
1010
import javax.inject.Inject
@@ -13,7 +13,7 @@ import javax.inject.Inject
1313
class CCDroidXApp : Application(), Configuration.Provider {
1414

1515
@Inject
16-
lateinit var workerFactory: MyWorkerFactory
16+
lateinit var workerFactory: HiltWorkerFactory
1717

1818
override fun onCreate() {
1919
super.onCreate()

app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/MainActivity.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
8181
if (host == "project" && path.isNotEmpty()) {
8282
lifecycleScope.launch {
8383
val projectId = path.toLongOrNull() ?: return@launch
84-
val url = viewModel.getProjectUrlById(projectId) ?: return@launch
84+
val url = viewModel.getProjectUrlById(projectId)
8585
openInBrowser(this@MainActivity, url)
8686
}
8787
}

app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/sync/SyncProjectWorker.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class SyncProjectWorker @AssistedInject constructor(
1515
@Assisted appContext: Context,
1616
@Assisted workerParams: WorkerParameters,
1717
val syncProjects: SyncProjects,
18-
val notifyProjectStatus: NotifyProjectStatus
18+
val notifyProjectStatus: NotifyProjectStatus,
1919
) : CoroutineWorker(appContext, workerParams) {
2020

2121
companion object {
@@ -33,7 +33,7 @@ class SyncProjectWorker @AssistedInject constructor(
3333
return Result.failure()
3434
}
3535

36-
Timber.i("finished syncing")
36+
Timber.i("finished syncing successfully")
3737
return Result.success()
3838
}
3939

app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/sync/SyncProjects.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package dev.aungkyawpaing.ccdroidx.feature.sync
22

3-
import dev.aungkyawpaing.ccdroidx.data.api.NetworkException
43
import dev.aungkyawpaing.ccdroidx.common.Project
54
import dev.aungkyawpaing.ccdroidx.data.ProjectRepo
5+
import dev.aungkyawpaing.ccdroidx.data.api.NetworkException
66
import dev.aungkyawpaing.ccdroidx.feature.wear.WearDataLayerSource
7+
import dev.aungkyawpaing.ccdroidx.feature.widget.WidgetManager
78
import kotlinx.coroutines.flow.firstOrNull
89
import java.time.Clock
910
import java.time.ZonedDateTime
@@ -13,7 +14,8 @@ class SyncProjects @Inject constructor(
1314
private val projectRepo: ProjectRepo,
1415
private val syncMetaDataStorage: SyncMetaDataStorage,
1516
private val wearDataLayerSource: WearDataLayerSource,
16-
private val clock: Clock
17+
private val clock: Clock,
18+
private val widgetManager: WidgetManager
1719
) {
1820

1921
suspend fun sync(
@@ -44,6 +46,7 @@ class SyncProjects @Inject constructor(
4446
lastSyncedState = LastSyncedState.SUCCESS
4547
)
4648
)
49+
widgetManager.updateDashboardWidget()
4750
wearDataLayerSource.updateDataItems()
4851
} catch (networkException: NetworkException) {
4952
syncMetaDataStorage.saveLastSyncedTime(

app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/sync/SyncWorkerScheduler.kt

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package dev.aungkyawpaing.ccdroidx.feature.sync
22

33
import android.content.Context
4-
import androidx.work.*
4+
import androidx.work.Constraints
5+
import androidx.work.ExistingPeriodicWorkPolicy
6+
import androidx.work.NetworkType
7+
import androidx.work.OneTimeWorkRequestBuilder
8+
import androidx.work.PeriodicWorkRequestBuilder
9+
import androidx.work.WorkManager
510
import java.time.Duration
611

712
class SyncWorkerScheduler(
813
val context: Context
914
) {
1015

1116
fun removeExistingAndScheduleWorker(syncInterval: Duration) {
12-
scheduleWorker(syncInterval)
17+
schedulePeriodicWork(syncInterval)
1318
}
1419

15-
private fun scheduleWorker(duration: Duration) {
20+
private fun schedulePeriodicWork(duration: Duration) {
1621
val constraints = Constraints.Builder()
1722
.setRequiredNetworkType(NetworkType.CONNECTED)
1823
.setRequiresBatteryNotLow(true)
@@ -31,6 +36,20 @@ class SyncWorkerScheduler(
3136
ExistingPeriodicWorkPolicy.UPDATE,
3237
syncWorkRequest
3338
)
39+
}
40+
41+
fun scheduleOneTimeWork() {
42+
val constraints = Constraints.Builder()
43+
.setRequiredNetworkType(NetworkType.CONNECTED)
44+
.build()
3445

46+
val syncWorkRequest =
47+
OneTimeWorkRequestBuilder<SyncProjectWorker>()
48+
.addTag(SyncProjectWorker.TAG)
49+
.setConstraints(constraints)
50+
.build()
51+
52+
WorkManager.getInstance(context)
53+
.enqueue(syncWorkRequest)
3554
}
36-
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package dev.aungkyawpaing.ccdroidx.feature.widget
2+
3+
import android.content.Context
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.collectAsState
6+
import androidx.compose.ui.unit.TextUnit
7+
import androidx.compose.ui.unit.TextUnitType
8+
import androidx.compose.ui.unit.dp
9+
import androidx.glance.ColorFilter
10+
import androidx.glance.GlanceId
11+
import androidx.glance.GlanceModifier
12+
import androidx.glance.GlanceTheme
13+
import androidx.glance.Image
14+
import androidx.glance.ImageProvider
15+
import androidx.glance.LocalContext
16+
import androidx.glance.action.ActionParameters
17+
import androidx.glance.action.actionParametersOf
18+
import androidx.glance.action.actionStartActivity
19+
import androidx.glance.action.clickable
20+
import androidx.glance.appwidget.GlanceAppWidget
21+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
22+
import androidx.glance.appwidget.SizeMode
23+
import androidx.glance.appwidget.action.ActionCallback
24+
import androidx.glance.appwidget.action.actionRunCallback
25+
import androidx.glance.appwidget.cornerRadius
26+
import androidx.glance.appwidget.lazy.LazyColumn
27+
import androidx.glance.appwidget.lazy.items
28+
import androidx.glance.appwidget.provideContent
29+
import androidx.glance.background
30+
import androidx.glance.layout.Alignment
31+
import androidx.glance.layout.Box
32+
import androidx.glance.layout.Column
33+
import androidx.glance.layout.Row
34+
import androidx.glance.layout.fillMaxSize
35+
import androidx.glance.layout.fillMaxWidth
36+
import androidx.glance.layout.height
37+
import androidx.glance.layout.padding
38+
import androidx.glance.layout.size
39+
import androidx.glance.text.FontWeight
40+
import androidx.glance.text.Text
41+
import androidx.glance.text.TextStyle
42+
import dagger.hilt.android.AndroidEntryPoint
43+
import dev.aungkyawpaing.ccdroidx.R
44+
import dev.aungkyawpaing.ccdroidx.common.BuildStatus
45+
import dev.aungkyawpaing.ccdroidx.common.Project
46+
import dev.aungkyawpaing.ccdroidx.data.ProjectRepo
47+
import dev.aungkyawpaing.ccdroidx.feature.MainActivity
48+
import dev.aungkyawpaing.ccdroidx.feature.sync.SyncWorkerScheduler
49+
import javax.inject.Inject
50+
51+
class DashboardWidget(
52+
private val projectRepo: ProjectRepo
53+
) : GlanceAppWidget() {
54+
55+
override val sizeMode = SizeMode.Exact
56+
override suspend fun provideGlance(context: Context, id: GlanceId) {
57+
58+
provideContent {
59+
val failingProjects =
60+
projectRepo.getAllNotBuildStatus(BuildStatus.SUCCESS).collectAsState(initial = emptyList())
61+
62+
DashboardWidgetContent(failingProjects.value)
63+
}
64+
}
65+
}
66+
67+
@Composable
68+
private fun DashboardWidgetContent(failingProjects: List<Project>) {
69+
val context = LocalContext.current
70+
71+
GlanceTheme {
72+
Column(
73+
modifier = GlanceModifier
74+
.fillMaxSize()
75+
.background(GlanceTheme.colors.background)
76+
) {
77+
78+
Row(
79+
modifier = GlanceModifier
80+
.fillMaxWidth()
81+
.background(GlanceTheme.colors.primary)
82+
.clickable(onClick = actionStartActivity(MainActivity::class.java)),
83+
verticalAlignment = Alignment.CenterVertically
84+
) {
85+
val title = if (failingProjects.isEmpty()) {
86+
context.getString(R.string.dashboard_widget_title_green)
87+
} else {
88+
context.getString(R.string.dashboard_widget_title_red, failingProjects.size.toString())
89+
}
90+
val titleStyle = TextStyle(
91+
color = GlanceTheme.colors.onPrimary,
92+
fontSize = TextUnit(16.0f, TextUnitType.Sp),
93+
fontWeight = FontWeight.Medium,
94+
)
95+
Image(
96+
provider = ImageProvider(R.drawable.ic_refresh_24),
97+
contentDescription = context.getString(R.string.menu_item_sync_project_status),
98+
colorFilter = ColorFilter.tint(GlanceTheme.colors.onPrimary),
99+
modifier = GlanceModifier.defaultWeight().size(48.dp).padding(12.dp)
100+
.clickable(onClick = actionRunCallback<WidgetRefreshAction>())
101+
)
102+
103+
Text(
104+
text = title,
105+
style = titleStyle,
106+
modifier = GlanceModifier.fillMaxWidth()
107+
)
108+
}
109+
110+
LazyColumn(modifier = GlanceModifier.padding(8.dp)) {
111+
items(failingProjects) { project ->
112+
Column {
113+
Box(
114+
modifier = GlanceModifier.cornerRadius(8.dp)
115+
.background(R.color.build_fail)
116+
.clickable(
117+
onClick = actionStartActivity(
118+
MainActivity::class.java,
119+
actionParametersOf(
120+
ActionParameters.Key<String>(MainActivity.INTENT_EXTRA_URL) to project.webUrl
121+
)
122+
)
123+
)
124+
) {
125+
Text(
126+
text = project.name,
127+
style = TextStyle(
128+
color = GlanceTheme.colors.onError,
129+
fontSize = TextUnit(12.0f, TextUnitType.Sp),
130+
fontWeight = FontWeight.Normal
131+
),
132+
maxLines = 1,
133+
modifier = GlanceModifier.fillMaxWidth().padding(8.dp)
134+
)
135+
}
136+
137+
Box(modifier = GlanceModifier.height(8.dp)) {}
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}
144+
145+
class WidgetRefreshAction : ActionCallback {
146+
override suspend fun onAction(
147+
context: Context,
148+
glanceId: GlanceId,
149+
parameters: ActionParameters
150+
) {
151+
SyncWorkerScheduler(context).scheduleOneTimeWork()
152+
}
153+
}
154+
155+
@AndroidEntryPoint
156+
class DashboardWidgetReceiver : GlanceAppWidgetReceiver() {
157+
158+
@Inject
159+
lateinit var projectRepo: ProjectRepo
160+
161+
override val glanceAppWidget: GlanceAppWidget
162+
get() = DashboardWidget(projectRepo)
163+
}

0 commit comments

Comments
 (0)