Skip to content

Commit

Permalink
Add Widget to see failing projects at a glance
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-paing committed Mar 24, 2024
1 parent 1d5d82d commit 0cf7655
Show file tree
Hide file tree
Showing 23 changed files with 421 additions and 44 deletions.
7 changes: 6 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ dependencies {

implementation(libs.accompanist)

implementation(libs.androidx.glance.appwidget)
testImplementation(libs.androidx.glance.appwidget.testing)
implementation(libs.androidx.glance.material3)

implementation(libs.androidx.hilt.navigation)
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.ksp)
Expand Down Expand Up @@ -200,9 +204,10 @@ dependencies {
implementation(libs.permissionFlow.android)

implementation(libs.dagger.hilt.android)
implementation(libs.dagger.hilt.work)
ksp(libs.dagger.hilt.compiler)
ksp(libs.dagger.hilt.android.compiler)
implementation(libs.androidx.hilt.work)
ksp(libs.androidx.hilt.compiler)
androidTestImplementation(libs.dagger.hilt.android.testing)
kspAndroidTest(libs.dagger.hilt.compiler)
kspAndroidTest(libs.dagger.hilt.android.compiler)
Expand Down
25 changes: 23 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.CCDroidX">

<activity
android:name=".feature.MainActivity"
android:exported="true"
Expand All @@ -36,11 +37,31 @@
</intent-filter>
</activity>

<!-- If you want to disable android.startup completely. -->
<receiver
android:name=".feature.widget.DashboardWidgetReceiver"
android:exported="true"
android:label="Dashboard">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/dashboard_widget_info" />
</receiver>


<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>

<meta-data
android:name="firebase_crashlytics_collection_enabled"
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/dev/aungkyawpaing/ccdroidx/CCDroidXApp.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package dev.aungkyawpaing.ccdroidx

import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp
import dev.aungkyawpaing.ccdroidx.work.MyWorkerFactory
import dev.shreyaspatil.permissionFlow.PermissionFlow
import timber.log.Timber
import javax.inject.Inject
Expand All @@ -13,7 +13,7 @@ import javax.inject.Inject
class CCDroidXApp : Application(), Configuration.Provider {

@Inject
lateinit var workerFactory: MyWorkerFactory
lateinit var workerFactory: HiltWorkerFactory

override fun onCreate() {
super.onCreate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
if (host == "project" && path.isNotEmpty()) {
lifecycleScope.launch {
val projectId = path.toLongOrNull() ?: return@launch
val url = viewModel.getProjectUrlById(projectId) ?: return@launch
val url = viewModel.getProjectUrlById(projectId)
openInBrowser(this@MainActivity, url)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SyncProjectWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
val syncProjects: SyncProjects,
val notifyProjectStatus: NotifyProjectStatus
val notifyProjectStatus: NotifyProjectStatus,
) : CoroutineWorker(appContext, workerParams) {

companion object {
Expand All @@ -33,7 +33,7 @@ class SyncProjectWorker @AssistedInject constructor(
return Result.failure()
}

Timber.i("finished syncing")
Timber.i("finished syncing successfully")
return Result.success()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package dev.aungkyawpaing.ccdroidx.feature.sync

import dev.aungkyawpaing.ccdroidx.data.api.NetworkException
import dev.aungkyawpaing.ccdroidx.common.Project
import dev.aungkyawpaing.ccdroidx.data.ProjectRepo
import dev.aungkyawpaing.ccdroidx.data.api.NetworkException
import dev.aungkyawpaing.ccdroidx.feature.wear.WearDataLayerSource
import dev.aungkyawpaing.ccdroidx.feature.widget.WidgetManager
import kotlinx.coroutines.flow.firstOrNull
import java.time.Clock
import java.time.ZonedDateTime
Expand All @@ -13,7 +14,8 @@ class SyncProjects @Inject constructor(
private val projectRepo: ProjectRepo,
private val syncMetaDataStorage: SyncMetaDataStorage,
private val wearDataLayerSource: WearDataLayerSource,
private val clock: Clock
private val clock: Clock,
private val widgetManager: WidgetManager
) {

suspend fun sync(
Expand Down Expand Up @@ -44,6 +46,7 @@ class SyncProjects @Inject constructor(
lastSyncedState = LastSyncedState.SUCCESS
)
)
widgetManager.updateDashboardWidget()
wearDataLayerSource.updateDataItems()
} catch (networkException: NetworkException) {
syncMetaDataStorage.saveLastSyncedTime(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package dev.aungkyawpaing.ccdroidx.feature.sync

import android.content.Context
import androidx.work.*
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.time.Duration

class SyncWorkerScheduler(
val context: Context
) {

fun removeExistingAndScheduleWorker(syncInterval: Duration) {
scheduleWorker(syncInterval)
schedulePeriodicWork(syncInterval)
}

private fun scheduleWorker(duration: Duration) {
private fun schedulePeriodicWork(duration: Duration) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
Expand All @@ -31,6 +36,20 @@ class SyncWorkerScheduler(
ExistingPeriodicWorkPolicy.UPDATE,
syncWorkRequest
)
}

fun scheduleOneTimeWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val syncWorkRequest =
OneTimeWorkRequestBuilder<SyncProjectWorker>()
.addTag(SyncProjectWorker.TAG)
.setConstraints(constraints)
.build()

WorkManager.getInstance(context)
.enqueue(syncWorkRequest)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package dev.aungkyawpaing.ccdroidx.feature.widget

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.glance.ColorFilter
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.items
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.size
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import dagger.hilt.android.AndroidEntryPoint
import dev.aungkyawpaing.ccdroidx.R
import dev.aungkyawpaing.ccdroidx.common.BuildStatus
import dev.aungkyawpaing.ccdroidx.common.Project
import dev.aungkyawpaing.ccdroidx.data.ProjectRepo
import dev.aungkyawpaing.ccdroidx.feature.MainActivity
import dev.aungkyawpaing.ccdroidx.feature.sync.SyncWorkerScheduler
import javax.inject.Inject

class DashboardWidget(
private val projectRepo: ProjectRepo
) : GlanceAppWidget() {

override val sizeMode = SizeMode.Exact
override suspend fun provideGlance(context: Context, id: GlanceId) {

provideContent {
val failingProjects =
projectRepo.getAllNotBuildStatus(BuildStatus.SUCCESS).collectAsState(initial = emptyList())

DashboardWidgetContent(failingProjects.value)
}
}
}

@Composable
private fun DashboardWidgetContent(failingProjects: List<Project>) {
val context = LocalContext.current

GlanceTheme {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(GlanceTheme.colors.background)
) {

Row(
modifier = GlanceModifier
.fillMaxWidth()
.background(GlanceTheme.colors.primary)
.clickable(onClick = actionStartActivity(MainActivity::class.java)),
verticalAlignment = Alignment.CenterVertically
) {
val title = if (failingProjects.isEmpty()) {
context.getString(R.string.dashboard_widget_title_green)
} else {
context.getString(R.string.dashboard_widget_title_red, failingProjects.size.toString())
}
val titleStyle = TextStyle(
color = GlanceTheme.colors.onPrimary,
fontSize = TextUnit(16.0f, TextUnitType.Sp),
fontWeight = FontWeight.Medium,
)
Image(
provider = ImageProvider(R.drawable.ic_refresh_24),
contentDescription = context.getString(R.string.menu_item_sync_project_status),
colorFilter = ColorFilter.tint(GlanceTheme.colors.onPrimary),
modifier = GlanceModifier.defaultWeight().size(48.dp).padding(12.dp)
.clickable(onClick = actionRunCallback<WidgetRefreshAction>())
)

Text(
text = title,
style = titleStyle,
modifier = GlanceModifier.fillMaxWidth()
)
}

LazyColumn(modifier = GlanceModifier.padding(8.dp)) {
items(failingProjects) { project ->
Column {
Box(
modifier = GlanceModifier.cornerRadius(8.dp)
.background(R.color.build_fail)
.clickable(
onClick = actionStartActivity(
MainActivity::class.java,
actionParametersOf(
ActionParameters.Key<String>(MainActivity.INTENT_EXTRA_URL) to project.webUrl
)
)
)
) {
Text(
text = project.name,
style = TextStyle(
color = GlanceTheme.colors.onError,
fontSize = TextUnit(12.0f, TextUnitType.Sp),
fontWeight = FontWeight.Normal
),
maxLines = 1,
modifier = GlanceModifier.fillMaxWidth().padding(8.dp)
)
}

Box(modifier = GlanceModifier.height(8.dp)) {}
}
}
}
}
}
}

class WidgetRefreshAction : ActionCallback {
override suspend fun onAction(
context: Context,
glanceId: GlanceId,
parameters: ActionParameters
) {
SyncWorkerScheduler(context).scheduleOneTimeWork()
}
}

@AndroidEntryPoint
class DashboardWidgetReceiver : GlanceAppWidgetReceiver() {

@Inject
lateinit var projectRepo: ProjectRepo

override val glanceAppWidget: GlanceAppWidget
get() = DashboardWidget(projectRepo)
}
Loading

0 comments on commit 0cf7655

Please sign in to comment.