Skip to content

Commit

Permalink
feat(widget): impl widget update procedure.
Browse files Browse the repository at this point in the history
  • Loading branch information
I-Info committed Oct 24, 2023
1 parent e78ae01 commit 085d866
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 53 deletions.
6 changes: 5 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ android {
buildFeatures {
compose true
viewBinding true
buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.3'
Expand All @@ -64,6 +65,7 @@ android {

dependencies {
// Androidx Core & UI
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-animation:1.0.0-rc01'
implementation 'androidx.activity:activity-compose:1.8.0'
Expand Down Expand Up @@ -91,9 +93,11 @@ dependencies {

implementation "com.squareup.moshi:moshi:1.15.0"

// Work
implementation "androidx.work:work-runtime-ktx:$work_version"

// Dependency injection (Hilt)
implementation "com.google.dagger:hilt-android:$hilt_version"
implementation 'androidx.appcompat:appcompat:1.6.1'
kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ class HomeViewModel @Inject constructor(
if (state is LoadResult.Ready && state.data != null) {
val day = state.data
if (day.isInTerm) {
courseRepository.getCourses(day.year, day.term)
.map { it.filterToday(day) }
courseRepository.getCourses(day.year, day.term, day.week, day.dayOfWeek)
} else flowOf(emptyList())
} else flowOf(emptyList())
}
Expand Down Expand Up @@ -123,11 +122,6 @@ class HomeViewModel @Inject constructor(
}
}

private fun List<Course>.filterToday(day: TermDayState): List<Course> =
this.filter {
(day.dayOfWeek == it.dayOfWeek) && (day.week in it.weeks)
}

/**
* Sync with upstream
*/
Expand Down
78 changes: 39 additions & 39 deletions app/src/main/kotlin/com/zjutjh/ijh/widget/ScheduleWidget.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.zjutjh.ijh.widget

import android.content.Context
import android.util.Log
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.glance.Button
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
Expand All @@ -17,81 +17,81 @@ import androidx.glance.appwidget.provideContent
import androidx.glance.background
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.padding
import androidx.glance.text.Text
import com.zjutjh.ijh.data.repository.CourseRepository
import com.zjutjh.ijh.data.repository.WeJhInfoRepository
import com.zjutjh.ijh.model.Course
import com.zjutjh.ijh.ui.component.shortTime
import com.zjutjh.ijh.ui.model.toTermDayState
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import com.zjutjh.ijh.work.ScheduleWidgetUpdateWorker
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import java.time.ZonedDateTime

// TODO: Unfinished yet
// TODO: Unfinished yet, in early stages
class ScheduleWidget : GlanceAppWidget() {

@EntryPoint
@InstallIn(SingletonComponent::class)
interface ScheduleWidgetEntryPoint {
val courseRepository: CourseRepository
val weJhInfoRepository: WeJhInfoRepository
}

override suspend fun provideGlance(context: Context, id: GlanceId) {
val hiltEntryPoint = EntryPointAccessors.fromApplication<ScheduleWidgetEntryPoint>(context)
val entryPoint =
EntryPointAccessors.fromApplication<ScheduleWidgetReceiver.Repositories>(context)

val info = hiltEntryPoint.weJhInfoRepository.infoStream
val info = entryPoint.weJhInfoRepository.infoStream
.map { it?.toTermDayState() }
.first()
val courses = if (info != null) {
hiltEntryPoint.courseRepository
.getCourses(info.year, info.term)
.map { courses ->
courses.filter {
it.weeks.contains(info.week) && it.dayOfWeek == info.dayOfWeek
}
}
entryPoint.courseRepository
.getCourses(info.year, info.term, info.week, info.dayOfWeek)
} else flow { emit(emptyList()) }

Log.d("ScheduleWidget", "provideGlance: $info")
val syncTime = entryPoint.courseRepository.lastSyncTimeStream

provideContent {
Log.d("ScheduleWidget", "provideGlance: $info")
Content(courses)
Content(courses, syncTime) {
ScheduleWidgetUpdateWorker.enqueue(context)
}
}
}

@Composable
fun Content(coursesFlow: Flow<List<Course>>) {
fun Content(
coursesFlow: Flow<List<Course>>,
syncTimeFlow: Flow<ZonedDateTime?>,
onUpdate: () -> Unit
) {
val courses by coursesFlow.collectAsState(null)
val syncTime by syncTimeFlow.collectAsState(null)

GlanceTheme {
Box(
modifier = GlanceModifier
.fillMaxSize()
.padding(10.dp)
.background(MaterialTheme.colorScheme.background)
.background(MaterialTheme.colorScheme.surface)
.appWidgetBackground()
) {
if (courses != null) {
if (courses!!.isEmpty()) {
Text("No course.")
} else {
Column(
modifier = GlanceModifier
.cornerRadius(10.dp)
.background(MaterialTheme.colorScheme.secondaryContainer)
) {
courses!!.forEach {
CourseItem(course = it)
Column {
if (syncTime != null) {
Row {
Button(text = "Update", onClick = onUpdate)
Text("Last sync: ${syncTime!!.toLocalTime()}")
}
}
if (courses!!.isEmpty()) {
Text("No course.")
} else {
Column(
modifier = GlanceModifier
.cornerRadius(10.dp)
.background(MaterialTheme.colorScheme.secondaryContainer)
) {
courses!!.forEach {
CourseItem(course = it)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,53 @@ package com.zjutjh.ijh.widget

import android.content.Context
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.zjutjh.ijh.data.repository.CourseRepository
import com.zjutjh.ijh.data.repository.WeJhInfoRepository
import com.zjutjh.ijh.work.ScheduleWidgetUpdateWorker
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Duration

/**
* Implementation of App Widget functionality.
*/
class ScheduleWidgetReceiver : GlanceAppWidgetReceiver() {

@EntryPoint
@InstallIn(SingletonComponent::class)
interface Repositories {
val courseRepository: CourseRepository
val weJhInfoRepository: WeJhInfoRepository
}

override val glanceAppWidget = ScheduleWidget()

override fun onEnabled(context: Context) {
super.onEnabled(context)
// Enter relevant functionality for when the first widget is created
val manager = WorkManager.getInstance(context)
val request = PeriodicWorkRequestBuilder<ScheduleWidgetUpdateWorker>(
Duration.ofHours(1), Duration.ofMinutes(15)
).setConstraints(
Constraints(requiresBatteryNotLow = true, requiresDeviceIdle = true)
).build()

manager.enqueueUniquePeriodicWork(
ScheduleWidgetUpdateWorker.PERIODIC_UNIQUE_NAME,
ExistingPeriodicWorkPolicy.KEEP,
request
)
}

override fun onDisabled(context: Context) {
super.onEnabled(context)
// Enter relevant functionality for when the last widget is disabled
val manager = WorkManager.getInstance(context)
manager.cancelUniqueWork(ScheduleWidgetUpdateWorker.PERIODIC_UNIQUE_NAME)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.zjutjh.ijh.work

import android.content.Context
import androidx.glance.appwidget.updateAll
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkerParameters
import com.zjutjh.ijh.exception.UnauthorizedException
import com.zjutjh.ijh.widget.ScheduleWidget
import com.zjutjh.ijh.widget.ScheduleWidgetReceiver
import dagger.hilt.android.EntryPointAccessors

class ScheduleWidgetUpdateWorker(
private val context: Context,
workerParameters: WorkerParameters
) :
CoroutineWorker(context, workerParameters) {
companion object {
// Periodic work and one time work should have different unique names.
const val UNIQUE_NAME = "ScheduleWidgetUpdater"
const val PERIODIC_UNIQUE_NAME = "ScheduleWidgetPeriodicUpdater"

fun enqueue(context: Context, force: Boolean = false) {
val manager = androidx.work.WorkManager.getInstance(context)
val request = androidx.work.OneTimeWorkRequestBuilder<ScheduleWidgetUpdateWorker>()
.build()

var policy = ExistingWorkPolicy.KEEP

if (force) {
policy = ExistingWorkPolicy.REPLACE
}

manager.enqueueUniqueWork(
UNIQUE_NAME,
policy,
request
)
}
}

private val entryPoint =
EntryPointAccessors.fromApplication<ScheduleWidgetReceiver.Repositories>(context)

override suspend fun doWork(): Result {
return try {
val info = entryPoint.weJhInfoRepository.sync()
entryPoint.courseRepository.sync(info.first, info.second)

// Notify all existing widgets to update
ScheduleWidget().updateAll(context)

Result.success()
} catch (e: UnauthorizedException) {
// Not logged in, stop the worker
Result.failure()
} catch (_: Exception) {
Result.retry()
}
}
}
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ buildscript {
lifecycle_version = '2.6.2'
compose_version = '1.5.4'
hilt_version = '2.48.1'
retrofit_version = "2.9.0"
protobuf_version = '3.24.4'
retrofit_version = '2.9.0'
protobuf_version = '3.24.0'
room_version = '2.6.0'
work_version = '2.8.1'
}
} // Top-level build file where you can add configuration options common to all sub-projects/modules.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.zjutjh.ijh.data.repository
import com.zjutjh.ijh.model.Course
import com.zjutjh.ijh.model.Term
import kotlinx.coroutines.flow.Flow
import java.time.DayOfWeek
import java.time.ZonedDateTime

/**
Expand All @@ -11,7 +12,15 @@ import java.time.ZonedDateTime
interface CourseRepository {
val lastSyncTimeStream: Flow<ZonedDateTime?>

/**
* Get courses of [year] and [term]
*/
fun getCourses(year: Int, term: Term): Flow<List<Course>>

/**
* Get courses of [year] and [term] in [week] and [dayOfWeek]
*/
fun getCourses(year: Int, term: Term, week: Int, dayOfWeek: DayOfWeek): Flow<List<Course>>

suspend fun sync(year: Int, term: Term)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.zjutjh.ijh.network.ZfDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.time.DayOfWeek
import java.time.ZonedDateTime
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -29,6 +30,17 @@ class CourseRepositoryImpl @Inject constructor(
override fun getCourses(year: Int, term: Term): Flow<List<Course>> =
dao.getCourses(year, term).map { it.map(CourseEntity::asExternalModel) }

override fun getCourses(
year: Int,
term: Term,
week: Int,
dayOfWeek: DayOfWeek
): Flow<List<Course>> =
dao.getCourses(year, term).map { entities ->
entities.map(CourseEntity::asExternalModel)
.filter { it.dayOfWeek == dayOfWeek && week in it.weeks }
}

override val lastSyncTimeStream: Flow<ZonedDateTime?> =
localPreference.data.map {
if (it.hasCoursesLastSyncTime())
Expand All @@ -54,7 +66,9 @@ class CourseRepositoryImpl @Inject constructor(
localPreference.setCoursesLastSyncTime(ZonedDateTime.now())
}

// Insert new or updated elements and delete outdated elements
/**
* Insert new or updated elements and delete outdated elements
*/
private suspend fun updateCourses(old: List<CourseEntity>, new: List<CourseEntity>) {
val toDelete = old.toMutableList()
val toInsert = new.toMutableList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@ import kotlinx.coroutines.flow.Flow
interface WeJhInfoRepository {
val infoStream: Flow<WeJhInfo?>

/**
* Sync WeJhInfo from network to local
* @return Pair(year, term)
*/
suspend fun sync(): Pair<Int, Term>
}
Loading

0 comments on commit 085d866

Please sign in to comment.