Skip to content

Commit 2631d7b

Browse files
author
alllexey
committed
Improve widget's lesson list data retrieval
1 parent cb34e08 commit 2631d7b

File tree

9 files changed

+172
-33
lines changed

9 files changed

+172
-33
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ android {
4141
dependencies {
4242
implementation("io.nayuki:qrcodegen:1.8.0")
4343
implementation("io.github.alllexey123:my-itmo-api:1.2.3")
44+
implementation("androidx.datastore:datastore-preferences:1.1.1")
4445
implementation(libs.androidx.core.ktx)
4546
implementation(libs.androidx.appcompat)
4647
implementation(libs.material)

app/src/main/java/me/alllexey123/itmowidgets/AppContainer.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import me.alllexey123.itmowidgets.data.remote.ScheduleRemoteDataSourceImpl
1111
import me.alllexey123.itmowidgets.data.repository.QrCodeRepository
1212
import me.alllexey123.itmowidgets.data.repository.ScheduleRepository
1313
import me.alllexey123.itmowidgets.data.PreferencesStorage
14+
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListRepository
1415
import me.alllexey123.itmowidgets.util.QrBitmapRenderer
1516
import me.alllexey123.itmowidgets.util.QrCodeGenerator
1617
import java.io.File
@@ -31,6 +32,8 @@ class AppContainer(context: Context) {
3132
}
3233
}
3334

35+
val lessonListRepository by lazy { LessonListRepository(context) }
36+
3437
val scheduleRepository: ScheduleRepository by lazy {
3538
ScheduleRepository(
3639
localDataSource = ScheduleLocalDataSourceImpl(

app/src/main/java/me/alllexey123/itmowidgets/ui/settings/SettingsFragment.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
8282
listOf("single_lesson_widget_style", "list_lesson_widget_style")
8383
.map { s -> findPreference<ListPreference>(s) }.forEach { preference ->
8484
preference?.setOnPreferenceChangeListener { pref, newValue ->
85-
storage.setLessonWidgetStyleChanged(true)
8685
updateAllWidgets()
8786
true
8887
}

app/src/main/java/me/alllexey123/itmowidgets/ui/widgets/LessonListWidget.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import androidx.core.net.toUri
99
import me.alllexey123.itmowidgets.ItmoWidgetsApp
1010
import me.alllexey123.itmowidgets.R
1111
import me.alllexey123.itmowidgets.ui.schedule.ScheduleActivity
12-
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListDataHolder
13-
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListWidgetData
1412
import me.alllexey123.itmowidgets.workers.LessonWidgetUpdateWorker
1513

1614

@@ -33,12 +31,9 @@ class LessonListWidget : AppWidgetProvider() {
3331
context: Context,
3432
appWidgetManager: AppWidgetManager,
3533
appWidgetId: Int,
36-
widgetData: LessonListWidgetData,
3734
layoutId: Int,
3835
onlyDataChanged: Boolean
3936
) {
40-
LessonListDataHolder.setData(widgetData)
41-
4237
if (!onlyDataChanged){
4338
val intent = Intent(context, LessonListWidgetService::class.java).apply {
4439
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
@@ -52,8 +47,9 @@ class LessonListWidget : AppWidgetProvider() {
5247
views.setRemoteAdapter(R.id.lesson_list, intent)
5348

5449
appWidgetManager.updateAppWidget(appWidgetId, views)
50+
} else {
51+
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.lesson_list)
5552
}
56-
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.lesson_list)
5753
}
5854

5955

app/src/main/java/me/alllexey123/itmowidgets/ui/widgets/LessonListWidgetService.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import android.view.View
66
import android.widget.RemoteViews
77
import android.widget.RemoteViewsService
88
import androidx.core.content.ContextCompat
9+
import kotlinx.coroutines.flow.first
10+
import kotlinx.coroutines.runBlocking
11+
import me.alllexey123.itmowidgets.ItmoWidgetsApp
912
import me.alllexey123.itmowidgets.R
10-
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListDataHolder
1113
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListWidgetData
1214
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListWidgetEntry
1315
import me.alllexey123.itmowidgets.util.ScheduleUtils
@@ -20,23 +22,31 @@ class LessonListWidgetService : RemoteViewsService() {
2022

2123
class ViewsFactory(private val context: Context) : RemoteViewsFactory {
2224

23-
private var data: LessonListWidgetData = LessonListDataHolder.getData()
25+
private val lessonListRepository =
26+
(context.applicationContext as ItmoWidgetsApp).appContainer.lessonListRepository
27+
28+
private var data: LessonListWidgetData = LessonListWidgetData(listOf())
2429

2530
override fun onCreate() {
26-
loadData()
2731
}
2832

2933
override fun onDataSetChanged() {
3034
loadData()
3135
}
3236

3337
private fun loadData() {
34-
data = LessonListDataHolder.getData()
38+
runBlocking {
39+
data = lessonListRepository.data.first()
40+
}
3541
}
3642

3743
override fun getCount(): Int = data.entries.size
3844

3945
override fun getViewAt(position: Int): RemoteViews? {
46+
if (position >= data.entries.size) {
47+
return null
48+
}
49+
4050
val entry = data.entries[position]
4151

4252
val rv = RemoteViews(context.packageName, entry.layoutId)
@@ -71,7 +81,7 @@ class LessonListWidgetService : RemoteViewsService() {
7181
}
7282

7383
override fun getLoadingView(): RemoteViews? = null
74-
override fun getViewTypeCount(): Int = 3
84+
override fun getViewTypeCount(): Int = 9
7585
override fun hasStableIds(): Boolean = true
7686
override fun getItemId(position: Int): Long = position.toLong()
7787
override fun onDestroy() {}
Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
11
package me.alllexey123.itmowidgets.ui.widgets.data
22

3-
object LessonListDataHolder {
4-
@Volatile
5-
private var data: LessonListWidgetData = LessonListWidgetData(
6-
listOf(LessonListWidgetEntry.Updating)
7-
)
8-
9-
fun setData(newData: LessonListWidgetData) {
10-
data = newData
3+
import android.content.Context
4+
import androidx.datastore.preferences.core.edit
5+
import androidx.datastore.preferences.core.stringPreferencesKey
6+
import androidx.datastore.preferences.preferencesDataStore
7+
import com.google.gson.Gson
8+
import com.google.gson.GsonBuilder
9+
import kotlinx.coroutines.flow.Flow
10+
import kotlinx.coroutines.flow.map
11+
import me.alllexey123.itmowidgets.util.RuntimeTypeAdapterFactory
12+
13+
val Context.lessonListDataStore by preferencesDataStore(name = "lesson_list_data")
14+
15+
class LessonListRepository(private val context: Context) {
16+
private val gson: Gson by lazy {
17+
val lessonListEntryAdapterFactory = RuntimeTypeAdapterFactory.of(LessonListWidgetEntry::class.java)
18+
.registerSubtype(LessonListWidgetEntry.Error::class.java, "error")
19+
.registerSubtype(LessonListWidgetEntry.FullDayEmpty::class.java, "full_day_empty")
20+
.registerSubtype(LessonListWidgetEntry.NoMoreLessons::class.java, "no_more_lessons")
21+
.registerSubtype(LessonListWidgetEntry.LessonListEnd::class.java, "lesson_list_end")
22+
.registerSubtype(LessonListWidgetEntry.LessonData::class.java, "lesson_data")
23+
GsonBuilder()
24+
.registerTypeAdapterFactory(lessonListEntryAdapterFactory)
25+
.create()
1126
}
27+
private val KEY = stringPreferencesKey("lesson_list_json")
1228

13-
fun getData(): LessonListWidgetData = data
29+
val data: Flow<LessonListWidgetData> =
30+
context.lessonListDataStore.data.map { prefs ->
31+
prefs[KEY]?.let { gson.fromJson(it, LessonListWidgetData::class.java) }
32+
?: LessonListWidgetData(listOf(LessonListWidgetEntry.Updating))
33+
}
34+
35+
suspend fun setData(newData: LessonListWidgetData) {
36+
context.lessonListDataStore.edit { prefs ->
37+
prefs[KEY] = gson.toJson(newData)
38+
}
39+
}
1440
}

app/src/main/java/me/alllexey123/itmowidgets/ui/widgets/data/LessonListWidgetEntry.kt

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,24 @@ package me.alllexey123.itmowidgets.ui.widgets.data
22

33
import me.alllexey123.itmowidgets.R
44

5-
sealed class LessonListWidgetEntry(open val layoutId: Int) {
6-
object Error : LessonListWidgetEntry(R.layout.item_lesson_list_error)
7-
object FullDayEmpty : LessonListWidgetEntry(R.layout.item_lesson_list_empty)
8-
object NoMoreLessons : LessonListWidgetEntry(R.layout.item_lesson_list_no_more)
9-
object LessonListEnd : LessonListWidgetEntry(R.layout.item_lesson_list_end)
10-
object Updating : LessonListWidgetEntry(R.layout.item_lesson_list_updating)
5+
sealed class LessonListWidgetEntry {
6+
abstract val layoutId: Int
7+
8+
object Error : LessonListWidgetEntry() {
9+
override val layoutId: Int = R.layout.item_lesson_list_error
10+
}
11+
object FullDayEmpty : LessonListWidgetEntry() {
12+
override val layoutId: Int = R.layout.item_lesson_list_empty
13+
}
14+
object NoMoreLessons : LessonListWidgetEntry() {
15+
override val layoutId: Int = R.layout.item_lesson_list_no_more
16+
}
17+
object LessonListEnd : LessonListWidgetEntry() {
18+
override val layoutId: Int = R.layout.item_lesson_list_end
19+
}
20+
object Updating : LessonListWidgetEntry() {
21+
override val layoutId: Int = R.layout.item_lesson_list_updating
22+
}
1123

1224
// null to hide
1325
data class LessonData(
@@ -18,5 +30,5 @@ sealed class LessonListWidgetEntry(open val layoutId: Int) {
1830
val room: String? = null,
1931
val building: String? = null,
2032
override val layoutId: Int,
21-
) : LessonListWidgetEntry(layoutId)
33+
) : LessonListWidgetEntry()
2234
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package me.alllexey123.itmowidgets.util
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.JsonElement
5+
import com.google.gson.JsonObject
6+
import com.google.gson.JsonParseException
7+
import com.google.gson.JsonPrimitive
8+
import com.google.gson.TypeAdapter
9+
import com.google.gson.TypeAdapterFactory
10+
import com.google.gson.reflect.TypeToken
11+
import com.google.gson.stream.JsonReader
12+
import com.google.gson.stream.JsonWriter
13+
import kotlin.collections.iterator
14+
15+
class RuntimeTypeAdapterFactory<T> private constructor(
16+
private val baseType: Class<*>,
17+
private val typeFieldName: String,
18+
private val maintainType: Boolean
19+
) : TypeAdapterFactory {
20+
private val labelToSubtype: MutableMap<String, Class<*>> = LinkedHashMap()
21+
private val subtypeToLabel: MutableMap<Class<*>, String> = LinkedHashMap()
22+
23+
companion object {
24+
fun <T> of(baseType: Class<T>, typeFieldName: String = "type", maintainType: Boolean = false): RuntimeTypeAdapterFactory<T> {
25+
return RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType)
26+
}
27+
}
28+
29+
fun registerSubtype(subtype: Class<out T>, label: String): RuntimeTypeAdapterFactory<T> {
30+
if (subtypeToLabel.containsKey(subtype) || labelToSubtype.containsKey(label)) {
31+
throw IllegalArgumentException("types and labels must be unique")
32+
}
33+
labelToSubtype[label] = subtype
34+
subtypeToLabel[subtype] = label
35+
return this
36+
}
37+
38+
override fun <R> create(gson: Gson, type: TypeToken<R>): TypeAdapter<R>? {
39+
if (type.rawType != baseType) {
40+
return null
41+
}
42+
43+
val labelToDelegate: MutableMap<String, TypeAdapter<*>> = LinkedHashMap()
44+
val subtypeToDelegate: MutableMap<Class<*>, TypeAdapter<*>> = LinkedHashMap()
45+
for ((key, value) in labelToSubtype) {
46+
val delegate = gson.getDelegateAdapter(this, TypeToken.get(value))
47+
labelToDelegate[key] = delegate
48+
subtypeToDelegate[value] = delegate
49+
}
50+
51+
return object : TypeAdapter<R>() {
52+
override fun read(input: JsonReader): R {
53+
val jsonElement = gson.fromJson<JsonElement>(input, JsonElement::class.java)
54+
val labelJsonElement: JsonElement?
55+
labelJsonElement = if (maintainType) {
56+
jsonElement.asJsonObject.get(typeFieldName)
57+
} else {
58+
jsonElement.asJsonObject.remove(typeFieldName)
59+
}
60+
if (labelJsonElement == null) {
61+
throw JsonParseException("cannot deserialize ${baseType} because it does not define a field named ${typeFieldName}")
62+
}
63+
val label = labelJsonElement.asString
64+
val delegate = labelToDelegate[label]
65+
?: throw JsonParseException("cannot deserialize ${baseType} subtype named ${label}; did you forget to register a subtype?")
66+
return delegate.fromJsonTree(jsonElement) as R
67+
}
68+
69+
override fun write(out: JsonWriter, value: R) {
70+
val srcType = value!!::class.java
71+
val label = subtypeToLabel[srcType]
72+
?: throw JsonParseException("cannot serialize ${srcType.name}; did you forget to register a subtype?")
73+
val delegate = subtypeToDelegate[srcType] as TypeAdapter<R>
74+
var jsonObject = delegate.toJsonTree(value).asJsonObject
75+
if (maintainType) {
76+
gson.toJson(jsonObject, out)
77+
return
78+
}
79+
val clone = JsonObject()
80+
if (jsonObject.has(typeFieldName)) {
81+
throw JsonParseException("cannot serialize ${srcType.name} because it already defines a field named ${typeFieldName}")
82+
}
83+
clone.add(typeFieldName, JsonPrimitive(label))
84+
for ((key, value1) in jsonObject.entrySet()) {
85+
clone.add(key, value1)
86+
}
87+
gson.toJson(clone, out)
88+
}
89+
}.nullSafe() as TypeAdapter<R>
90+
}
91+
}

app/src/main/java/me/alllexey123/itmowidgets/workers/LessonWidgetUpdateWorker.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import me.alllexey123.itmowidgets.ItmoWidgetsApp
1212
import me.alllexey123.itmowidgets.ui.widgets.LessonListWidget
1313
import me.alllexey123.itmowidgets.ui.widgets.SingleLessonWidget
1414
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListWidgetData
15+
import me.alllexey123.itmowidgets.ui.widgets.data.LessonListWidgetEntry
1516
import me.alllexey123.itmowidgets.ui.widgets.data.LessonWidgetDataManager
1617
import me.alllexey123.itmowidgets.ui.widgets.data.SingleLessonWidgetData
1718
import java.time.Duration
@@ -29,6 +30,7 @@ class LessonWidgetUpdateWorker(
2930
val appContainer = (appContext as ItmoWidgetsApp).appContainer
3031
val storage = appContainer.storage
3132
val repository = appContainer.scheduleRepository
33+
val lessonListRepository = appContainer.lessonListRepository
3234
storage.setLastUpdateTimestamp(System.currentTimeMillis())
3335

3436
val appWidgetManager = AppWidgetManager.getInstance(appContext)
@@ -49,7 +51,7 @@ class LessonWidgetUpdateWorker(
4951

5052
val dataManager = LessonWidgetDataManager(repository, storage)
5153
val widgetsState = dataManager.getLessonWidgetsState()
52-
54+
lessonListRepository.setData(widgetsState.lessonListWidgetData)
5355
updateSingleLessonWidgets(
5456
appWidgetManager,
5557
singleWidgetIds,
@@ -59,16 +61,16 @@ class LessonWidgetUpdateWorker(
5961
updateLessonListWidgets(
6062
appWidgetManager,
6163
listWidgetIds,
62-
widgetsState.lessonListWidgetData,
6364
onlyDataChanged
6465
)
6566

6667
storage.setLessonWidgetStyleChanged(false)
6768

6869
scheduleNextUpdate(appContext, widgetsState.nextUpdateAt.plusSeconds(3))
6970
} catch (e: Exception) {
71+
e.printStackTrace()
7072
storage.setErrorLog("[${javaClass.name}]: ${e.stackTraceToString()}}")
71-
73+
lessonListRepository.setData(LessonListWidgetData(listOf(LessonListWidgetEntry.Error)))
7274
scheduleNextUpdate(appContext, null)
7375
}
7476

@@ -88,13 +90,12 @@ class LessonWidgetUpdateWorker(
8890
private fun updateLessonListWidgets(
8991
appWidgetManager: AppWidgetManager,
9092
widgetIds: IntArray,
91-
data: LessonListWidgetData,
9293
onlyDataChanged: Boolean
9394
) {
9495
widgetIds.forEach { appWidgetId ->
9596
val providerInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
9697
val outerLayoutId = providerInfo.initialLayout
97-
LessonListWidget.updateAppWidget(appContext, appWidgetManager, appWidgetId, data, outerLayoutId, onlyDataChanged)
98+
LessonListWidget.updateAppWidget(appContext, appWidgetManager, appWidgetId, outerLayoutId, onlyDataChanged)
9899
}
99100
}
100101

0 commit comments

Comments
 (0)