Skip to content

Commit a15f7b5

Browse files
author
LeanBitLab
committed
feat: support multi-widget config & add release CI
1 parent d7e0cc0 commit a15f7b5

6 files changed

Lines changed: 260 additions & 26 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Release APK
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch:
8+
inputs:
9+
tag_name:
10+
description: 'Tag name for the release (e.g. v1.0.0)'
11+
required: true
12+
default: 'v-dev'
13+
14+
permissions:
15+
contents: write
16+
17+
jobs:
18+
build-and-release:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Checkout Code
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Set up Java
28+
uses: actions/setup-java@v4
29+
with:
30+
distribution: 'zulu'
31+
java-version: '17'
32+
cache: 'gradle'
33+
34+
- name: Configure Keystore Signing
35+
env:
36+
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
37+
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
38+
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
39+
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
40+
if: ${{ env.KEYSTORE_BASE64 != '' && env.KEYSTORE_PASSWORD != '' && env.KEY_ALIAS != '' && env.KEY_PASSWORD != '' }}
41+
run: |
42+
echo "$KEYSTORE_BASE64" | base64 --decode > my-release-key-v2.jks
43+
echo "storePassword=$KEYSTORE_PASSWORD" > keystore.properties
44+
echo "keyPassword=$KEY_PASSWORD" >> keystore.properties
45+
echo "keyAlias=$KEY_ALIAS" >> keystore.properties
46+
echo "storeFile=../my-release-key-v2.jks" >> keystore.properties
47+
48+
- name: Make Gradle Wrapper Executable
49+
run: chmod +x gradlew
50+
51+
- name: Run Unit Tests
52+
run: ./gradlew test
53+
54+
- name: Build Release APK
55+
run: ./gradlew assembleRelease
56+
57+
- name: Create GitHub Release and Upload APK
58+
uses: softprops/action-gh-release@v2
59+
with:
60+
tag_name: ${{ github.event.inputs.tag_name || github.ref_name }}
61+
generate_release_notes: true
62+
files: |
63+
app/build/outputs/apk/release/*.apk
64+
draft: false
65+
prerelease: ${{ contains(github.event.inputs.tag_name || github.ref_name, '-') }}
66+
env:
67+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎app/build.gradle.kts‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ android {
2626
signingConfigs {
2727
create("release") {
2828
val keystorePropertiesFile = rootProject.file("keystore.properties")
29-
val keystoreProperties = Properties().apply {
30-
load(keystorePropertiesFile.inputStream())
29+
if (keystorePropertiesFile.exists()) {
30+
val keystoreProperties = Properties().apply {
31+
load(keystorePropertiesFile.inputStream())
32+
}
33+
storeFile = file(keystoreProperties.getProperty("storeFile"))
34+
storePassword = keystoreProperties.getProperty("storePassword")
35+
keyAlias = keystoreProperties.getProperty("keyAlias")
36+
keyPassword = keystoreProperties.getProperty("keyPassword")
3137
}
32-
33-
storeFile = file(keystoreProperties.getProperty("storeFile"))
34-
storePassword = keystoreProperties.getProperty("storePassword")
35-
keyAlias = keystoreProperties.getProperty("keyAlias")
36-
keyPassword = keystoreProperties.getProperty("keyPassword")
3738
}
3839
}
3940

@@ -42,7 +43,10 @@ android {
4243
isMinifyEnabled = true
4344
isShrinkResources = true
4445
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
45-
signingConfig = signingConfigs.getByName("release")
46+
val keystorePropertiesFile = rootProject.file("keystore.properties")
47+
if (keystorePropertiesFile.exists()) {
48+
signingConfig = signingConfigs.getByName("release")
49+
}
4650
}
4751
debug {
4852
applicationIdSuffix = ".debug"

‎app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import android.os.Build
2525
import android.content.ComponentName
2626
import android.content.Context
2727
import android.content.Intent
28+
import android.content.SharedPreferences
2829
import android.content.IntentFilter
2930
import android.net.NetworkCapabilities
3031
import android.app.usage.NetworkStatsManager
@@ -210,8 +211,14 @@ class AwidgetProvider : AppWidgetProvider() {
210211
}
211212

212213
// Suspended function called from Coroutine
213-
fun buildAppWidgetRemoteViews(context: Context, mode: UpdateMode = UpdateMode.FULL): RemoteViews {
214-
val prefs = context.getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
214+
fun buildAppWidgetRemoteViews(context: Context, appWidgetId: Int, mode: UpdateMode = UpdateMode.FULL): RemoteViews {
215+
val globalPrefs = context.getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
216+
val prefs = if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
217+
val wPrefs = context.getSharedPreferences("com.leanbitlab.lwidget.PREFS_$appWidgetId", Context.MODE_PRIVATE)
218+
FallbackPreferences(wPrefs, globalPrefs)
219+
} else {
220+
globalPrefs
221+
}
215222

216223
// --- Load Preferences ---
217224
val showTime = prefs.getBoolean("show_time", true)
@@ -491,7 +498,7 @@ class AwidgetProvider : AppWidgetProvider() {
491498
return tickViews
492499
} else if (mode == UpdateMode.CALENDAR_ONLY) {
493500
val calViews = RemoteViews(context.packageName, layoutId)
494-
if (showEvents) loadCalendarEvents(context, calViews, sizeEvents, primaryColor, secondaryColor)
501+
if (showEvents) loadCalendarEvents(context, calViews, sizeEvents, primaryColor, secondaryColor, prefs)
495502
return calViews
496503
} else if (mode == UpdateMode.TASKS_ONLY) {
497504
val taskViews = RemoteViews(context.packageName, layoutId)
@@ -852,7 +859,7 @@ class AwidgetProvider : AppWidgetProvider() {
852859
views.setViewVisibility(R.id.events_container, if (showEvents || showTasks) android.view.View.VISIBLE else android.view.View.GONE)
853860

854861
if (showEvents) {
855-
loadCalendarEvents(context, views, sizeEvents, primaryColor, secondaryColor)
862+
loadCalendarEvents(context, views, sizeEvents, primaryColor, secondaryColor, prefs)
856863
} else if (showTasks) {
857864
loadTasks(context, views, sizeTasks, primaryColor)
858865
}
@@ -882,15 +889,18 @@ class AwidgetProvider : AppWidgetProvider() {
882889
views.setOnClickPendingIntent(R.id.events_container, refreshPendingIntent)
883890
}
884891

885-
val settingsIntent = Intent(context, MainActivity::class.java)
886-
val settingsPendingIntent = PendingIntent.getActivity(context, 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE)
892+
val settingsIntent = Intent(context, MainActivity::class.java).apply {
893+
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
894+
data = android.net.Uri.parse("lwidget://widget/$appWidgetId")
895+
}
896+
val settingsPendingIntent = PendingIntent.getActivity(context, appWidgetId, settingsIntent, PendingIntent.FLAG_IMMUTABLE)
887897
views.setOnClickPendingIntent(R.id.widget_root, settingsPendingIntent)
888898

889899
return views
890900
}
891901

892902
fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, mode: UpdateMode = UpdateMode.FULL) {
893-
val views = buildAppWidgetRemoteViews(context, mode)
903+
val views = buildAppWidgetRemoteViews(context, appWidgetId, mode)
894904
if (mode == UpdateMode.FULL) {
895905
appWidgetManager.updateAppWidget(appWidgetId, views)
896906
} else {
@@ -979,8 +989,7 @@ class AwidgetProvider : AppWidgetProvider() {
979989
return events
980990
}
981991

982-
private fun bindCalendarEvents(context: Context, views: RemoteViews, events: List<EventInfo>, textSizeSp: Float, primaryColor: Int, secondaryColor: Int, eventViews: List<Int>) {
983-
val prefs = context.getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
992+
private fun bindCalendarEvents(context: Context, views: RemoteViews, events: List<EventInfo>, textSizeSp: Float, primaryColor: Int, secondaryColor: Int, eventViews: List<Int>, prefs: SharedPreferences) {
984993
val showDayAbbr = prefs.getBoolean("show_day_abbr_in_events", true)
985994

986995
val timeFormatter = getFormatter("h:mm")
@@ -1057,7 +1066,7 @@ class AwidgetProvider : AppWidgetProvider() {
10571066
}
10581067
}
10591068

1060-
private fun loadCalendarEvents(context: Context, views: RemoteViews, textSizeSp: Float, primaryColor: Int, secondaryColor: Int) {
1069+
private fun loadCalendarEvents(context: Context, views: RemoteViews, textSizeSp: Float, primaryColor: Int, secondaryColor: Int, prefs: SharedPreferences) {
10611070
if (androidx.core.content.ContextCompat.checkSelfPermission(
10621071
context, android.Manifest.permission.READ_CALENDAR
10631072
) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
@@ -1073,7 +1082,7 @@ class AwidgetProvider : AppWidgetProvider() {
10731082

10741083
try {
10751084
val events = fetchCalendarEvents(context)
1076-
bindCalendarEvents(context, views, events, textSizeSp, primaryColor, secondaryColor, eventViews)
1085+
bindCalendarEvents(context, views, events, textSizeSp, primaryColor, secondaryColor, eventViews, prefs)
10771086
} catch (e: Exception) {
10781087
// Log and gracefully handle crash
10791088
android.util.Log.e("LWidget", "Error loading calendar events", e)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.leanbitlab.lwidget
2+
3+
import android.content.SharedPreferences
4+
5+
class FallbackPreferences(
6+
private val widgetPrefs: SharedPreferences,
7+
private val globalPrefs: SharedPreferences
8+
) : SharedPreferences {
9+
10+
override fun getAll(): Map<String, *> {
11+
val merged = HashMap<String, Any?>()
12+
merged.putAll(globalPrefs.all)
13+
merged.putAll(widgetPrefs.all)
14+
return merged
15+
}
16+
17+
override fun getString(key: String, defValue: String?): String? {
18+
return if (widgetPrefs.contains(key)) {
19+
widgetPrefs.getString(key, defValue)
20+
} else {
21+
globalPrefs.getString(key, defValue)
22+
}
23+
}
24+
25+
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
26+
return if (widgetPrefs.contains(key)) {
27+
widgetPrefs.getStringSet(key, defValues)
28+
} else {
29+
globalPrefs.getStringSet(key, defValues)
30+
}
31+
}
32+
33+
override fun getInt(key: String, defValue: Int): Int {
34+
return if (widgetPrefs.contains(key)) {
35+
widgetPrefs.getInt(key, defValue)
36+
} else {
37+
globalPrefs.getInt(key, defValue)
38+
}
39+
}
40+
41+
override fun getLong(key: String, defValue: Long): Long {
42+
return if (widgetPrefs.contains(key)) {
43+
widgetPrefs.getLong(key, defValue)
44+
} else {
45+
globalPrefs.getLong(key, defValue)
46+
}
47+
}
48+
49+
override fun getFloat(key: String, defValue: Float): Float {
50+
return if (widgetPrefs.contains(key)) {
51+
widgetPrefs.getFloat(key, defValue)
52+
} else {
53+
globalPrefs.getFloat(key, defValue)
54+
}
55+
}
56+
57+
override fun getBoolean(key: String, defValue: Boolean): Boolean {
58+
return if (widgetPrefs.contains(key)) {
59+
widgetPrefs.getBoolean(key, defValue)
60+
} else {
61+
globalPrefs.getBoolean(key, defValue)
62+
}
63+
}
64+
65+
override fun contains(key: String): Boolean {
66+
return widgetPrefs.contains(key) || globalPrefs.contains(key)
67+
}
68+
69+
override fun edit(): SharedPreferences.Editor {
70+
return FallbackEditor(widgetPrefs.edit())
71+
}
72+
73+
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
74+
widgetPrefs.registerOnSharedPreferenceChangeListener(listener)
75+
globalPrefs.registerOnSharedPreferenceChangeListener(listener)
76+
}
77+
78+
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
79+
widgetPrefs.unregisterOnSharedPreferenceChangeListener(listener)
80+
globalPrefs.unregisterOnSharedPreferenceChangeListener(listener)
81+
}
82+
83+
class FallbackEditor(private val editor: SharedPreferences.Editor) : SharedPreferences.Editor {
84+
override fun putString(key: String, value: String?): SharedPreferences.Editor = apply { editor.putString(key, value) }
85+
override fun putStringSet(key: String, values: MutableSet<String>?): SharedPreferences.Editor = apply { editor.putStringSet(key, values) }
86+
override fun putInt(key: String, value: Int): SharedPreferences.Editor = apply { editor.putInt(key, value) }
87+
override fun putLong(key: String, value: Long): SharedPreferences.Editor = apply { editor.putLong(key, value) }
88+
override fun putFloat(key: String, value: Float): SharedPreferences.Editor = apply { editor.putFloat(key, value) }
89+
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = apply { editor.putBoolean(key, value) }
90+
override fun remove(key: String): SharedPreferences.Editor = apply { editor.remove(key) }
91+
override fun clear(): SharedPreferences.Editor = apply { editor.clear() }
92+
override fun commit(): Boolean = editor.commit()
93+
override fun apply() = editor.apply()
94+
}
95+
}

0 commit comments

Comments
 (0)