diff --git a/.fleet/goals/example.md b/.fleet/goals/example.md
deleted file mode 100644
index a473a00..0000000
--- a/.fleet/goals/example.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-milestone: "1"
----
-
-# Example Fleet Goal
-
-Analyze the codebase for potential improvements and create
-issues for the engineering team.
-
-## Tools
-- Test Coverage: `npx vitest --coverage --json`
-
-## Assessment Hints
-- Focus on missing error handling in API routes
-- Look for hardcoded configuration values
-
-## Insight Hints
-- Report on overall test coverage metrics
-- Note any unusually complex functions (cyclomatic complexity)
-
-## Constraints
-- Do NOT propose changes already covered by open issues
-- Do NOT propose changes rejected in recently closed issues
-- Keep tasks small and isolated — one logical change per issue
diff --git a/.github/workflows/fleet-analyze.yml b/.github/workflows/fleet-analyze.yml
index 73efc52..32ba997 100644
--- a/.github/workflows/fleet-analyze.yml
+++ b/.github/workflows/fleet-analyze.yml
@@ -4,8 +4,6 @@
name: Fleet Analyze
on:
- schedule:
- - cron: '0 */6 * * *'
workflow_dispatch:
inputs:
goal:
diff --git a/.github/workflows/fleet-merge.yml b/.github/workflows/fleet-merge.yml
index 87ac367..751c9c3 100644
--- a/.github/workflows/fleet-merge.yml
+++ b/.github/workflows/fleet-merge.yml
@@ -4,8 +4,6 @@
name: Fleet Merge
on:
- schedule:
- - cron: '0 */3 * * *'
workflow_dispatch:
inputs:
mode:
diff --git a/activity_main_patch.diff b/activity_main_patch.diff
deleted file mode 100644
index 92b385f..0000000
--- a/activity_main_patch.diff
+++ /dev/null
@@ -1,19 +0,0 @@
---- app/src/main/res/layout/activity_main.xml
-+++ app/src/main/res/layout/activity_main.xml
-@@ -1246,6 +1246,16 @@
- android:layout_height="wrap_content"/>
-
-
-+
-+
-+
-+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5daf6f8..9db95d8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -15,8 +15,8 @@ android {
applicationId = "com.leanbitlab.lwidget"
minSdk = 26
targetSdk = 35
- versionCode = 15
- versionName = "2.0"
+ versionCode = 16
+ versionName = "2.1"
}
testOptions {
diff --git a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt b/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
index 9bc7daa..5d0ee2a 100644
--- a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
+++ b/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
@@ -138,8 +138,9 @@ class AwidgetProvider : AppWidgetProvider() {
android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
)
- // 1 minute
- val intervalMillis = 1L * 60L * 1000L
+ val prefs = context.getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
+ val intervalMinutes = prefs.getFloat("update_interval", 15f)
+ val intervalMillis = (intervalMinutes * 60f * 1000f).toLong().coerceAtLeast(60000L) // min 1 min
alarmManager.setInexactRepeating(
android.app.AlarmManager.RTC,
diff --git a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt.orig b/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt.orig
deleted file mode 100644
index ae333f9..0000000
--- a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt.orig
+++ /dev/null
@@ -1,1272 +0,0 @@
-/*
- * Copyright (C) 2026 LeanBitLab
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.leanbitlab.lwidget
-
-import android.app.AlarmManager
-import android.app.PendingIntent
-import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProvider
-import android.os.Build
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.net.ConnectivityManager
-import android.net.NetworkCapabilities
-import android.app.usage.NetworkStatsManager
-import android.os.BatteryManager
-import android.text.Spannable
-import android.text.SpannableString
-import android.text.style.ForegroundColorSpan
-import android.widget.RemoteViews
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import java.time.Instant
-import java.time.LocalDate
-import java.time.LocalDateTime
-import java.time.ZoneId
-import java.time.format.DateTimeFormatter
-import java.util.Locale
-
-enum class UpdateMode {
- FULL, TICK, CALENDAR_ONLY, TASKS_ONLY, ALARM_ONLY
-}
-
-class AwidgetProvider : AppWidgetProvider() {
-
- override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
- val pendingResult = goAsync()
- CoroutineScope(Dispatchers.IO).launch {
- try {
- for (appWidgetId in appWidgetIds) {
- updateAppWidget(context, appWidgetManager, appWidgetId, UpdateMode.FULL)
- }
- } finally {
- pendingResult.finish()
- }
- }
- }
-
- override fun onEnabled(context: Context) {
- super.onEnabled(context)
- scheduleWork(context)
- }
-
- override fun onDisabled(context: Context) {
- super.onDisabled(context)
- cancelWork(context)
- }
-
- override fun onReceive(context: Context, intent: Intent) {
- super.onReceive(context, intent)
-
- val appWidgetManager = AppWidgetManager.getInstance(context)
- val thisAppWidget = ComponentName(context, AwidgetProvider::class.java)
- val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
-
- if (intent.action in listOf(
- Intent.ACTION_BOOT_COMPLETED,
- ACTION_BATTERY_UPDATE,
- StepCounterService.ACTION_STEP_UPDATE,
- Intent.ACTION_PROVIDER_CHANGED,
- android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED,
- "nodomain.freeyourgadget.gadgetbridge.ACTION_GENERIC_WEATHER"
- )) {
- val pendingResult = goAsync()
- CoroutineScope(Dispatchers.IO).launch {
- try {
- when (intent.action) {
- Intent.ACTION_BOOT_COMPLETED -> {
- scheduleWork(context)
- appWidgetIds.forEach { updateAppWidget(context, appWidgetManager, it, UpdateMode.FULL) }
- }
- ACTION_BATTERY_UPDATE -> {
- appWidgetIds.forEach { updateAppWidget(context, appWidgetManager, it, UpdateMode.TICK) }
- }
- StepCounterService.ACTION_STEP_UPDATE -> {
- // Step event updates match Tick mode conceptually
- appWidgetIds.forEach { updateAppWidget(context, appWidgetManager, it, UpdateMode.TICK) }
- }
- android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED -> {
- appWidgetIds.forEach { updateAppWidget(context, appWidgetManager, it, UpdateMode.ALARM_ONLY) }
- }
- Intent.ACTION_PROVIDER_CHANGED -> {
- val host = intent.data?.host
- val mode = if (host == "com.android.calendar") UpdateMode.CALENDAR_ONLY else UpdateMode.TASKS_ONLY
- appWidgetIds.forEach { updateAppWidget(context, appWidgetManager, it, mode) }
- }
- "nodomain.freeyourgadget.gadgetbridge.ACTION_GENERIC_WEATHER" -> {
- val weatherJson = intent.getStringExtra("WeatherJson")
- if (!weatherJson.isNullOrEmpty()) {
- com.leanbitlab.lwidget.weather.BreezyWeatherFetcher.saveLatestWeatherData(context, weatherJson)
- appWidgetIds.forEach { updateAppWidget(context, appWidgetManager, it, UpdateMode.FULL) }
- }
- }
- }
- } finally {
- pendingResult.finish()
- }
- }
- }
- }
-
- private fun scheduleWork(context: Context) {
- val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as android.app.AlarmManager
- val intent = Intent(context, AwidgetProvider::class.java).apply {
- action = ACTION_BATTERY_UPDATE
- }
- val pendingIntent = android.app.PendingIntent.getBroadcast(
- context,
- 500,
- intent,
- android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
- )
-
- // 1 minute
- val intervalMillis = 1L * 60L * 1000L
-
- alarmManager.setInexactRepeating(
- android.app.AlarmManager.RTC,
- System.currentTimeMillis() + intervalMillis,
- intervalMillis,
- pendingIntent
- )
- }
-
- private fun cancelWork(context: Context) {
- val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as android.app.AlarmManager
- val intent = Intent(context, AwidgetProvider::class.java).apply {
- action = ACTION_BATTERY_UPDATE
- }
- val pendingIntent = android.app.PendingIntent.getBroadcast(
- context,
- 500,
- intent,
- android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
- )
- alarmManager.cancel(pendingIntent)
- }
-
- companion object {
- const val ACTION_BATTERY_UPDATE = "com.leanbitlab.lwidget.ACTION_BATTERY_UPDATE"
- const val PERMISSION_READ_TASKS_ORG = "org.tasks.permission.READ_TASKS"
- const val PERMISSION_READ_TASKS_ASTRID = "com.todoroo.astrid.READ"
-
- // Suspended function called from Coroutine
- fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, mode: UpdateMode = UpdateMode.FULL) {
- val prefs = context.getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
-
- // --- Load Preferences ---
- val showTime = prefs.getBoolean("show_time", true)
- val sizeTime = prefs.getFloat("size_time", 64f)
-
- val showDate = prefs.getBoolean("show_date", true)
- val sizeDate = prefs.getFloat("size_date", 14f)
-
- val showBattery = prefs.getBoolean("show_battery", true)
- val sizeBattery = prefs.getFloat("size_battery", 24f)
- val boldBattery = prefs.getBoolean("bold_battery", false)
-
- val showTemp = prefs.getBoolean("show_temp", true)
- val sizeTemp = prefs.getFloat("size_temp", 18f)
- val boldTemp = prefs.getBoolean("bold_temp", false)
-
- val showWeatherCondition = prefs.getBoolean("show_weather_condition", false)
- val sizeWeather = prefs.getFloat("size_weather", 18f)
- val boldWeather = prefs.getBoolean("bold_weather", false)
-
- var showEvents = prefs.getBoolean("show_events", true)
- if (showEvents && androidx.core.content.ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
- showEvents = false
- }
- val sizeEvents = prefs.getFloat("size_events", 14f)
-
- // Fetch Breezy Weather Data
- val bweather = com.leanbitlab.lwidget.weather.BreezyWeatherFetcher.fetchLocalWeather(context)
- val showWeatherIconOnly = prefs.getBoolean("show_weather_icon_only", false)
-
- android.util.Log.d("WidgetLife", "UpdateMode FULL | Condition: $showWeatherCondition | IconOnly: $showWeatherIconOnly | WeatherData: ${bweather?.currentCondition}")
-
- val useSystemTheme = prefs.getBoolean("use_system_theme", false)
- val useDynamicColors = prefs.getBoolean("use_dynamic_colors", true)
-
- // Determine if light theme based on system or manual override
- val useLightTheme = if (useSystemTheme) {
- val nightMode = context.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK
- nightMode != android.content.res.Configuration.UI_MODE_NIGHT_YES
- } else {
- false // Default to dark when system theme is off
- }
-
- val timeFormatIdx = prefs.getInt("time_format_idx", 0)
- val dateFormatIdx = prefs.getInt("date_format_idx", 0)
-
- var showData = prefs.getBoolean("show_data_usage", false)
- if (showData) {
- val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as android.app.AppOpsManager
- val opMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- appOps.unsafeCheckOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.packageName)
- } else {
- @Suppress("DEPRECATION")
- appOps.checkOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.packageName)
- }
- if (opMode != android.app.AppOpsManager.MODE_ALLOWED) showData = false
- }
- val sizeData = prefs.getFloat("size_data", 14f)
-
- val showWorldClock = prefs.getBoolean("show_world_clock", false)
- val sizeWorldClock = prefs.getFloat("size_world_clock", 18f)
- val worldClockZoneStr = prefs.getString("world_clock_zone_str", "UTC") ?: "UTC"
-
- val showStorage = prefs.getBoolean("show_storage", true)
- val sizeStorage = prefs.getFloat("size_storage", 14f)
-
- var showTasks = prefs.getBoolean("show_tasks", false)
- if (showTasks && androidx.core.content.ContextCompat.checkSelfPermission(context, PERMISSION_READ_TASKS_ORG) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
- showTasks = false
- }
- val sizeTasks = prefs.getFloat("size_tasks", 14f)
-
- val showNextAlarm = prefs.getBoolean("show_next_alarm", true)
- val sizeNextAlarm = prefs.getFloat("size_next_alarm", 14f)
-
- var showSteps = prefs.getBoolean("show_steps", false)
- if (showSteps && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- if (androidx.core.content.ContextCompat.checkSelfPermission(context, android.Manifest.permission.ACTIVITY_RECOGNITION) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
- showSteps = false
- }
- }
- val sizeSteps = prefs.getFloat("size_steps", 14f)
-
- // Make sure the background Step Service is running if steps or keep-alive is enabled
- val keepAlive = prefs.getBoolean("keep_alive", false)
- val serviceIntent = Intent(context, StepCounterService::class.java)
- // FOREGROUND_SERVICE_TYPE_HEALTH requires ACTIVITY_RECOGNITION at runtime
- val hasActivityPerm = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- androidx.core.content.ContextCompat.checkSelfPermission(context, android.Manifest.permission.ACTIVITY_RECOGNITION) == android.content.pm.PackageManager.PERMISSION_GRANTED
- } else true
- if ((showSteps || keepAlive) && hasActivityPerm) {
- context.startForegroundService(serviceIntent)
- } else {
- context.stopService(serviceIntent)
- }
-
-
- val fontStyle = prefs.getInt("font_style", 0)
-
- val bgOpacity = prefs.getFloat("bg_opacity", 100f)
- val textColorPrimaryIdx = prefs.getInt("text_color_primary_idx", 0)
- val textColorSecondaryIdx = prefs.getInt("text_color_secondary_idx", 0)
- val bgColorIdx = prefs.getInt("bg_color_idx", 0)
-
- // --- Theme & Font Setup ---
- fun getLayout(fontIdx: Int): Int {
- return when (fontIdx) {
- 1 -> R.layout.widget_layout_serif
- 2 -> R.layout.widget_layout_mono
- 3 -> R.layout.widget_layout_cursive
- 4 -> R.layout.widget_layout_condensed
- 5 -> R.layout.widget_layout_condensed_light
- 6 -> R.layout.widget_layout_light
- 7 -> R.layout.widget_layout_medium
- 8 -> R.layout.widget_layout_black
- 9 -> R.layout.widget_layout_thin
- 10 -> R.layout.widget_layout_smallcaps
- else -> R.layout.widget_layout
- }
- }
-
- val layoutId = getLayout(fontStyle)
-
- val views = RemoteViews(context.packageName, layoutId)
-
- // --- Background & Outline Application ---
- val outlineColorIdx = prefs.getInt("outline_color_idx", 0)
-
- // Background
- views.setImageViewResource(R.id.widget_background, R.drawable.widget_bg_fill)
-
- // Resolve background color (0=Default, 1=System Accent, 2=Custom)
- fun resolveBgColor(idx: Int, isLight: Boolean): Int {
- return when (idx) {
- 0 -> if (isLight) android.graphics.Color.WHITE else android.graphics.Color.parseColor("#212121")
- 1 -> if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- context.getColor(android.R.color.system_accent1_500)
- } else {
- android.graphics.Color.CYAN
- }
- 2 -> {
- val r = prefs.getInt("bg_color_r", 255)
- val g = prefs.getInt("bg_color_g", 255)
- val b = prefs.getInt("bg_color_b", 255)
- android.graphics.Color.rgb(r, g, b)
- }
- else -> if (isLight) android.graphics.Color.WHITE else android.graphics.Color.parseColor("#212121")
- }
- }
-
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- views.setColorStateList(R.id.widget_background, "setImageTintList", android.content.res.ColorStateList.valueOf(resolveBgColor(bgColorIdx, useLightTheme)))
- } else {
- views.setInt(R.id.widget_background, "setColorFilter", resolveBgColor(bgColorIdx, useLightTheme))
- }
-
- val alpha255 = (bgOpacity * 255 / 100).toInt().coerceIn(0, 255)
- views.setInt(R.id.widget_background, "setImageAlpha", alpha255)
-
- // Outline
- // Resolve outline using same logic (0=Default, 1=System, 2=Custom)
- fun resolveOutlineColor(idx: Int): Int {
- return when (idx) {
- 0 -> context.getColor(R.color.widget_outline) // Default
- 1 -> if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- context.getColor(android.R.color.system_accent1_500)
- } else {
- android.graphics.Color.CYAN
- }
- 2 -> {
- val r = prefs.getInt("outline_color_r", 255)
- val g = prefs.getInt("outline_color_g", 255)
- val b = prefs.getInt("outline_color_b", 255)
- android.graphics.Color.rgb(r, g, b)
- }
- else -> context.getColor(R.color.widget_outline)
- }
- }
-
- val showOutline = prefs.getBoolean("show_outline", true)
- val outlineColor = resolveOutlineColor(outlineColorIdx)
- views.setImageViewResource(R.id.widget_outline, R.drawable.widget_bg_outline)
- views.setViewVisibility(R.id.widget_outline, if (showOutline) android.view.View.VISIBLE else android.view.View.GONE)
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- views.setColorStateList(R.id.widget_outline, "setImageTintList", android.content.res.ColorStateList.valueOf(outlineColor))
- } else {
- views.setInt(R.id.widget_outline, "setColorFilter", outlineColor)
- }
- views.setInt(R.id.widget_outline, "setImageAlpha", 255)
-
- // Resolve Colors
- fun resolveColor(idx: Int, isPrimary: Boolean, isLight: Boolean): Int {
- return ColorResolver.resolveColor(
- context = context,
- prefs = prefs,
- useDynamicColors = useDynamicColors,
- idx = idx,
- isPrimary = isPrimary,
- isLight = isLight
- )
- }
-
- val primaryColor = resolveColor(textColorPrimaryIdx, true, useLightTheme)
- val secondaryColor = resolveColor(textColorSecondaryIdx, false, useLightTheme)
-
- // Slightly distinct colors for date and next alarm
- val dateColor = if (useDynamicColors && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- // Warm accent for date
- context.getColor(if (useLightTheme) android.R.color.system_accent2_700 else android.R.color.system_accent2_100)
- } else {
- if (useLightTheme) android.graphics.Color.parseColor("#AA555544") else android.graphics.Color.parseColor("#BBDDDDCC")
- }
- val alarmColor = if (useDynamicColors && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- // Cool tertiary accent for alarm
- context.getColor(if (useLightTheme) android.R.color.system_accent3_700 else android.R.color.system_accent3_100)
- } else {
- if (useLightTheme) android.graphics.Color.parseColor("#AA445566") else android.graphics.Color.parseColor("#BBAACCDD")
- }
-
- // Background & outline dynamic color
- if (useDynamicColors && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- // Warm neutral surface for background (overrides custom bg color when dynamic is on)
- views.setColorStateList(R.id.widget_background, "setImageTintList", android.content.res.ColorStateList.valueOf(context.getColor(if (useLightTheme) android.R.color.system_neutral2_50 else android.R.color.system_neutral1_800)))
- // Accent-tinted outline
- if (showOutline) {
- views.setColorStateList(R.id.widget_outline, "setImageTintList", android.content.res.ColorStateList.valueOf(context.getColor(if (useLightTheme) android.R.color.system_accent1_300 else android.R.color.system_accent1_400)))
- }
- }
-
- if (mode == UpdateMode.TICK) {
- val tickViews = RemoteViews(context.packageName, layoutId)
- val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
- context.registerReceiver(null, ifilter)
- }
- val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: 0
- val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: 100
- val batteryPct = (level * 100 / scale.toFloat()).toInt()
- val batterySpannable = android.text.SpannableString("${batteryPct}%")
- batterySpannable.setSpan(android.text.style.RelativeSizeSpan(0.5f), batterySpannable.length - 1, batterySpannable.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- val tempInt = batteryStatus?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0
- val tempVal = tempInt / 10f
- if (showSteps) loadStepCount(context, tickViews, prefs)
- if (showBattery) tickViews.setTextViewText(R.id.text_battery, batterySpannable)
- if (showTemp) {
- val tempStr = String.format("%.1f", tempVal)
- val tempText = "$tempStr°C"
- val tempSpan = android.text.SpannableString(tempText)
- val cIdx = tempText.indexOf("°C")
- if (cIdx != -1) {
- tempSpan.setSpan(android.text.style.RelativeSizeSpan(0.5f), cIdx, cIdx + 2, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
- if (boldTemp) tempSpan.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, tempSpan.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- tickViews.setTextViewText(R.id.text_temp, tempSpan)
- }
- if (showData) updateDataUsage(context, tickViews, prefs)
- if (showStorage) updateStorageStats(context, tickViews, prefs)
- appWidgetManager.partiallyUpdateAppWidget(appWidgetId, tickViews)
- return
- } else if (mode == UpdateMode.CALENDAR_ONLY) {
- val calViews = RemoteViews(context.packageName, layoutId)
- if (showEvents) loadCalendarEvents(context, calViews, sizeEvents, primaryColor, secondaryColor)
- appWidgetManager.partiallyUpdateAppWidget(appWidgetId, calViews)
- return
- } else if (mode == UpdateMode.TASKS_ONLY) {
- val taskViews = RemoteViews(context.packageName, layoutId)
- if (showTasks) loadTasks(context, taskViews, sizeTasks, primaryColor)
- appWidgetManager.partiallyUpdateAppWidget(appWidgetId, taskViews)
- return
- } else if (mode == UpdateMode.ALARM_ONLY) {
- val alarmViews = RemoteViews(context.packageName, layoutId)
- if (showNextAlarm) loadNextAlarm(context, alarmViews, sizeNextAlarm, secondaryColor)
- appWidgetManager.partiallyUpdateAppWidget(appWidgetId, alarmViews)
- return
- }
-
- // --- Apply Time ---
- val timeVisible = showTime || showWorldClock
- views.setViewVisibility(R.id.time_container, if (timeVisible) android.view.View.VISIBLE else android.view.View.GONE)
-
- views.setViewVisibility(R.id.clock_time, if (showTime) android.view.View.VISIBLE else android.view.View.GONE)
- views.setTextViewTextSize(R.id.clock_time, android.util.TypedValue.COMPLEX_UNIT_SP, sizeTime)
- views.setTextColor(R.id.clock_time, primaryColor)
-
- val (timeFormat12, timeFormat24) = when(timeFormatIdx) {
- 0 -> "h:mm" to "H:mm"
- 1 -> "H:mm" to "H:mm"
- else -> "h:mm" to "H:mm"
- }
- views.setCharSequence(R.id.clock_time, "setFormat12Hour", timeFormat12)
- views.setCharSequence(R.id.clock_time, "setFormat24Hour", timeFormat24)
-
- // --- World Clock ---
- views.setViewVisibility(R.id.text_world_clock, if (showWorldClock) android.view.View.VISIBLE else android.view.View.GONE)
- if (showWorldClock) {
- loadWorldClock(views, sizeWorldClock, secondaryColor, worldClockZoneStr, timeFormat12.contains("a"))
- }
-
- // --- Apply Date ---
- val dateVisible = showDate || showNextAlarm
- views.setViewVisibility(R.id.date_container, if (dateVisible) android.view.View.VISIBLE else android.view.View.GONE)
-
- views.setViewVisibility(R.id.clock_date, if (showDate) android.view.View.VISIBLE else android.view.View.GONE)
- views.setTextViewTextSize(R.id.clock_date, android.util.TypedValue.COMPLEX_UNIT_SP, sizeDate)
- views.setTextColor(R.id.clock_date, dateColor)
-
- val (dateFormat12, dateFormat24) = when(dateFormatIdx) {
- 0 -> "EEEE, MMMM dd" to "EEEE, MMMM dd"
- 1 -> "EEE, MMM dd" to "EEE, MMM dd"
- 2 -> "dd/MM/yyyy" to "dd/MM/yyyy"
- else -> "EEEE, MMMM dd" to "EEEE, MMMM dd"
- }
- views.setCharSequence(R.id.clock_date, "setFormat12Hour", dateFormat12)
- views.setCharSequence(R.id.clock_date, "setFormat24Hour", dateFormat24)
-
- // --- Apply Battery & Temp ---
- views.setViewVisibility(R.id.text_battery, if (showBattery) android.view.View.VISIBLE else android.view.View.GONE)
- views.setTextViewTextSize(R.id.text_battery, android.util.TypedValue.COMPLEX_UNIT_SP, sizeBattery)
-
- views.setViewVisibility(R.id.text_temp, if (showTemp) android.view.View.VISIBLE else android.view.View.GONE)
- views.setTextViewTextSize(R.id.text_temp, android.util.TypedValue.COMPLEX_UNIT_SP, sizeTemp)
- views.setTextColor(R.id.text_battery, secondaryColor)
- views.setTextColor(R.id.text_temp, secondaryColor)
-
- val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
- context.registerReceiver(null, ifilter)
- }
-
- val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: 0
- val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: 100
- val batteryPct = (level * 100 / scale.toFloat()).toInt()
-
- val tempInt = batteryStatus?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0
- val tempVal = tempInt / 10f
-
- if (showBattery) {
- val batterySpannable = android.text.SpannableString("${batteryPct}%")
- batterySpannable.setSpan(android.text.style.RelativeSizeSpan(0.5f), batterySpannable.length - 1, batterySpannable.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- if (boldBattery) batterySpannable.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, batterySpannable.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- views.setTextViewText(R.id.text_battery, batterySpannable)
- }
- if (showTemp) {
- val tempStr = String.format("%.1f", tempVal)
- val tempText = "$tempStr°C"
- val tempSpan = android.text.SpannableString(tempText)
- val cIdx = tempText.indexOf("°C")
- if (cIdx != -1) {
- tempSpan.setSpan(android.text.style.RelativeSizeSpan(0.5f), cIdx, cIdx + 2, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
- if (boldTemp) tempSpan.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, tempSpan.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- views.setTextViewText(R.id.text_temp, tempSpan)
- }
-
- // --- Weather Condition ---
- val showWeather = showWeatherCondition && bweather != null
- views.setViewVisibility(R.id.text_weather_condition, if (showWeather) android.view.View.VISIBLE else android.view.View.GONE)
- if (showWeather && bweather != null) {
- var weatherCode = bweather.currentConditionCode
- var weatherText = bweather.currentCondition
- var hasWarning = false
-
- // Check week forecasts for warnings
- val forecasts = bweather.forecasts
- if (forecasts != null && forecasts.isNotEmpty()) {
- for ((index, forecast) in forecasts.take(7).withIndex()) {
- val fCode = forecast.conditionCode
- if (fCode != null && (
- fCode in listOf(500, 501, 502, 503, 504, 511, 520, 521, 522, 531) || // Rain
- fCode in listOf(600, 601, 602, 611, 612, 615, 616, 620, 621, 622) || // Snow
- fCode in listOf(210, 211, 212, 221, 230, 231, 232) // Storm
- )) {
- hasWarning = true
- weatherCode = fCode
-
- // Determine string representation of the day
- val dayText = when (index) {
- 0 -> "today"
- 1 -> "tomorrow"
- else -> {
- val localDate = java.time.LocalDate.now().plusDays(index.toLong())
- localDate.dayOfWeek.getDisplayName(java.time.format.TextStyle.SHORT, java.util.Locale.getDefault())
- }
- }
-
- // Extract probability and create warning string
- val precipString = if (forecast.precipProbability != null && forecast.precipProbability > 0) "${forecast.precipProbability}% " else ""
- val conditionWarning = when (fCode) {
- in listOf(500, 501, 502, 503, 504, 511, 520, 521, 522, 531) -> if (index <= 1) "Rain $dayText" else "Rain on $dayText"
- in listOf(600, 601, 602, 611, 612, 615, 616, 620, 621, 622) -> if (index <= 1) "Snow $dayText" else "Snow on $dayText"
- in listOf(210, 211, 212, 221, 230, 231, 232) -> if (index <= 1) "Storm $dayText" else "Storm on $dayText"
- else -> "Warning"
- }
- weatherText = "$precipString$conditionWarning"
- break
- }
- }
- }
-
- var conditionText = weatherText
- if (conditionText.isNullOrEmpty()) {
- conditionText = "Unknown"
- }
-
- // Get weather icon string representation
- val weatherIcon = when (weatherCode) {
- 800 -> "☀️" // Clear
- 801, 802 -> "⛅" // Partly Cloudy
- 803, 804 -> "☁️" // Cloudy
- 500, 501, 502, 503, 504, 511, 520, 521, 522, 531 -> "🌧️" // Rain
- 600, 601, 602, 611, 612, 615, 616, 620, 621, 622 -> "❄️" // Snow
- 771 -> "🌬️" // Wind
- 741 -> "🌫️" // Fog
- 751 -> "🌁" // Haze
- 210, 211, 212, 221, 230, 231, 232 -> "⛈️" // Thunderstorm
- else -> ""
- }
-
- val displayString = if (showWeatherIconOnly && !hasWarning) weatherIcon else "$conditionText $weatherIcon"
-
- // If it has warning format like "Rain on Sun 🌧️", make " on Sun 🌧️" smaller
- if (hasWarning) {
- val fullMatch = weatherText ?: "Unknown"
- val span = android.text.SpannableString(displayString.trim())
-
- // The day portion is the last word for today/tomorrow, or the last two words for "on Sun"
- val lastSpaceIdx = fullMatch.lastIndexOf(' ')
- val onSpaceIdx = fullMatch.lastIndexOf(" on ")
-
- val shrinkStartIndex = if (onSpaceIdx != -1) onSpaceIdx else lastSpaceIdx
- if (shrinkStartIndex != -1 && shrinkStartIndex < span.length) {
- span.setSpan(android.text.style.RelativeSizeSpan(0.75f), shrinkStartIndex, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- } else if (weatherIcon.isNotEmpty()) {
- span.setSpan(android.text.style.RelativeSizeSpan(0.75f), span.length - weatherIcon.length, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
- if (boldWeather) span.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- views.setTextViewText(R.id.text_weather_condition, span)
- } else {
- val weatherSpan = android.text.SpannableString(displayString.trim())
- if (boldWeather) weatherSpan.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, weatherSpan.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- views.setTextViewText(R.id.text_weather_condition, weatherSpan)
- }
- views.setTextViewTextSize(R.id.text_weather_condition, android.util.TypedValue.COMPLEX_UNIT_SP, sizeWeather)
- views.setTextColor(R.id.text_weather_condition, secondaryColor)
-
- val launchIntent = context.packageManager.getLaunchIntentForPackage("org.breezyweather")
- if (launchIntent != null) {
- val pendingIntent = android.app.PendingIntent.getActivity(
- context, 0, launchIntent,
- android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
- )
- views.setOnClickPendingIntent(R.id.text_weather_condition, pendingIntent)
- }
- }
-
- // --- Data Usage ---
- views.setViewVisibility(R.id.text_data_usage, if (showData) android.view.View.VISIBLE else android.view.View.GONE)
- if (showData) {
- views.setTextViewTextSize(R.id.text_data_usage, android.util.TypedValue.COMPLEX_UNIT_SP, sizeData)
- views.setTextColor(R.id.text_data_usage, secondaryColor)
- updateDataUsage(context, views, prefs)
- }
-
- // --- Storage ---
- views.setViewVisibility(R.id.text_storage, if (showStorage) android.view.View.VISIBLE else android.view.View.GONE)
- if (showStorage) {
- views.setTextViewTextSize(R.id.text_storage, android.util.TypedValue.COMPLEX_UNIT_SP, sizeStorage)
- views.setTextColor(R.id.text_storage, secondaryColor)
- updateStorageStats(context, views, prefs)
- }
-
- // --- Step Counter ---
- views.setViewVisibility(R.id.text_steps, if (showSteps) android.view.View.VISIBLE else android.view.View.GONE)
- if (showSteps) {
- views.setTextViewTextSize(R.id.text_steps, android.util.TypedValue.COMPLEX_UNIT_SP, sizeSteps)
- views.setTextColor(R.id.text_steps, secondaryColor)
- loadStepCount(context, views, prefs)
- }
-
- // --- Screen Time ---
- val showScreenTime = prefs.getBoolean("show_screen_time", false)
- val sizeScreenTime = prefs.getFloat("size_screen_time", 14f)
- views.setViewVisibility(R.id.text_screen_time, if (showScreenTime) android.view.View.VISIBLE else android.view.View.GONE)
- if (showScreenTime) {
- views.setTextViewTextSize(R.id.text_screen_time, android.util.TypedValue.COMPLEX_UNIT_SP, sizeScreenTime)
- views.setTextColor(R.id.text_screen_time, secondaryColor)
- updateScreenTime(context, views, prefs)
- }
-
- // --- Dynamic Spacing Logic for Both Sides ---
- fun dpToPx(dp: Float): Int {
- return (dp * context.resources.displayMetrics.density).toInt()
- }
-
- // To precisely manage font intrinsic top padding, we drop the container's top padding to 0
- // and apply a custom top padding precisely computed based on the top item sizes.
- val basePadding = dpToPx(16f)
- views.setViewPadding(R.id.inner_container, basePadding, 0, basePadding, basePadding)
-
- // Left Side: Time or Date or Events
- if (showTime || showWorldClock) {
- val size = if (showTime) sizeTime else sizeWorldClock
- val intrinsicGap = size * 0.18f
- views.setViewPadding(R.id.time_container, 0, maxOf(0, dpToPx(16f - intrinsicGap)), 0, 0)
- views.setViewPadding(R.id.date_container, 0, 0, 0, 0)
- } else if (showDate || showNextAlarm) {
- views.setViewPadding(R.id.time_container, 0, 0, 0, 0)
- val size = if (showDate) sizeDate else sizeNextAlarm
- val intrinsicGap = size * 0.18f
- views.setViewPadding(R.id.date_container, 0, maxOf(0, dpToPx(16f - intrinsicGap)), 0, 0)
- } else {
- views.setViewPadding(R.id.time_container, 0, 0, 0, 0)
- views.setViewPadding(R.id.date_container, 0, 0, 0, 0)
- }
-
- // Events container: add spacing gap below date/time if they exist
- val eventsVisible = showEvents || showTasks
- val leftHasContent = (showTime || showWorldClock || showDate || showNextAlarm)
- if (eventsVisible) {
- val topMargin = if (leftHasContent) dpToPx(8f) else {
- val size = if (showEvents) sizeEvents else sizeTasks
- val intrinsicGap = size * 0.18f
- maxOf(0, dpToPx(16f - intrinsicGap))
- }
- views.setViewPadding(R.id.events_container, 0, topMargin, 0, 0)
- }
-
- // Right Side Stack: ordered by user preference
- data class StackEntry(val viewId: Int, val isVisible: Boolean, val size: Float, val key: String)
-
- val allRightItems = listOf(
- StackEntry(R.id.text_battery, showBattery, sizeBattery, "show_battery"),
- StackEntry(R.id.text_temp, showTemp, sizeTemp, "show_temp"),
- StackEntry(R.id.text_weather_condition, showWeather, sizeWeather, "show_weather_condition"),
- StackEntry(R.id.text_data_usage, showData, sizeData, "show_data_usage"),
- StackEntry(R.id.text_storage, showStorage, sizeStorage, "show_storage"),
- StackEntry(R.id.text_steps, showSteps, sizeSteps, "show_steps"),
- StackEntry(R.id.text_screen_time, showScreenTime, sizeScreenTime, "show_screen_time")
- )
-
- val savedOrder = prefs.getString("widget_right_column_order", "")
- val rightStack = if (savedOrder.isNullOrEmpty()) {
- allRightItems
- } else {
- val orderKeys = savedOrder.split(",")
- val ordered = orderKeys.mapNotNull { k -> allRightItems.find { it.key == k } }
- val remaining = allRightItems.filter { item -> item.key !in orderKeys }
- ordered + remaining
- }
-
- // Position items using explicit padding instead of layout_below
- // Calculate cumulative Y positions for each visible item
- val rightDp = context.resources.displayMetrics.density
- var cumulativeTopDp = 16f // Starting top margin from top of widget
- for (entry in rightStack) {
- if (entry.isVisible) {
- val topPaddingPx = (cumulativeTopDp * rightDp).toInt()
- views.setViewPadding(entry.viewId, 0, topPaddingPx, 0, 0)
- // Advance by this item's height + small gap
- val itemHeightDp = entry.size * 1.2f // approximate line height
- cumulativeTopDp += itemHeightDp + 2f
- }
- }
-
- // --- Click Actions ---
- val clockPackages = listOf("com.android.deskclock", "com.google.android.deskclock", "com.simplemobiletools.clock", "org.fossify.clock")
- val alarmIntent = getBestIntent(context, clockPackages, Intent(android.provider.AlarmClock.ACTION_SHOW_ALARMS))
- val alarmPendingIntent = PendingIntent.getActivity(context, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.clock_time, alarmPendingIntent)
-
- val calendarPackages = listOf("org.fossify.calendar", "com.simplemobiletools.calendar", "com.google.android.calendar", "com.android.calendar")
- val baseCalIntent = Intent(Intent.ACTION_VIEW).apply {
- data = android.net.Uri.parse("content://com.android.calendar/time")
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- }
- val calendarIntent = getBestIntent(context, calendarPackages, baseCalIntent)
- val calendarPendingIntent = PendingIntent.getActivity(context, 1, calendarIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.clock_date, calendarPendingIntent)
-
- val batteryIntent = Intent(Intent.ACTION_POWER_USAGE_SUMMARY)
- val batteryPendingIntent = PendingIntent.getActivity(context, 2, batteryIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.text_battery, batteryPendingIntent)
- views.setOnClickPendingIntent(R.id.text_temp, batteryPendingIntent)
-
- val storageIntent = Intent(android.provider.Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
- val storagePendingIntent = PendingIntent.getActivity(context, 3, storageIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.text_storage, storagePendingIntent)
-
- val dataIntent = Intent(android.provider.Settings.ACTION_DATA_USAGE_SETTINGS)
- val dataPendingIntent = PendingIntent.getActivity(context, 4, dataIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.text_data_usage, dataPendingIntent)
-
- // --- Calendar Events OR Tasks ---
- views.setViewVisibility(R.id.events_container, if (showEvents || showTasks) android.view.View.VISIBLE else android.view.View.GONE)
-
- if (showEvents) {
- loadCalendarEvents(context, views, sizeEvents, primaryColor, secondaryColor)
- } else if (showTasks) {
- loadTasks(context, views, sizeTasks, primaryColor)
- }
-
- // --- Next Alarm ---
- views.setViewVisibility(R.id.text_next_alarm, if (showNextAlarm) android.view.View.VISIBLE else android.view.View.GONE)
- if (showNextAlarm) {
- loadNextAlarm(context, views, sizeNextAlarm, alarmColor)
- }
- // Click action for Next Alarm (same as Clock)
- views.setOnClickPendingIntent(R.id.text_next_alarm, alarmPendingIntent)
-
- val refreshIntent = Intent(context, AwidgetProvider::class.java).apply {
- action = ACTION_BATTERY_UPDATE
- }
- val refreshPendingIntent = PendingIntent.getBroadcast(context, 10, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-
- if (showTasks) {
- val tasksIntent = context.packageManager.getLaunchIntentForPackage("org.tasks")
- if (tasksIntent != null) {
- val tasksPendingIntent = PendingIntent.getActivity(context, 11, tasksIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.events_container, tasksPendingIntent)
- } else {
- views.setOnClickPendingIntent(R.id.events_container, refreshPendingIntent)
- }
- } else {
- views.setOnClickPendingIntent(R.id.events_container, refreshPendingIntent)
- }
-
- val settingsIntent = Intent(context, MainActivity::class.java)
- val settingsPendingIntent = PendingIntent.getActivity(context, 0, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.widget_root, settingsPendingIntent)
-
- appWidgetManager.updateAppWidget(appWidgetId, views)
- }
-
-
- data class EventInfo(val id: Long, val title: String, val begin: Long, val isLocal: Boolean)
-
- private fun fetchCalendarEvents(context: Context): List {
- val syncedCalendarIds = mutableSetOf()
- val visibleCalendarIds = mutableSetOf()
-
- val calSelection = "${android.provider.CalendarContract.Calendars.VISIBLE} = 1"
-
- context.contentResolver.query(
- android.provider.CalendarContract.Calendars.CONTENT_URI,
- arrayOf(
- android.provider.CalendarContract.Calendars._ID,
- android.provider.CalendarContract.Calendars.ACCOUNT_TYPE,
- android.provider.CalendarContract.Calendars.ACCOUNT_NAME,
- android.provider.CalendarContract.Calendars.CALENDAR_DISPLAY_NAME
- ),
- calSelection, null, null
- )?.use { cursor ->
- val idIdx = cursor.getColumnIndex(android.provider.CalendarContract.Calendars._ID)
- val nameIdx = cursor.getColumnIndex(android.provider.CalendarContract.Calendars.ACCOUNT_NAME)
- val displayIdx = cursor.getColumnIndex(android.provider.CalendarContract.Calendars.CALENDAR_DISPLAY_NAME)
- while (cursor.moveToNext()) {
- val calId = cursor.getLong(idIdx)
- val accountName = cursor.getString(nameIdx) ?: ""
- val displayName = cursor.getString(displayIdx) ?: ""
-
- visibleCalendarIds.add(calId)
-
- if (displayName.contains("holiday", ignoreCase = true) ||
- accountName.contains("holiday", ignoreCase = true)) {
- syncedCalendarIds.add(calId)
- }
- }
- }
-
- if (visibleCalendarIds.isEmpty()) return emptyList()
-
- val projection = arrayOf(
- android.provider.CalendarContract.Instances.EVENT_ID,
- android.provider.CalendarContract.Events.TITLE,
- android.provider.CalendarContract.Instances.BEGIN,
- android.provider.CalendarContract.Instances.CALENDAR_ID
- )
-
- val now = System.currentTimeMillis()
- val endQuery = now + android.text.format.DateUtils.DAY_IN_MILLIS * 30
-
- val uri = android.provider.CalendarContract.Instances.CONTENT_URI.buildUpon()
- .appendPath(now.toString())
- .appendPath(endQuery.toString())
- .build()
-
- val idList = visibleCalendarIds.joinToString(",")
- val selection = "${android.provider.CalendarContract.Instances.END} >= ? AND ${android.provider.CalendarContract.Instances.CALENDAR_ID} IN ($idList)"
- val selectionArgs = arrayOf(now.toString())
- val sortOrder = "${android.provider.CalendarContract.Instances.BEGIN} ASC"
-
- val events = mutableListOf()
-
- context.contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->
- val eventIdIdx = cursor.getColumnIndex(android.provider.CalendarContract.Instances.EVENT_ID)
- val titleIdx = cursor.getColumnIndex(android.provider.CalendarContract.Events.TITLE)
- val beginIdx = cursor.getColumnIndex(android.provider.CalendarContract.Instances.BEGIN)
- val calIdIdx = cursor.getColumnIndex(android.provider.CalendarContract.Instances.CALENDAR_ID)
-
- while (cursor.moveToNext() && events.size < 10) {
- val eventId = cursor.getLong(eventIdIdx)
- val title = cursor.getString(titleIdx) ?: "No Title"
- val begin = cursor.getLong(beginIdx)
- val calId = cursor.getLong(calIdIdx)
- val isLocal = !syncedCalendarIds.contains(calId)
- events.add(EventInfo(eventId, title, begin, isLocal))
- }
- }
- return events
- }
-
- private fun bindCalendarEvents(context: Context, views: RemoteViews, events: List, textSizeSp: Float, primaryColor: Int, secondaryColor: Int, eventViews: List) {
- val timeFormatter = DateTimeFormatter.ofPattern("h:mm a", Locale.getDefault())
- val dayFormatter = DateTimeFormatter.ofPattern("EEE", Locale.getDefault())
- val dateFormatter = DateTimeFormatter.ofPattern("d MMM h:mma", Locale.getDefault())
-
- if (events.isEmpty()) {
- views.setTextViewText(eventViews[0], "No events today")
- views.setTextColor(eventViews[0], secondaryColor)
- views.setTextViewTextSize(eventViews[0], android.util.TypedValue.COMPLEX_UNIT_SP, textSizeSp)
- views.setViewVisibility(eventViews[0], android.view.View.VISIBLE)
-
- val emptyIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(eventViews[0], emptyIntent)
-
- for (i in 1 until eventViews.size) {
- views.setViewVisibility(eventViews[i], android.view.View.GONE)
- }
- return
- }
-
- for (i in eventViews.indices) {
- if (i < events.size) {
- val event = events[i]
- val eventTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(event.begin), ZoneId.systemDefault())
- val today = LocalDate.now()
- val tomorrow = today.plusDays(1)
- val oneWeekLater = today.plusWeeks(1)
-
- val timeText = if (eventTime.toLocalDate().isEqual(today)) {
- "Today ${eventTime.format(timeFormatter)}"
- } else if (eventTime.toLocalDate().isEqual(tomorrow)) {
- "Tomorrow ${eventTime.format(timeFormatter)}"
- } else if (eventTime.toLocalDate().isBefore(oneWeekLater)) {
- "${eventTime.format(dayFormatter)} ${eventTime.format(timeFormatter)}"
- } else {
- eventTime.format(dateFormatter)
- }
-
- val fullText = "• $timeText ${event.title}"
- val spannable = SpannableString(fullText)
- val accentColor = context.getColor(R.color.widget_outline)
- spannable.setSpan(ForegroundColorSpan(accentColor), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-
- views.setTextViewText(eventViews[i], spannable)
- views.setTextColor(eventViews[i], if (event.isLocal) primaryColor else secondaryColor)
- views.setTextViewTextSize(eventViews[i], android.util.TypedValue.COMPLEX_UNIT_SP, textSizeSp)
- views.setViewVisibility(eventViews[i], android.view.View.VISIBLE)
-
- val eventIntent = Intent(Intent.ACTION_VIEW).apply {
- data = android.content.ContentUris.withAppendedId(android.provider.CalendarContract.Events.CONTENT_URI, event.id)
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
- }
- val eventPendingIntent = PendingIntent.getActivity(context, event.id.toInt(), eventIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(eventViews[i], eventPendingIntent)
- } else {
- views.setViewVisibility(eventViews[i], android.view.View.GONE)
- }
- }
- }
-
- private fun loadCalendarEvents(context: Context, views: RemoteViews, textSizeSp: Float, primaryColor: Int, secondaryColor: Int) {
- if (androidx.core.content.ContextCompat.checkSelfPermission(
- context, android.Manifest.permission.READ_CALENDAR
- ) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
- return
- }
-
- val eventViews = listOf(
- R.id.text_event_1, R.id.text_event_2, R.id.text_event_3,
- R.id.text_event_4, R.id.text_event_5, R.id.text_event_6,
- R.id.text_event_7, R.id.text_event_8, R.id.text_event_9,
- R.id.text_event_10
- )
-
- try {
- val events = fetchCalendarEvents(context)
- bindCalendarEvents(context, views, events, textSizeSp, primaryColor, secondaryColor, eventViews)
- } catch (e: Exception) {
- // Log and gracefully handle crash
- android.util.Log.e("LWidget", "Error loading calendar events", e)
- for (viewId in eventViews) {
- views.setViewVisibility(viewId, android.view.View.GONE)
- }
- }
- }
-
- private fun updateScreenTime(context: Context, views: RemoteViews, prefs: android.content.SharedPreferences) {
- val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as android.app.AppOpsManager
- val mode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- appOps.unsafeCheckOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.packageName)
- } else {
- appOps.checkOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.packageName)
- }
- if (mode != android.app.AppOpsManager.MODE_ALLOWED) {
- views.setViewVisibility(R.id.text_screen_time, android.view.View.GONE)
- return
- }
-
- val usageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as android.app.usage.UsageStatsManager
-
- val calendar = java.util.Calendar.getInstance()
- calendar.set(java.util.Calendar.HOUR_OF_DAY, 0)
- calendar.set(java.util.Calendar.MINUTE, 0)
- calendar.set(java.util.Calendar.SECOND, 0)
- calendar.set(java.util.Calendar.MILLISECOND, 0)
-
- val startTime = calendar.timeInMillis
- val endTime = System.currentTimeMillis()
-
- val stats = usageStatsManager.queryUsageStats(android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime)
-
- var totalForegroundTime = 0L
- if (stats != null) {
- for (usage in stats) {
- // Only count significant foreground usage correctly reported
- if (usage.totalTimeInForeground > 0) {
- totalForegroundTime += usage.totalTimeInForeground
- }
- }
- }
-
- val isBold = prefs.getBoolean("bold_screen_time", false)
-
- if (totalForegroundTime > 0) {
- val totalMinutes = totalForegroundTime / (1000 * 60)
- val hours = totalMinutes / 60
- val mins = totalMinutes % 60
- val timeString = if (hours > 0) "${hours}h ${mins}m \u23F3" else "${mins}m \u23F3" // ⏳
- val span = android.text.SpannableString(timeString)
- span.setSpan(android.text.style.RelativeSizeSpan(0.75f), span.length - 2, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- if (isBold) span.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- views.setTextViewText(R.id.text_screen_time, span)
- } else {
- val span = android.text.SpannableString("0m \u23F3")
- span.setSpan(android.text.style.RelativeSizeSpan(0.75f), span.length - 2, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- if (isBold) span.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- views.setTextViewText(R.id.text_screen_time, span)
- }
- }
-
- private fun updateDataUsage(context: Context, views: RemoteViews, prefs: android.content.SharedPreferences) {
- val networkStatsManager = context.getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager
- // Use java.time
- val startOfDay = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
- val endTime = System.currentTimeMillis()
-
- try {
- val bucket = networkStatsManager.querySummaryForDevice(
- NetworkCapabilities.TRANSPORT_CELLULAR,
- null,
- startOfDay,
- endTime
- )
-
- val bytes = bucket.rxBytes + bucket.txBytes
- val mb = bytes / (1024f * 1024f)
- val gb = mb / 1024f
-
- val text: CharSequence = if (gb >= 1.0f) {
- val gbStr = String.format("%.2f", gb)
- val span = android.text.SpannableString("$gbStr GB")
- span.setSpan(android.text.style.RelativeSizeSpan(0.5f), gbStr.length, gbStr.length + 3, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) // GB
- span
- } else {
- val mbStr = String.format("%.1f", mb)
- val span = android.text.SpannableString("$mbStr MB")
- span.setSpan(android.text.style.RelativeSizeSpan(0.5f), mbStr.length, mbStr.length + 3, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) // MB
- span
- }
-
- if (prefs.getBoolean("bold_data_usage", false) && text is android.text.SpannableString) {
- text.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, text.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
-
- views.setTextViewText(R.id.text_data_usage, text)
-
- } catch (e: SecurityException) {
- val res = context.resources
- // Assuming R.string.no_perm exists (we created strings.xml)
- views.setTextViewText(R.id.text_data_usage, res.getString(R.string.no_perm))
- } catch (e: Exception) {
- val res = context.resources
- views.setTextViewText(R.id.text_data_usage, res.getString(R.string.error))
- }
- }
-
- private data class TaskData(val title: String, val dueMillis: Long)
-
- private fun fetchActiveTasks(context: Context, limit: Int): List {
- val tasks = mutableListOf()
- val taskUri = android.net.Uri.parse("content://org.tasks/tasks")
- val selection = "completed=0 AND deleted=0"
- try {
- context.contentResolver.query(taskUri, null, selection, null, "dueDate ASC")?.use { cursor ->
- val titleIdx = cursor.getColumnIndex("title")
- val compIdx = cursor.getColumnIndex("completed")
- val delIdx = cursor.getColumnIndex("deleted")
- val dueIdx = cursor.getColumnIndex("dueDate")
-
- if (titleIdx == -1) return emptyList()
-
- while (cursor.moveToNext() && tasks.size < limit) {
- val completed = if (compIdx >= 0) cursor.getString(compIdx) else null
- val deleted = if (delIdx >= 0) cursor.getString(delIdx) else null
- val dueMillis = if (dueIdx >= 0) cursor.getLong(dueIdx) else 0L
-
- val isCompleted = completed != null && completed != "0"
- val isDeleted = deleted != null && deleted != "0"
-
- if (isCompleted || isDeleted) {
- continue
- }
-
- val title = cursor.getString(titleIdx) ?: "No Title"
- tasks.add(TaskData(title, dueMillis))
- }
- }
- } catch (e: Exception) {
- // Return empty list on failure
- }
- return tasks
- }
-
- private fun formatDueSuffix(dueMillis: Long): String {
- if (dueMillis <= 0) return ""
- val dueDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(dueMillis), ZoneId.systemDefault()).toLocalDate()
- val today = LocalDate.now()
- val tomorrow = today.plusDays(1)
-
- return if (dueDate.isBefore(today)) {
- " (Overdue)"
- } else if (dueDate.isEqual(today)) {
- " (Today)"
- } else if (dueDate.isEqual(tomorrow)) {
- " (Tomorrow)"
- } else {
- val df = DateTimeFormatter.ofPattern("MMM d", Locale.getDefault())
- " (${dueDate.format(df)})"
- }
- }
-
- private fun loadTasks(context: Context, views: RemoteViews, textSizeSp: Float, primaryColor: Int) {
- val eventViews = listOf(
- R.id.text_event_1, R.id.text_event_2, R.id.text_event_3,
- R.id.text_event_4, R.id.text_event_5, R.id.text_event_6,
- R.id.text_event_7, R.id.text_event_8, R.id.text_event_9,
- R.id.text_event_10
- )
-
- // Debugging: Check permission again contextually
- val hasPerm = context.checkSelfPermission(PERMISSION_READ_TASKS_ORG) == android.content.pm.PackageManager.PERMISSION_GRANTED ||
- context.checkSelfPermission(PERMISSION_READ_TASKS_ASTRID) == android.content.pm.PackageManager.PERMISSION_GRANTED
-
- if (!hasPerm) {
- views.setTextViewText(eventViews[0], "Missing Permission")
- views.setViewVisibility(eventViews[0], android.view.View.VISIBLE)
- for (j in 1 until eventViews.size) {
- views.setViewVisibility(eventViews[j], android.view.View.GONE)
- }
- return
- }
-
- val tasks = fetchActiveTasks(context, eventViews.size)
-
- if (tasks.isEmpty()) {
- for (viewId in eventViews) {
- views.setViewVisibility(viewId, android.view.View.GONE)
- }
- return
- }
-
- for (i in tasks.indices) {
- val task = tasks[i]
- val dueSuffix = formatDueSuffix(task.dueMillis)
- val fullText = "• ${task.title}$dueSuffix"
- val spannable = SpannableString(fullText)
- val accentColor = context.getColor(R.color.widget_outline)
- spannable.setSpan(ForegroundColorSpan(accentColor), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-
- views.setTextViewText(eventViews[i], spannable)
- views.setTextColor(eventViews[i], primaryColor)
- views.setTextViewTextSize(eventViews[i], android.util.TypedValue.COMPLEX_UNIT_SP, textSizeSp)
- views.setViewVisibility(eventViews[i], android.view.View.VISIBLE)
-
- val taskIntent = context.packageManager.getLaunchIntentForPackage("org.tasks")
- if (taskIntent != null) {
- val taskPendingIntent = PendingIntent.getActivity(context, 1000 + i, taskIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(eventViews[i], taskPendingIntent)
- }
- }
-
- for (j in tasks.size until eventViews.size) {
- views.setViewVisibility(eventViews[j], android.view.View.GONE)
- }
- }
-
- private fun loadWorldClock(views: RemoteViews, textSizeSp: Float, textColor: Int, zoneIdStr: String, is12Hour: Boolean) {
- try {
- val zoneId = ZoneId.of(zoneIdStr)
- val zdt = java.time.ZonedDateTime.now(zoneId)
- val pattern = if (is12Hour) "h:mm a" else "H:mm"
- val formatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault())
- val timeStr = zdt.format(formatter)
-
- // Format: "🌍 10:30 AM"
- val text = "\uD83C\uDF0D $timeStr"
- views.setTextViewText(R.id.text_world_clock, text)
- views.setTextViewTextSize(R.id.text_world_clock, android.util.TypedValue.COMPLEX_UNIT_SP, textSizeSp)
- views.setTextColor(R.id.text_world_clock, textColor)
- views.setViewVisibility(R.id.text_world_clock, android.view.View.VISIBLE)
-
- } catch (e: Exception) {
- views.setViewVisibility(R.id.text_world_clock, android.view.View.GONE)
- }
- }
-
- private fun loadNextAlarm(context: Context, views: RemoteViews, textSizeSp: Float, textColor: Int) {
- val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
- val nextAlarm = alarmManager.nextAlarmClock
-
- if (nextAlarm != null) {
- val nextAlarmTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(nextAlarm.triggerTime), ZoneId.systemDefault())
- val timeFormatter = DateTimeFormatter.ofPattern("h:mm a", Locale.getDefault())
- val timeText = nextAlarmTime.format(timeFormatter)
-
- // Format: "| ⏰ 7:00 AM"
- val fullText = "| ⏰ $timeText"
- views.setTextViewText(R.id.text_next_alarm, fullText)
- views.setTextViewTextSize(R.id.text_next_alarm, android.util.TypedValue.COMPLEX_UNIT_SP, textSizeSp)
- views.setTextColor(R.id.text_next_alarm, textColor)
- views.setViewVisibility(R.id.text_next_alarm, android.view.View.VISIBLE)
- } else {
- views.setViewVisibility(R.id.text_next_alarm, android.view.View.GONE)
- }
- }
-
- private fun updateStorageStats(context: Context, views: RemoteViews, prefs: android.content.SharedPreferences) {
- try {
- val path = android.os.Environment.getDataDirectory()
- val stat = android.os.StatFs(path.path)
- val freeBytes = stat.availableBlocksLong * stat.blockSizeLong
-
- val gb = freeBytes / (1024f * 1024f * 1024f)
-
- val gbStr = String.format("%.0f", gb)
- val span = android.text.SpannableString("$gbStr GB")
- span.setSpan(android.text.style.RelativeSizeSpan(0.5f), gbStr.length, gbStr.length + 3, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) // GB
-
- if (prefs.getBoolean("bold_storage", false)) {
- span.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
-
- views.setTextViewText(R.id.text_storage, span)
- } catch (e: Exception) {
- views.setTextViewText(R.id.text_storage, "Err")
- }
- }
-
- private fun loadStepCount(context: Context, views: RemoteViews, prefs: android.content.SharedPreferences) {
- try {
- val totalSteps = prefs.getFloat("last_total_steps", 0f)
- val baselineSteps = prefs.getFloat("step_baseline", 0f)
-
- val dailySteps = (totalSteps - baselineSteps).toInt().coerceAtLeast(0)
- val span = android.text.SpannableString("$dailySteps \uD83D\uDC5F") // 👟 outline sneaker
- span.setSpan(android.text.style.RelativeSizeSpan(0.75f), span.length - 2, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-
- if (prefs.getBoolean("bold_steps", false)) {
- span.setSpan(android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, span.length, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
-
- views.setTextViewText(R.id.text_steps, span)
- } catch (e: Exception) {
- // Fallback display if an exception occurs
- views.setTextViewText(R.id.text_steps, "Err")
- }
- }
-
- private fun getBestIntent(context: Context, packages: List, fallback: Intent): Intent {
- val pm = context.packageManager
- for (pkg in packages) {
- val intent = pm.getLaunchIntentForPackage(pkg)
- if (intent != null) {
- return intent
- }
- }
- return fallback
- }
- }
-}
diff --git a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt b/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
index 56ba133..940fc69 100644
--- a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
+++ b/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
@@ -108,6 +108,9 @@ class MainActivity : AppCompatActivity() {
setupSections()
updateLivePreview()
+ // Advanced Section
+ bindSlider(R.id.row_update_interval, "Update Interval (m)", "update_interval", 15f, 1f, 60f, "m")
+
// Setup Changelog
val versionName = try {
packageManager.getPackageInfo(packageName, 0).versionName
@@ -117,26 +120,6 @@ class MainActivity : AppCompatActivity() {
val tvVersion = findViewById(R.id.tv_changelog_version)
tvVersion.text = getString(R.string.changelog_version, versionName)
- val cardChangelog = findViewById(R.id.card_changelog)
- val changelogContent = findViewById(R.id.changelog_expandable_content)
- val ivChangelogExpand = findViewById(R.id.iv_changelog_expand)
- cardChangelog.setOnClickListener {
- val isCurrentlyVisible = changelogContent.visibility == View.VISIBLE
- changelogContent.visibility = if (isCurrentlyVisible) View.GONE else View.VISIBLE
- ivChangelogExpand.animate().rotation(if (isCurrentlyVisible) 0f else 180f).setDuration(200).start()
- }
-
- // Prevent parent scroll when touching the inner changelog scroll area
- findViewById(R.id.changelog_scroll).setOnTouchListener { v, event ->
- when (event.action) {
- android.view.MotionEvent.ACTION_DOWN, android.view.MotionEvent.ACTION_MOVE ->
- v.parent.requestDisallowInterceptTouchEvent(true)
- android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL ->
- v.parent.requestDisallowInterceptTouchEvent(false)
- }
- false
- }
-
findViewById(R.id.tv_github_link).setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://github.com/LeanBitLab/Lwidget"))
startActivity(intent)
@@ -201,11 +184,11 @@ class MainActivity : AppCompatActivity() {
}
private fun checkAllPermissions() {
- val cardPermissionList = findViewById(R.id.card_permission_list)
var widgetNeedsUpdate = false
// Check Calendar
- if (prefs.getBoolean("show_events", false) && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
+ val calMissing = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED
+ if (prefs.getBoolean("show_events", false) && calMissing) {
prefs.edit().putBoolean("show_events", false).apply()
findViewById(R.id.row_events_toggle).findViewById(R.id.row_switch).isChecked = false
findViewById(R.id.row_events_size).visibility = View.GONE
@@ -213,7 +196,8 @@ class MainActivity : AppCompatActivity() {
}
// Check Tasks
- if (prefs.getBoolean("show_tasks", false) && ContextCompat.checkSelfPermission(this, AwidgetProvider.PERMISSION_READ_TASKS_ORG) != PackageManager.PERMISSION_GRANTED) {
+ val tasksMissing = ContextCompat.checkSelfPermission(this, AwidgetProvider.PERMISSION_READ_TASKS_ORG) != PackageManager.PERMISSION_GRANTED
+ if (prefs.getBoolean("show_tasks", false) && tasksMissing) {
prefs.edit().putBoolean("show_tasks", false).apply()
findViewById(R.id.row_tasks_toggle).findViewById(R.id.row_switch).isChecked = false
findViewById(R.id.row_tasks_size).visibility = View.GONE
@@ -222,15 +206,13 @@ class MainActivity : AppCompatActivity() {
// Check Steps
var stepMissing = false
- if (prefs.getBoolean("show_steps", false)) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
- stepMissing = true
- }
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- stepMissing = true
- }
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
+ stepMissing = true
}
- if (stepMissing) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ stepMissing = true
+ }
+ if (prefs.getBoolean("show_steps", false) && stepMissing) {
prefs.edit().putBoolean("show_steps", false).apply()
findViewById(R.id.row_steps_toggle).findViewById(R.id.row_switch).isChecked = false
findViewById(R.id.row_steps_size).visibility = View.GONE
@@ -238,7 +220,8 @@ class MainActivity : AppCompatActivity() {
}
// Check Screen Time
- if (prefs.getBoolean("show_screen_time", false) && !hasUsageStatsPermission()) {
+ val usageMissing = !hasUsageStatsPermission()
+ if (prefs.getBoolean("show_screen_time", false) && usageMissing) {
prefs.edit().putBoolean("show_screen_time", false).apply()
findViewById(R.id.row_screen_time_toggle).findViewById(R.id.row_switch).isChecked = false
findViewById(R.id.row_screen_time_size).visibility = View.GONE
@@ -246,7 +229,7 @@ class MainActivity : AppCompatActivity() {
}
// Check Data Usage
- if (prefs.getBoolean("show_data_usage", false) && !hasUsageStatsPermission()) {
+ if (prefs.getBoolean("show_data_usage", false) && usageMissing) {
prefs.edit().putBoolean("show_data_usage", false).apply()
findViewById(R.id.row_data_toggle).findViewById(R.id.row_switch).isChecked = false
findViewById(R.id.row_data_size).visibility = View.GONE
@@ -254,21 +237,81 @@ class MainActivity : AppCompatActivity() {
}
// Check Breezy Weather
- if (prefs.getBoolean("show_weather_condition", false)) {
- if (!isAppInstalled("org.breezyweather") || ContextCompat.checkSelfPermission(this, "org.breezyweather.READ_PROVIDER") != PackageManager.PERMISSION_GRANTED) {
- prefs.edit().putBoolean("show_weather_condition", false).apply()
- findViewById(R.id.row_weather_toggle).findViewById(R.id.row_switch).isChecked = false
- findViewById(R.id.row_weather_size).visibility = View.GONE
- widgetNeedsUpdate = true
- }
+ val weatherMissing = !isAppInstalled("org.breezyweather") || ContextCompat.checkSelfPermission(this, "org.breezyweather.READ_PROVIDER") != PackageManager.PERMISSION_GRANTED
+ if (prefs.getBoolean("show_weather_condition", false) && weatherMissing) {
+ prefs.edit().putBoolean("show_weather_condition", false).apply()
+ findViewById(R.id.row_weather_toggle).findViewById(R.id.row_switch).isChecked = false
+ findViewById(R.id.row_weather_size).visibility = View.GONE
+ widgetNeedsUpdate = true
}
- cardPermissionList.visibility = View.GONE
-
if (widgetNeedsUpdate) {
updateWidget()
updateToggleAvailability()
}
+
+ // Update Permission Toggles
+ updatePermissionToggle(R.id.row_perm_calendar, "Calendar Events", !calMissing) {
+ if (calMissing) ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CALENDAR), 100)
+ else openAppSettings()
+ }
+ updatePermissionToggle(R.id.row_perm_tasks, "Tasks", !tasksMissing) {
+ if (tasksMissing) ActivityCompat.requestPermissions(this, arrayOf(AwidgetProvider.PERMISSION_READ_TASKS_ORG), 101)
+ else openAppSettings()
+ }
+ updatePermissionToggle(R.id.row_perm_steps, "Step Counter", !stepMissing) {
+ if (stepMissing) {
+ val neededPermissions = mutableListOf()
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
+ neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION)
+ }
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ ActivityCompat.requestPermissions(this, neededPermissions.toTypedArray(), 102)
+ } else openAppSettings()
+ }
+ updatePermissionToggle(R.id.row_perm_data_usage, "Data Usage", !usageMissing) {
+ try { startActivity(Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS)) } catch (e: Exception) {}
+ }
+ updatePermissionToggle(R.id.row_perm_screen_time, "Screen Time", !usageMissing) {
+ try { startActivity(Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS)) } catch (e: Exception) {}
+ }
+ updatePermissionToggle(R.id.row_perm_weather, "Weather", !weatherMissing) {
+ if (weatherMissing && !isAppInstalled("org.breezyweather")) {
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse("market://details?id=org.breezyweather")))
+ } catch (e: Exception) {
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://f-droid.org/packages/org.breezyweather/")))
+ } catch (e2: Exception) {}
+ }
+ } else if (weatherMissing) {
+ ActivityCompat.requestPermissions(this, arrayOf("org.breezyweather.READ_PROVIDER"), 104)
+ } else openAppSettings()
+ }
+ }
+
+ private fun updatePermissionToggle(viewId: Int, label: String, isGranted: Boolean, onClick: () -> Unit) {
+ val row = findViewById(viewId)
+ if (row != null) {
+ row.findViewById(R.id.row_label).text = label
+ val switchView = row.findViewById(R.id.row_switch)
+ switchView.setOnCheckedChangeListener(null)
+ switchView.isChecked = isGranted
+
+ row.setOnClickListener { onClick() }
+ switchView.setOnClickListener {
+ switchView.isChecked = isGranted // Revert instantly
+ onClick()
+ }
+ }
+ }
+
+ private fun openAppSettings() {
+ val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = android.net.Uri.parse("package:" + packageName)
+ startActivity(intent)
}
override fun onResume() {
@@ -756,6 +799,44 @@ class MainActivity : AppCompatActivity() {
}
+
+ private fun bindCategoryFoldable(headerId: Int, contentId: Int, title: String, iconResId: Int, prefKey: String) {
+ accordionViews[prefKey] = findViewById(contentId)
+ accordionHeaders[prefKey] = findViewById(headerId)
+
+ val header = findViewById(headerId)
+ header.findViewById(R.id.header_title).text = title
+ val headerIcon = header.findViewById(R.id.header_icon)
+ if (iconResId != 0) {
+ headerIcon.setImageResource(iconResId)
+ headerIcon.visibility = View.VISIBLE
+ } else {
+ headerIcon.visibility = View.GONE
+ }
+
+ val content = findViewById(contentId)
+ val chevron = header.findViewById(R.id.header_chevron)
+ val isExpanded = prefs.getBoolean(prefKey, false)
+ content.visibility = if (isExpanded) View.VISIBLE else View.GONE
+ chevron.rotation = if (isExpanded) 180f else 0f
+
+ header.setOnClickListener {
+ val nowExpanded = content.visibility != View.VISIBLE
+ if (nowExpanded) {
+ collapseAllExcept(prefKey)
+ content.visibility = View.VISIBLE
+ prefs.edit().putBoolean(prefKey, true).apply()
+ } else {
+ content.visibility = View.GONE
+ prefs.edit().putBoolean(prefKey, false).apply()
+ }
+ android.animation.ObjectAnimator.ofFloat(chevron, "rotation", if (nowExpanded) 180f else 0f).apply {
+ duration = 300
+ start()
+ }
+ }
+ }
+
private fun setupSections() {
contentSwitches.clear()
@@ -778,6 +859,11 @@ class MainActivity : AppCompatActivity() {
setupKeepAliveSection()
setupEventsAndTasksSections()
setupThemeSection(colorOptions)
+
+ // System sections
+ bindCategoryFoldable(R.id.header_advanced, R.id.content_advanced, "Advanced", 0, "section_advanced_expanded")
+ bindCategoryFoldable(R.id.header_permissions, R.id.content_permissions, "Permissions", 0, "section_permissions_expanded")
+ bindCategoryFoldable(R.id.header_about, R.id.content_about, "About", 0, "section_about_expanded")
}
private fun setupTimeSection(timeFormatOptions: List) {
@@ -1043,29 +1129,39 @@ class MainActivity : AppCompatActivity() {
}
}
private fun setupKeepAliveSection() {
- // Keep Alive
- val keepAliveSwitch = bindFoldedSection(
- R.id.header_keep_alive, R.drawable.ic_alarm, getString(R.string.section_keep_alive),
- R.id.content_keep_alive, R.id.row_keep_alive_toggle,
- "keep_alive", false
- )
-
- keepAliveSwitch.setOnCheckedChangeListener { _, isChecked ->
+ // Keep Alive (now inside Advanced section, not folded)
+ bindToggle(R.id.row_keep_alive_toggle, "Enable Keep Alive", "keep_alive", false) { isChecked ->
if (isChecked) {
+ val neededPermissions = mutableListOf()
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
+ neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION)
+ }
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 104)
- keepAliveSwitch.isChecked = false
- return@setOnCheckedChangeListener
+ neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ if (neededPermissions.isNotEmpty()) {
+ val switch = findViewById(R.id.row_keep_alive_toggle).findViewById(R.id.row_switch)
+ switch.isChecked = false
+ ActivityCompat.requestPermissions(this, neededPermissions.toTypedArray(), 105)
+ return@bindToggle
}
}
prefs.edit().putBoolean("keep_alive", isChecked).apply()
- updateFeatureRowVisibility(keepAliveSwitch, isChecked)
val showSteps = prefs.getBoolean("show_steps", false)
val serviceIntent = Intent(this, StepCounterService::class.java)
- if (isChecked || showSteps) { startForegroundService(serviceIntent) }
- else { stopService(serviceIntent) }
- updateWidget()
+ val hasActivityPerm = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q ||
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_GRANTED
+ if ((isChecked || showSteps) && hasActivityPerm) {
+ try {
+ ContextCompat.startForegroundService(this, serviceIntent)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else {
+ stopService(serviceIntent)
+ }
}
}
private fun setupEventsAndTasksSections() {
@@ -1482,7 +1578,7 @@ class MainActivity : AppCompatActivity() {
private fun bindSlider(
viewId: Int, title: String, prefKey: String, defValue: Float,
- minValue: Float, maxValue: Float
+ minValue: Float, maxValue: Float, suffix: String = "%"
) {
val row = findViewById(viewId)
val tvTitle = row.findViewById(R.id.row_label)
@@ -1495,11 +1591,11 @@ class MainActivity : AppCompatActivity() {
slider.valueFrom = minValue
slider.valueTo = maxValue
slider.value = currentValue.coerceIn(minValue, maxValue)
- tvValue.text = "${currentValue.toInt()}%"
+ tvValue.text = "${currentValue.toInt()}$suffix"
slider.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
- tvValue.text = "${value.toInt()}%"
+ tvValue.text = "${value.toInt()}$suffix"
prefs.edit().putFloat(prefKey, value).apply()
updateWidget()
}
diff --git a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt.orig b/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt.orig
deleted file mode 100644
index ccf949a..0000000
--- a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt.orig
+++ /dev/null
@@ -1,1506 +0,0 @@
-/*
- * Copyright (C) 2026 LeanBitLab
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.leanbitlab.lwidget
-
-import android.Manifest
-import android.appwidget.AppWidgetManager
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.pm.PackageManager
-import android.os.Bundle
-import android.view.View
-import android.widget.AutoCompleteTextView
-import android.widget.ListView
-import android.widget.TextView
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updateLayoutParams
-import android.view.ViewGroup
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.card.MaterialCardView
-import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
-import com.google.android.material.slider.Slider
-import com.google.android.material.switchmaterial.SwitchMaterial
-
-// Data class for reorderable items
-data class ReorderItem(
- val key: String, // e.g. "show_battery"
- val label: String, // e.g. "Battery"
- var enabled: Boolean
-)
-
-// Adapter for reorder RecyclerView
-class ReorderAdapter(
- private val items: MutableList,
- private val onOrderChanged: () -> Unit
-) : RecyclerView.Adapter() {
-
- class ViewHolder(val view: android.view.View) : RecyclerView.ViewHolder(view) {
- val handle: android.widget.ImageView = view.findViewById(R.id.reorder_handle)
- val name: TextView = view.findViewById(R.id.reorder_item_name)
- val enabled: SwitchMaterial = view.findViewById(R.id.reorder_item_enabled)
- }
-
- override fun onCreateViewHolder(parent: android.view.ViewGroup, viewType: Int): ViewHolder {
- val view = android.view.LayoutInflater.from(parent.context)
- .inflate(R.layout.settings_reorder_item, parent, false)
- return ViewHolder(view)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val item = items[position]
- holder.name.text = item.label
- holder.enabled.isChecked = item.enabled
- }
-
- override fun getItemCount() = items.size
-
- fun moveItem(from: Int, to: Int) {
- val moved = items.removeAt(from)
- items.add(to, moved)
- notifyItemMoved(from, to)
- onOrderChanged()
- }
-}
-
-class MainActivity : AppCompatActivity() {
-
- private lateinit var prefs: SharedPreferences
- private val contentSwitches = mutableListOf()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- com.google.android.material.color.DynamicColors.applyToActivityIfAvailable(this)
- setContentView(R.layout.activity_main)
-
- prefs = getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
-
- if (prefs.getBoolean("is_first_launch", true)) {
- startActivity(Intent(this, SetupActivity::class.java))
- finish()
- return
- }
-
- checkAllPermissions()
- setupSections()
-
- // Setup Changelog
- val versionName = try {
- packageManager.getPackageInfo(packageName, 0).versionName
- } catch (e: Exception) {
- "Unknown"
- }
- val tvVersion = findViewById(R.id.tv_changelog_version)
- tvVersion.text = getString(R.string.changelog_version, versionName)
-
- val cardChangelog = findViewById(R.id.card_changelog)
- val changelogContent = findViewById(R.id.changelog_expandable_content)
- val ivChangelogExpand = findViewById(R.id.iv_changelog_expand)
- cardChangelog.setOnClickListener {
- val isCurrentlyVisible = changelogContent.visibility == View.VISIBLE
- changelogContent.visibility = if (isCurrentlyVisible) View.GONE else View.VISIBLE
- ivChangelogExpand.animate().rotation(if (isCurrentlyVisible) 0f else 180f).setDuration(200).start()
- }
-
- // Prevent parent scroll when touching the inner changelog scroll area
- findViewById(R.id.changelog_scroll).setOnTouchListener { v, event ->
- when (event.action) {
- android.view.MotionEvent.ACTION_DOWN, android.view.MotionEvent.ACTION_MOVE ->
- v.parent.requestDisallowInterceptTouchEvent(true)
- android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL ->
- v.parent.requestDisallowInterceptTouchEvent(false)
- }
- false
- }
-
- findViewById(R.id.tv_github_link).setOnClickListener {
- val intent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://github.com/LeanBitLab/Lwidget"))
- startActivity(intent)
- }
-
- findViewById(R.id.tv_privacy_policy).setOnClickListener {
- val intent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://github.com/LeanBitLab/Lwidget/wiki/Privacy-Policy"))
- startActivity(intent)
- }
-
-
- val fab = findViewById(R.id.fab_update)
- fab.setOnClickListener {
- updateWidget()
- }
-
- // Apply navigation bar insets to FAB so it doesn't overlap gesture nav
- ViewCompat.setOnApplyWindowInsetsListener(fab) { view, windowInsets ->
- val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- view.updateLayoutParams {
- bottomMargin = insets.bottom + (24 * resources.displayMetrics.density).toInt()
- rightMargin = insets.right + (24 * resources.displayMetrics.density).toInt()
- }
- windowInsets
- }
-
- // Handle Collapsing Toolbar Title Fade and Header Fade
- val appBar = findViewById(R.id.app_bar)
- val titleApp = findViewById(R.id.title_app)
- val expandedHeader = findViewById(R.id.header_expanded)
-
- appBar.addOnOffsetChangedListener(com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
- val totalScrollRange = appBar.totalScrollRange
- val percentage = kotlin.math.abs(verticalOffset).toFloat() / totalScrollRange.toFloat()
-
- // Fade in toolbar title when nearing collapse (e.g. last 20% of scroll)
- val alphaTitle = ((percentage - 0.8f) / 0.2f).coerceIn(0f, 1f)
- titleApp.alpha = alphaTitle
-
- // Fade out expanded header as we scroll up (first 50% of scroll)
- // Starts dense (1f) and fades to 0f by the time we are halfway collapsed
- val alphaHeader = (1f - (percentage / 0.5f)).coerceIn(0f, 1f)
- expandedHeader.alpha = alphaHeader
- // Optional: Scale down slightly for a nicer effect
- val scale = (1f - (percentage * 0.1f)).coerceIn(0.9f, 1f)
- expandedHeader.scaleX = scale
- expandedHeader.scaleY = scale
- })
- }
-
- private fun checkAllPermissions() {
- val cardPermissionList = findViewById(R.id.card_permission_list)
- var widgetNeedsUpdate = false
-
- // Check Calendar
- if (prefs.getBoolean("show_events", false) && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
- prefs.edit().putBoolean("show_events", false).apply()
- findViewById(R.id.row_events_toggle).findViewById(R.id.row_switch).isChecked = false
- findViewById(R.id.row_events_size).visibility = View.GONE
- widgetNeedsUpdate = true
- }
-
- // Check Tasks
- if (prefs.getBoolean("show_tasks", false) && ContextCompat.checkSelfPermission(this, AwidgetProvider.PERMISSION_READ_TASKS_ORG) != PackageManager.PERMISSION_GRANTED) {
- prefs.edit().putBoolean("show_tasks", false).apply()
- findViewById(R.id.row_tasks_toggle).findViewById(R.id.row_switch).isChecked = false
- findViewById(R.id.row_tasks_size).visibility = View.GONE
- widgetNeedsUpdate = true
- }
-
- // Check Steps
- var stepMissing = false
- if (prefs.getBoolean("show_steps", false)) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
- stepMissing = true
- }
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- stepMissing = true
- }
- }
- if (stepMissing) {
- prefs.edit().putBoolean("show_steps", false).apply()
- findViewById(R.id.row_steps_toggle).findViewById(R.id.row_switch).isChecked = false
- findViewById(R.id.row_steps_size).visibility = View.GONE
- widgetNeedsUpdate = true
- }
-
- // Check Screen Time
- if (prefs.getBoolean("show_screen_time", false) && !hasUsageStatsPermission()) {
- prefs.edit().putBoolean("show_screen_time", false).apply()
- findViewById(R.id.row_screen_time_toggle).findViewById(R.id.row_switch).isChecked = false
- findViewById(R.id.row_screen_time_size).visibility = View.GONE
- widgetNeedsUpdate = true
- }
-
- // Check Data Usage
- if (prefs.getBoolean("show_data_usage", false) && !hasUsageStatsPermission()) {
- prefs.edit().putBoolean("show_data_usage", false).apply()
- findViewById(R.id.row_data_toggle).findViewById(R.id.row_switch).isChecked = false
- findViewById(R.id.row_data_size).visibility = View.GONE
- widgetNeedsUpdate = true
- }
-
- // Check Breezy Weather
- if (prefs.getBoolean("show_weather_condition", false)) {
- if (!isAppInstalled("org.breezyweather") || ContextCompat.checkSelfPermission(this, "org.breezyweather.READ_PROVIDER") != PackageManager.PERMISSION_GRANTED) {
- prefs.edit().putBoolean("show_weather_condition", false).apply()
- findViewById(R.id.row_weather_toggle).findViewById(R.id.row_switch).isChecked = false
- findViewById(R.id.row_weather_size).visibility = View.GONE
- widgetNeedsUpdate = true
- }
- }
-
- cardPermissionList.visibility = View.GONE
-
- if (widgetNeedsUpdate) {
- updateWidget()
- updateToggleAvailability()
- }
- }
-
- override fun onResume() {
- super.onResume()
- // Re-check permissions when returning (especially for Data Usage settings)
- checkAllPermissions()
- // Force a full widget update every time the app is opened
- updateWidget()
- }
-
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- checkAllPermissions()
- if (requestCode == 101) {
- // Task permission result - trigger update
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- updateWidget()
- }
- } else if (requestCode == 103) {
- // Breezy Weather permission result
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- prefs.edit().putBoolean("show_weather_condition", true).apply()
- findViewById(R.id.row_weather_toggle).findViewById(R.id.row_switch).isChecked = true
- findViewById(R.id.row_weather_size).visibility = View.VISIBLE
- updateWidget()
- updateToggleAvailability()
-
- // Show Gadgetbridge module prompt
- android.app.AlertDialog.Builder(this)
- .setTitle("Important Step")
- .setMessage("If the weather doesn't show up on your widget soon:\n\nOpen Breezy Weather → Settings → External Modules → Enable 'Send Gadgetbridge Data' & toggle on 'Lwidget'.")
- .setPositiveButton("Got it", null)
- .show()
- }
- }
- }
-
- // ===== FOLDED SECTION HELPERS =====
-
- // Top-level accordion: feature cards (Time, Battery, Appearance, etc.)
- private val accordionViews = mutableMapOf()
- private val accordionHeaders = mutableMapOf()
- // Nested sections: own accordion among themselves
- private val nestedViews = mutableMapOf()
- private val nestedHeaders = mutableMapOf()
-
- private fun collapseAllExcept(exceptKey: String) {
- accordionViews.forEach { (key, view) ->
- if (key != exceptKey && view.visibility == View.VISIBLE) {
- // Extract section name from key like "section_world_clock_expanded"
- val sectionName = key.replace("section_", "").replace("_expanded", "")
- collapseSectionNestedContent(sectionName)
- view.visibility = View.GONE
- prefs.edit().putBoolean(key, false).apply()
- accordionHeaders[key]?.let { resetChevron(it) }
- }
- }
- dismissKeyboard()
- }
-
- private fun collapseNestedExcept(exceptKey: String) {
- nestedViews.forEach { (key, view) ->
- if (key != exceptKey && view.visibility == View.VISIBLE) {
- view.visibility = View.GONE
- prefs.edit().putBoolean(key, false).apply()
- nestedHeaders[key]?.let { resetChevron(it) }
- }
- }
- }
-
- private fun resetChevron(header: View) {
- val chevron = header.findViewById(R.id.header_chevron)
- ?: header.findViewById(R.id.header_chevron_appearance_outline)
- ?: header.findViewById(R.id.header_chevron_appearance_colors)
- ?: header.findViewById(R.id.header_chevron_appearance_theme)
- ?: header.findViewById(R.id.header_chevron_appearance_font)
- ?: header.findViewById(R.id.header_chevron_appearance_transparency)
- chevron?.rotation = 0f
- }
-
- private fun bindFoldedSection(
- headerId: Int, iconResId: Int?, title: String,
- contentId: Int,
- toggleRowId: Int,
- prefShowKey: String, defShow: Boolean,
- sizeRowId: Int? = null, prefSizeKey: String? = null,
- defSize: Float = 14f, minSize: Float = 10f, maxSize: Float = 72f,
- selectorRowId: Int? = null, selectorOptions: List? = null,
- prefSelectorKey: String? = null, defSelectorIdx: Int = 0,
- isContent: Boolean = false,
- subSettingsContainerId: Int? = null,
- validateToggle: ((Boolean) -> Boolean)? = null,
- onChanged: ((Boolean) -> Unit)? = null
- ): SwitchMaterial {
- val header = findViewById(headerId)
- val chevron = header.findViewById(R.id.header_chevron)
- val headerIcon = header.findViewById(R.id.header_icon)
- val headerTitle = header.findViewById(R.id.header_title)
- val content = findViewById(contentId)
-
- val sectionKey = prefShowKey.replace("show_", "")
- val expandedPrefKey = "section_${sectionKey}_expanded"
- accordionViews[expandedPrefKey] = content
- accordionHeaders[expandedPrefKey] = header
-
- headerTitle.text = title
- if (iconResId != null) {
- headerIcon.setImageResource(iconResId)
- headerIcon.visibility = View.VISIBLE
- } else {
- headerIcon.visibility = View.GONE
- }
-
- // Expand/collapse - read from prefs, apply visibility
- val isExpandedFromPrefs = prefs.getBoolean(expandedPrefKey, false)
- content.visibility = if (isExpandedFromPrefs) View.VISIBLE else View.GONE
- chevron.rotation = if (isExpandedFromPrefs) 180f else 0f
-
- // Header click: expand this one and collapse all others
- header.setOnClickListener {
- val nowExpanded = content.visibility != View.VISIBLE
- if (nowExpanded) {
- collapseAllExcept(expandedPrefKey)
- content.visibility = View.VISIBLE
- prefs.edit().putBoolean(expandedPrefKey, true).apply()
- } else {
- // Collapsing - dismiss keyboard and close nested subsections
- collapseSectionNestedContent(sectionKey)
- content.visibility = View.GONE
- prefs.edit().putBoolean(expandedPrefKey, false).apply()
- }
- android.animation.ObjectAnimator.ofFloat(chevron, "rotation", if (nowExpanded) 180f else 0f).apply {
- duration = 300
- start()
- }
- }
-
- // Toggle row
- val toggleRow = findViewById(toggleRowId)
- val toggleSwitch = toggleRow.findViewById(R.id.row_switch)
- val toggleLabel = toggleRow.findViewById(R.id.row_label)
- val toggleCard = toggleRow.findViewById(R.id.toggle_row_card)
- toggleLabel.text = "Enable"
-
- if (isContent) contentSwitches.add(toggleSwitch)
-
- val isShown = prefs.getBoolean(prefShowKey, defShow)
- toggleSwitch.isChecked = isShown
-
- // Sub-settings alpha
- val subSettings = subSettingsContainerId?.let { findViewById(it) }
- subSettings?.alpha = if (isShown) 1.0f else 0.4f
- updateToggleCardStyle(toggleCard, isShown)
-
- // Size row visibility
- val sizeRow = sizeRowId?.let { findViewById(it) }
- sizeRow?.visibility = if (isShown) View.VISIBLE else View.GONE
-
- // Selector row visibility
- val selectorRow = selectorRowId?.let { findViewById(it) }
- selectorRow?.visibility = if (isShown) View.VISIBLE else View.GONE
-
- onChanged?.invoke(isShown)
-
- // Internal listener - ALWAYS handles visibility
- toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked && !checkLimit()) {
- toggleSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- if (validateToggle?.invoke(isChecked) == false) {
- toggleSwitch.isChecked = !isChecked
- return@setOnCheckedChangeListener
- }
- prefs.edit().putBoolean(prefShowKey, isChecked).apply()
- subSettings?.alpha = if (isChecked) 1.0f else 0.4f
- updateToggleCardStyle(toggleCard, isChecked)
- sizeRow?.visibility = if (isChecked) View.VISIBLE else View.GONE
- selectorRow?.visibility = if (isChecked) View.VISIBLE else View.GONE
- onChanged?.invoke(isChecked)
- updateWidget()
- if (isContent) updateToggleAvailability()
- }
-
- // Size row setup
- if (sizeRowId != null && prefSizeKey != null) {
- val sizeRowInner = findViewById(sizeRowId)
- val slider = sizeRowInner.findViewById(R.id.row_slider)
- val valueLabel = sizeRowInner.findViewById(R.id.row_value)
- val sizeLabel = sizeRowInner.findViewById(R.id.row_label)
- sizeLabel.text = "Size"
-
- val currentSize = prefs.getFloat(prefSizeKey, defSize)
- slider.valueFrom = minSize
- slider.valueTo = maxSize
- slider.value = currentSize.coerceIn(minSize, maxSize)
- valueLabel.text = "${currentSize.toInt()}"
-
- slider.addOnChangeListener { _, value, fromUser ->
- if (fromUser) {
- valueLabel.text = "${value.toInt()}"
- prefs.edit().putFloat(prefSizeKey, value).apply()
- updateWidget()
- }
- }
- }
-
- // Selector row setup (skip world clock timezone - uses custom search layout)
- if (selectorRowId != null && selectorOptions != null && prefSelectorKey != null && prefSelectorKey != "world_clock_zone_str") {
- val selectorRowInner = findViewById(selectorRowId)
- val autoCompleteTextView = selectorRowInner.findViewById(R.id.row_value)
- val selectorLabel = selectorRowInner.findViewById(R.id.row_label)
- selectorLabel.text = title
-
- val adapter = android.widget.ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, selectorOptions)
- autoCompleteTextView.setAdapter(adapter)
-
- if (prefSelectorKey == "world_clock_zone_str") {
- val currentVal = prefs.getString(prefSelectorKey, "UTC") ?: "UTC"
- autoCompleteTextView.setText(currentVal, false)
- autoCompleteTextView.setOnItemClickListener { _, _, position, _ ->
- val selected = selectorOptions.getOrElse(position) { "UTC" }
- prefs.edit().putString(prefSelectorKey, selected).apply()
- updateWidget()
- selectorRowInner.clearFocus()
- autoCompleteTextView.clearFocus()
- }
- } else {
- val currentIdx = prefs.getInt(prefSelectorKey, defSelectorIdx)
- autoCompleteTextView.setText(selectorOptions.getOrElse(currentIdx) { selectorOptions[defSelectorIdx] }, false)
- autoCompleteTextView.setOnItemClickListener { _, _, position, _ ->
- prefs.edit().putInt(prefSelectorKey, position).apply()
- updateWidget()
- selectorRowInner.clearFocus()
- autoCompleteTextView.clearFocus()
- }
- }
-
- // Clear focus/dropdown shade when dismissed
- autoCompleteTextView.setOnFocusChangeListener { _, hasFocus ->
- if (!hasFocus) {
- autoCompleteTextView.clearFocus()
- }
- }
- }
-
- return toggleSwitch
- }
-
- private fun updateToggleCardStyle(card: com.google.android.material.card.MaterialCardView?, enabled: Boolean) {
- if (card == null) return
- if (enabled) {
- card.setCardBackgroundColor(android.graphics.Color.TRANSPARENT)
- card.strokeWidth = (1f * resources.displayMetrics.density).toInt()
- card.setStrokeColor(android.content.res.ColorStateList.valueOf(
- com.google.android.material.color.MaterialColors.getColor(card, com.google.android.material.R.attr.colorPrimary)
- ))
- } else {
- card.setCardBackgroundColor(
- com.google.android.material.color.MaterialColors.getColor(card, com.google.android.material.R.attr.colorSurfaceContainerLow)
- )
- card.setStrokeColor(android.content.res.ColorStateList.valueOf(
- com.google.android.material.color.MaterialColors.getColor(card, com.google.android.material.R.attr.colorOutlineVariant)
- ))
- }
- }
-
- // Helper for callers who override the toggle listener to update row visibility
- private fun updateFeatureRowVisibility(switch: SwitchMaterial, isChecked: Boolean, sizeRowId: Int? = null) {
- val toggleRow = switch.parent as? View
- val toggleCard = toggleRow?.findViewById(R.id.toggle_row_card)
- updateToggleCardStyle(toggleCard, isChecked)
- sizeRowId?.let { findViewById(it)?.visibility = if (isChecked) View.VISIBLE else View.GONE }
- }
-
- private fun bindTimezoneSearch(
- rowId: Int, zoneIds: List, prefKey: String, defaultVal: String
- ) {
- val row = findViewById(rowId)
- val searchEdit = row.findViewById(R.id.zone_search_edit)
- val listView = row.findViewById(R.id.zone_search_list)
-
- val currentVal = prefs.getString(prefKey, defaultVal) ?: defaultVal
- searchEdit.setText(currentVal)
-
- var filteredList: MutableList = mutableListOf()
- val filteredAdapter = android.widget.ArrayAdapter(this, android.R.layout.simple_list_item_1, filteredList)
-
- // Text watcher for filtering
- searchEdit.addTextChangedListener(object : android.text.TextWatcher {
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
- override fun afterTextChanged(s: android.text.Editable?) {
- val query = s?.toString()?.lowercase() ?: ""
- if (query.isEmpty()) {
- filteredList.clear()
- listView.visibility = View.GONE
- return
- }
- filteredList.clear()
- filteredList.addAll(zoneIds.filter { it.lowercase().contains(query) })
- filteredAdapter.notifyDataSetChanged()
- listView.visibility = if (filteredList.isEmpty()) View.GONE else View.VISIBLE
- }
- })
-
- listView.adapter = filteredAdapter
- listView.setOnItemClickListener { _, _, position, _ ->
- val selected = filteredList[position]
- searchEdit.setText(selected)
- listView.visibility = View.GONE
- searchEdit.clearFocus()
- prefs.edit().putString(prefKey, selected).apply()
- updateWidget()
- }
-
- // Intercept touch events so parent NestedScrollView doesn't steal them
- listView.setOnTouchListener { v, event ->
- v.parent.requestDisallowInterceptTouchEvent(true)
- false
- }
-
- // Show list on focus
- searchEdit.setOnFocusChangeListener { _, hasFocus ->
- if (hasFocus) {
- val query = searchEdit.text?.toString()?.lowercase() ?: ""
- if (query.isEmpty()) {
- filteredList.clear()
- filteredList.addAll(zoneIds)
- filteredAdapter.notifyDataSetChanged()
- listView.visibility = View.VISIBLE
- }
- }
- }
- }
-
- // Dismiss keyboard and collapse nested subsections when a parent section collapses
- private fun collapseSectionNestedContent(sectionKey: String) {
- // World clock timezone search
- if (sectionKey == "world_clock") {
- val worldClockZoneRow = findViewById(R.id.row_world_clock_zone)
- val searchEdit = worldClockZoneRow?.findViewById(R.id.zone_search_edit)
- val listView = worldClockZoneRow?.findViewById(R.id.zone_search_list)
- searchEdit?.clearFocus()
- listView?.visibility = View.GONE
- }
- // Appearance reorder section
- if (sectionKey == "appearance") {
- // collapse reorder too
- }
- // Appearance subsections
- if (sectionKey == "appearance") {
- val outlineContent = findViewById(R.id.content_appearance_outline)
- val colorsContent = findViewById(R.id.content_appearance_colors)
- val themeContent = findViewById(R.id.content_appearance_theme)
- val fontContent = findViewById(R.id.content_appearance_font)
- val transparencyContent = findViewById(R.id.content_appearance_transparency)
- outlineContent?.visibility = View.GONE
- colorsContent?.visibility = View.GONE
- themeContent?.visibility = View.GONE
- fontContent?.visibility = View.GONE
- transparencyContent?.visibility = View.GONE
- prefs.edit()
- .putBoolean("section_appearance_outline_expanded", false)
- .putBoolean("section_appearance_colors_expanded", false)
- .putBoolean("section_appearance_theme_expanded", false)
- .putBoolean("section_appearance_font_expanded", false)
- .putBoolean("section_appearance_transparency_expanded", false)
- .apply()
- // Reset nested chevrons
- listOf(
- R.id.header_chevron_appearance_outline,
- R.id.header_chevron_appearance_colors,
- R.id.header_chevron_appearance_theme,
- R.id.header_chevron_appearance_font,
- R.id.header_chevron_appearance_transparency
- ).forEach { id ->
- findViewById(id)?.rotation = 0f
- }
- }
- dismissKeyboard()
- }
-
- private fun dismissKeyboard() {
- val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager
- currentFocus?.let { imm?.hideSoftInputFromWindow(it.windowToken, 0) }
- }
-
- private fun bindReorderSection() {
- val defaultOrder = listOf(
- ReorderItem("show_battery", getString(R.string.section_battery), prefs.getBoolean("show_battery", true)),
- ReorderItem("show_temp", getString(R.string.section_temp), prefs.getBoolean("show_temp", true)),
- ReorderItem("show_weather_condition", getString(R.string.section_weather_condition), prefs.getBoolean("show_weather_condition", false)),
- ReorderItem("show_data_usage", getString(R.string.section_data_usage), prefs.getBoolean("show_data_usage", false)),
- ReorderItem("show_storage", getString(R.string.section_storage), prefs.getBoolean("show_storage", true)),
- ReorderItem("show_steps", getString(R.string.section_steps), prefs.getBoolean("show_steps", false)),
- ReorderItem("show_screen_time", getString(R.string.section_screen_time), prefs.getBoolean("show_screen_time", false))
- )
-
- val savedOrder = prefs.getString("widget_right_column_order", "")
- val items = if (savedOrder.isNullOrEmpty()) {
- defaultOrder.toMutableList()
- } else {
- val keys = savedOrder.split(",")
- val list = mutableListOf()
- keys.forEach { key ->
- val item = defaultOrder.find { it.key == key }
- if (item != null) list.add(item)
- else list.add(ReorderItem(key, key.replace("show_", "").replace("_", " ").capitalize(), prefs.getBoolean(key, false)))
- }
- // Add any new items not in saved order
- defaultOrder.forEach { default ->
- if (!list.any { it.key == default.key }) list.add(default)
- }
- list
- }
-
- val recyclerView = findViewById(R.id.reorder_recycler)
- val adapter = ReorderAdapter(items) {
- // Save order on every move
- val orderStr = items.joinToString(",") { it.key }
- prefs.edit().putString("widget_right_column_order", orderStr).apply()
- updateWidget()
- }
- recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
- recyclerView.adapter = adapter
-
- // Intercept touch events so parent NestedScrollView doesn't steal them
- recyclerView.setOnTouchListener { v, event ->
- v.parent.requestDisallowInterceptTouchEvent(true)
- false
- }
-
-
- val callback = object : androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback(
- androidx.recyclerview.widget.ItemTouchHelper.UP or androidx.recyclerview.widget.ItemTouchHelper.DOWN, 0
- ) {
- override fun onMove(rv: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
- adapter.moveItem(viewHolder.adapterPosition, target.adapterPosition)
- return true
- }
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
- }
- ItemTouchHelper(callback).attachToRecyclerView(recyclerView)
- }
-
- private fun bindNestedCard(
- headerId: Int, title: String, contentId: Int, sectionKey: String,
- chevronViewId: Int? = null
- ) {
- val header = findViewById(headerId)
- val content = findViewById(contentId)
- val chevronView = header.findViewById(
- chevronViewId ?: R.id.header_chevron
- )
-
- // Standalone toggle - no accordion, each section independent
- nestedViews[sectionKey] = content
- nestedHeaders[sectionKey] = header
-
- val isExpanded = prefs.getBoolean(sectionKey, false)
- content.visibility = if (isExpanded) View.VISIBLE else View.GONE
- chevronView.rotation = if (isExpanded) 180f else 0f
-
- header.setOnClickListener {
- val nowExpanded = content.visibility != View.VISIBLE
- if (nowExpanded) {
- collapseNestedExcept(sectionKey)
- content.visibility = View.VISIBLE
- prefs.edit().putBoolean(sectionKey, true).apply()
- } else {
- content.visibility = View.GONE
- prefs.edit().putBoolean(sectionKey, false).apply()
- }
- android.animation.ObjectAnimator.ofFloat(chevronView, "rotation", if (nowExpanded) 180f else 0f).apply {
- duration = 300
- start()
- }
- }
-
- }
-
- private fun setupSections() {
- contentSwitches.clear()
-
- val zoneIds = java.time.ZoneId.getAvailableZoneIds().sorted()
- val dateFormatOptions = listOf(getString(R.string.date_format_full), getString(R.string.date_format_short), getString(R.string.date_format_numeric))
- val timeFormatOptions = listOf(getString(R.string.format_12h), getString(R.string.format_24h))
- val colorOptions = listOf(getString(R.string.color_default), getString(R.string.color_system_accent), getString(R.string.color_custom))
-
- setupTimeSection(timeFormatOptions)
- setupNextAlarmSection()
- setupWorldClockSection(zoneIds)
- setupDateSection(dateFormatOptions)
- setupBatterySection()
- setupTempSection()
- setupWeatherSection()
- setupDataUsageSection()
- setupStorageSection()
- setupStepsSection()
- setupScreenTimeSection()
- setupKeepAliveSection()
- setupEventsAndTasksSections()
- setupThemeSection(colorOptions)
- }
-
- private fun setupTimeSection(timeFormatOptions: List) {
- // Time
- bindFoldedSection(
- R.id.header_time, R.drawable.ic_time, getString(R.string.section_time),
- R.id.content_time, R.id.row_time_toggle,
- "show_time", true,
- sizeRowId = R.id.row_time_size, prefSizeKey = "size_time", defSize = 64f, minSize = 12f, maxSize = 120f,
- selectorRowId = R.id.row_time_format, selectorOptions = timeFormatOptions, prefSelectorKey = "time_format_idx", defSelectorIdx = 0,
- isContent = true
- )
- }
- private fun setupNextAlarmSection() {
- // Next Alarm
- bindFoldedSection(
- R.id.header_next_alarm, R.drawable.ic_alarm, getString(R.string.section_next_alarm),
- R.id.content_next_alarm, R.id.row_next_alarm_toggle,
- "show_next_alarm", true,
- sizeRowId = R.id.row_next_alarm_size, prefSizeKey = "size_next_alarm", defSize = 14f, minSize = 10f, maxSize = 24f,
- isContent = true
- )
- }
- private fun setupWorldClockSection(zoneIds: List) {
- // World Clock
- bindFoldedSection(
- R.id.header_world_clock, R.drawable.ic_world, getString(R.string.section_world_clock),
- R.id.content_world_clock, R.id.row_world_clock_toggle,
- "show_world_clock", false,
- sizeRowId = R.id.row_world_clock_size, prefSizeKey = "size_world_clock", defSize = 18f, minSize = 10f, maxSize = 32f,
- isContent = true
- )
- bindTimezoneSearch(R.id.row_world_clock_zone, zoneIds, "world_clock_zone_str", "UTC")
- }
- private fun setupDateSection(dateFormatOptions: List) {
- // Date
- bindFoldedSection(
- R.id.header_date, R.drawable.ic_date, getString(R.string.section_date),
- R.id.content_date, R.id.row_date_toggle,
- "show_date", true,
- sizeRowId = R.id.row_date_size, prefSizeKey = "size_date", defSize = 14f, minSize = 10f, maxSize = 24f,
- selectorRowId = R.id.row_date_format, selectorOptions = dateFormatOptions, prefSelectorKey = "date_format_idx", defSelectorIdx = 0,
- isContent = true
- )
- }
- private fun setupBatterySection() {
- // Battery
- bindFoldedSection(
- R.id.header_battery, R.drawable.ic_battery, getString(R.string.section_battery),
- R.id.content_battery, R.id.row_battery_toggle,
- "show_battery", true,
- sizeRowId = R.id.row_battery_size, prefSizeKey = "size_battery", defSize = 24f, minSize = 10f, maxSize = 74f,
- isContent = true
- ).also { it.tag = "battery" }
- bindToggle(R.id.row_battery_bold, "Bold Text", "bold_battery", false)
- }
- private fun setupTempSection() {
- // Temp
- bindFoldedSection(
- R.id.header_temp, R.drawable.ic_temp, getString(R.string.section_temp),
- R.id.content_temp, R.id.row_temp_toggle,
- "show_temp", true,
- sizeRowId = R.id.row_temp_size, prefSizeKey = "size_temp", defSize = 18f, minSize = 10f, maxSize = 74f,
- isContent = true
- ).also { it.tag = "temp" }
- bindToggle(R.id.row_temp_bold, "Bold Text", "bold_temp", false)
- }
- private fun setupWeatherSection() {
- // Weather
- val weatherSwitch = bindFoldedSection(
- R.id.header_weather, R.drawable.ic_weather, getString(R.string.section_weather_condition),
- R.id.content_weather, R.id.row_weather_toggle,
- "show_weather_condition", false,
- sizeRowId = R.id.row_weather_size, prefSizeKey = "size_weather", defSize = 18f, minSize = 10f, maxSize = 74f,
- isContent = true
- ).also { it.tag = "weather_condition" }
- bindToggle(R.id.row_weather_bold, "Bold Text", "bold_weather", false)
-
- // Override weather listener for Breezy Weather check
- weatherSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- if (!isAppInstalled("org.breezyweather")) {
- weatherSwitch.isChecked = false
- com.google.android.material.snackbar.Snackbar.make(
- findViewById(R.id.fab_update),
- "Breezy Weather app (with DataBridge enabled) is required.",
- com.google.android.material.snackbar.Snackbar.LENGTH_LONG
- ).setAction("Install") {
- try {
- startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://github.com/breezy-weather/breezy-weather/releases")))
- } catch (e: Exception) {}
- }.show()
- return@setOnCheckedChangeListener
- }
- if (ContextCompat.checkSelfPermission(this, "org.breezyweather.READ_PROVIDER") != PackageManager.PERMISSION_GRANTED) {
- weatherSwitch.isChecked = false
- android.app.AlertDialog.Builder(this)
- .setTitle("Permission Clarification")
- .setMessage("To display the weather, Lwidget needs to read data from Breezy Weather.\n\nAndroid will now ask for 'Location' access. Please note: Lwidget DOES NOT access your location, nor does it have permission to access the internet. This is simply how Android categorizes Breezy Weather's data sharing permission.")
- .setPositiveButton("Continue") { _, _ ->
- ActivityCompat.requestPermissions(this, arrayOf("org.breezyweather.READ_PROVIDER"), 103)
- }
- .setNegativeButton("Cancel", null)
- .show()
- return@setOnCheckedChangeListener
- }
- if (!checkLimit()) {
- weatherSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- }
- prefs.edit().putBoolean("show_weather_condition", isChecked).apply()
- updateFeatureRowVisibility(weatherSwitch, isChecked, R.id.row_weather_size)
- updateWidget()
- updateToggleAvailability()
- if (isChecked) {
- android.app.AlertDialog.Builder(this)
- .setTitle("Important Step")
- .setMessage("If the weather doesn't show up on your widget soon:\n\nOpen Breezy Weather → Settings → External Modules → Enable 'Send Gadgetbridge Data' & toggle on 'Lwidget'.")
- .setPositiveButton("Got it", null)
- .show()
- }
- }
- }
- private fun setupDataUsageSection() {
- // Data Usage
- val dataSwitch = bindFoldedSection(
- R.id.header_data, R.drawable.ic_data, getString(R.string.section_data_usage),
- R.id.content_data, R.id.row_data_toggle,
- "show_data_usage", false,
- sizeRowId = R.id.row_data_size, prefSizeKey = "size_data", defSize = 14f, minSize = 10f, maxSize = 74f,
- isContent = true
- ).also { it.tag = "data" }
- bindToggle(R.id.row_data_bold, "Bold Text", "bold_data_usage", false)
-
- dataSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- if (!hasUsageStatsPermission()) {
- dataSwitch.isChecked = false
- try {
- startActivity(Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS))
- com.google.android.material.snackbar.Snackbar.make(
- findViewById(R.id.fab_update),
- getString(R.string.perm_usage_access_title),
- com.google.android.material.snackbar.Snackbar.LENGTH_LONG
- ).show()
- } catch (e: Exception) {}
- return@setOnCheckedChangeListener
- }
- if (!checkLimit()) {
- dataSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- }
- prefs.edit().putBoolean("show_data_usage", isChecked).apply()
- updateFeatureRowVisibility(dataSwitch, isChecked, R.id.row_data_size)
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- }
- }
- private fun setupStorageSection() {
- // Storage
- bindFoldedSection(
- R.id.header_storage, R.drawable.ic_storage, getString(R.string.section_storage),
- R.id.content_storage, R.id.row_storage_toggle,
- "show_storage", true,
- sizeRowId = R.id.row_storage_size, prefSizeKey = "size_storage", defSize = 14f, minSize = 10f, maxSize = 74f,
- isContent = true
- ).also { it.tag = "storage" }
- bindToggle(R.id.row_storage_bold, "Bold Text", "bold_storage", false)
- }
- private fun setupStepsSection() {
- // Steps
- val stepsSwitch = bindFoldedSection(
- R.id.header_steps, R.drawable.ic_steps, getString(R.string.section_steps),
- R.id.content_steps, R.id.row_steps_toggle,
- "show_steps", false,
- sizeRowId = R.id.row_steps_size, prefSizeKey = "size_steps", defSize = 14f, minSize = 10f, maxSize = 74f,
- isContent = true
- ).also { it.tag = "steps" }
- bindToggle(R.id.row_steps_bold, "Bold Text", "bold_steps", false)
-
- stepsSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- val neededPermissions = mutableListOf()
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
- neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION)
- }
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU &&
- ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
- }
- if (neededPermissions.isNotEmpty()) {
- ActivityCompat.requestPermissions(this, neededPermissions.toTypedArray(), 102)
- stepsSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- }
- if (!checkLimit()) {
- stepsSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- }
- prefs.edit().putBoolean("show_steps", isChecked).apply()
- val keepAlive = prefs.getBoolean("keep_alive", false)
- val serviceIntent = Intent(this, StepCounterService::class.java)
- if (isChecked) {
- val hasPermission = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_GRANTED
- } else { true }
- if (hasPermission) { startForegroundService(serviceIntent) }
- else {
- prefs.edit().putBoolean("show_steps", false).apply()
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- return@setOnCheckedChangeListener
- }
- } else if (!keepAlive) { stopService(serviceIntent) }
- updateFeatureRowVisibility(stepsSwitch, isChecked, R.id.row_steps_size)
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- }
- }
- private fun setupScreenTimeSection() {
- // Screen Time
- val screenTimeSwitch = bindFoldedSection(
- R.id.header_screen_time, R.drawable.ic_time, getString(R.string.section_screen_time),
- R.id.content_screen_time, R.id.row_screen_time_toggle,
- "show_screen_time", false,
- sizeRowId = R.id.row_screen_time_size, prefSizeKey = "size_screen_time", defSize = 14f, minSize = 10f, maxSize = 74f,
- isContent = true
- ).also { it.tag = "screen_time" }
- bindToggle(R.id.row_screen_time_bold, "Bold Text", "bold_screen_time", false)
-
- screenTimeSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- if (!hasUsageStatsPermission()) {
- screenTimeSwitch.isChecked = false
- try {
- startActivity(Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS))
- com.google.android.material.snackbar.Snackbar.make(
- findViewById(R.id.fab_update),
- getString(R.string.perm_usage_access_title),
- com.google.android.material.snackbar.Snackbar.LENGTH_LONG
- ).show()
- } catch (e: Exception) {}
- return@setOnCheckedChangeListener
- }
- if (!checkLimit()) {
- screenTimeSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- }
- prefs.edit().putBoolean("show_screen_time", isChecked).apply()
- updateFeatureRowVisibility(screenTimeSwitch, isChecked, R.id.row_screen_time_size)
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- }
- }
- private fun setupKeepAliveSection() {
- // Keep Alive
- val keepAliveSwitch = bindFoldedSection(
- R.id.header_keep_alive, R.drawable.ic_alarm, getString(R.string.section_keep_alive),
- R.id.content_keep_alive, R.id.row_keep_alive_toggle,
- "keep_alive", false
- )
-
- keepAliveSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- val neededPermissions = mutableListOf()
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q &&
- ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
- neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION)
- }
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU &&
- ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
- }
- if (neededPermissions.isNotEmpty()) {
- ActivityCompat.requestPermissions(this, neededPermissions.toTypedArray(), 104)
- keepAliveSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- }
- prefs.edit().putBoolean("keep_alive", isChecked).apply()
- updateFeatureRowVisibility(keepAliveSwitch, isChecked)
- val showSteps = prefs.getBoolean("show_steps", false)
- val serviceIntent = Intent(this, StepCounterService::class.java)
- if (isChecked || showSteps) { startForegroundService(serviceIntent) }
- else { stopService(serviceIntent) }
- updateWidget()
- }
- }
- private fun setupEventsAndTasksSections() {
- // Events
- val eventsSwitch = bindFoldedSection(
- R.id.header_events, R.drawable.ic_events, getString(R.string.section_events),
- R.id.content_events, R.id.row_events_toggle,
- "show_events", false,
- sizeRowId = R.id.row_events_size, prefSizeKey = "size_events", defSize = 14f, minSize = 10f, maxSize = 18f,
- isContent = true
- )
-
- // Tasks
- val tasksSwitch = bindFoldedSection(
- R.id.header_tasks, R.drawable.ic_tasks, getString(R.string.section_tasks),
- R.id.content_tasks, R.id.row_tasks_toggle,
- "show_tasks", false,
- sizeRowId = R.id.row_tasks_size, prefSizeKey = "size_tasks", defSize = 14f, minSize = 10f, maxSize = 18f,
- isContent = true
- )
-
- // Mutual Exclusion: Events vs Tasks
- eventsSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CALENDAR), 100)
- eventsSwitch.isChecked = false
- return@setOnCheckedChangeListener
- }
- if (checkLimit()) {
- tasksSwitch.isChecked = false
- prefs.edit().putBoolean("show_events", true).putBoolean("show_tasks", false).apply()
- updateFeatureRowVisibility(eventsSwitch, true, R.id.row_events_size)
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- } else { eventsSwitch.isChecked = false }
- } else {
- prefs.edit().putBoolean("show_events", false).apply()
- updateFeatureRowVisibility(eventsSwitch, false, R.id.row_events_size)
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- }
- }
- tasksSwitch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- if (!isAppInstalled("org.tasks")) {
- tasksSwitch.isChecked = false
- com.google.android.material.snackbar.Snackbar.make(
- findViewById(R.id.fab_update),
- "Tasks.org app is required for this feature.",
- com.google.android.material.snackbar.Snackbar.LENGTH_LONG
- ).setAction("Install") {
- try {
- startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse("market://details?id=org.tasks")))
- } catch (e: Exception) {
- startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://play.google.com/store/apps/details?id=org.tasks")))
- }
- }.show()
- return@setOnCheckedChangeListener
- }
- if (ContextCompat.checkSelfPermission(this, AwidgetProvider.PERMISSION_READ_TASKS_ORG) != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, arrayOf(AwidgetProvider.PERMISSION_READ_TASKS_ORG), 101)
- }
- if (checkLimit()) {
- eventsSwitch.isChecked = false
- prefs.edit().putBoolean("show_tasks", true).putBoolean("show_events", false).apply()
- updateFeatureRowVisibility(tasksSwitch, true, R.id.row_tasks_size)
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- } else { tasksSwitch.isChecked = false }
- } else {
- prefs.edit().putBoolean("show_tasks", false).apply()
- updateFeatureRowVisibility(tasksSwitch, false, R.id.row_tasks_size)
- updateWidget()
- updateToggleAvailability()
- checkAllPermissions()
- }
- }
- }
- private fun setupThemeSection(colorOptions: List) {
- // ===== THEME =====
- // Use bindFoldedSection for the main card (top-level accordion), not bindNestedCard
- accordionViews["section_appearance_expanded"] = findViewById(R.id.content_appearance)
- accordionHeaders["section_appearance_expanded"] = findViewById(R.id.header_appearance)
- // Set title and icon
- val appearanceHeader = findViewById(R.id.header_appearance)
- appearanceHeader.findViewById(R.id.header_title).text = "Theme"
- val appearanceHeaderIcon = appearanceHeader.findViewById(R.id.header_icon)
- appearanceHeaderIcon.setImageResource(R.drawable.ic_palette)
- appearanceHeaderIcon.visibility = View.VISIBLE
-
- val appearanceContent = findViewById(R.id.content_appearance)
- val appearanceChevron = findViewById(R.id.header_appearance).findViewById(R.id.header_chevron)
- val appearanceIsExpanded = prefs.getBoolean("section_appearance_expanded", false)
- appearanceContent.visibility = if (appearanceIsExpanded) View.VISIBLE else View.GONE
- appearanceChevron.rotation = if (appearanceIsExpanded) 180f else 0f
-
- findViewById(R.id.header_appearance).setOnClickListener {
- val nowExpanded = appearanceContent.visibility != View.VISIBLE
- if (nowExpanded) {
- collapseAllExcept("section_appearance_expanded")
- appearanceContent.visibility = View.VISIBLE
- prefs.edit().putBoolean("section_appearance_expanded", true).apply()
- } else {
- appearanceContent.visibility = View.GONE
- prefs.edit().putBoolean("section_appearance_expanded", false).apply()
- }
- android.animation.ObjectAnimator.ofFloat(appearanceChevron, "rotation", if (nowExpanded) 180f else 0f).apply {
- duration = 300
- start()
- }
- }
-
- // Appearance Subsections (nested cards)
- bindNestedCard(R.id.header_appearance_outline, "OUTLINE", R.id.content_appearance_outline, "section_appearance_outline_expanded", R.id.header_chevron_appearance_outline)
- bindNestedCard(R.id.header_appearance_colors, "COLORS", R.id.content_appearance_colors, "section_appearance_colors_expanded", R.id.header_chevron_appearance_colors)
- bindNestedCard(R.id.header_appearance_theme, "THEME", R.id.content_appearance_theme, "section_appearance_theme_expanded", R.id.header_chevron_appearance_theme)
- bindNestedCard(R.id.header_appearance_font, "FONT", R.id.content_appearance_font, "section_appearance_font_expanded", R.id.header_chevron_appearance_font)
- bindNestedCard(R.id.header_appearance_transparency, "TRANSPARENCY", R.id.content_appearance_transparency, "section_appearance_transparency_expanded", R.id.header_chevron_appearance_transparency)
-
- // Reorder section
- bindNestedCard(R.id.header_appearance_reorder, "REORDER", R.id.content_appearance_reorder, "section_appearance_reorder_expanded", R.id.header_chevron_appearance_reorder)
- bindReorderSection()
-
- // Outline toggle
- bindToggle(R.id.row_outline_toggle, "Show Outline", "show_outline", true) { isChecked ->
- updateWidget()
- }
-
- // Dynamic Colors toggle
- val rowDynamicColors = findViewById(R.id.row_dynamic_colors_toggle)
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- rowDynamicColors.visibility = View.VISIBLE
- bindToggle(R.id.row_dynamic_colors_toggle, "Dynamic Colors", "use_dynamic_colors", true) { isChecked ->
- updateColorVisibility(isChecked)
- if (isChecked) {
- prefs.edit()
- .putInt("text_color_primary_idx", 0)
- .putInt("text_color_secondary_idx", 0)
- .putInt("date_color_idx", 0)
- .putInt("outline_color_idx", 0)
- .putInt("bg_color_idx", 0)
- .apply()
- }
- }
- } else {
- rowDynamicColors.visibility = View.GONE
- }
-
- // Theme toggle
- bindToggle(R.id.row_theme_toggle, "Light Theme", "use_system_theme", false) { isChecked ->
- applyTheme()
- }
-
- // BG Transparency
- bindSlider(R.id.row_bg_transparency, "Background Opacity", "bg_opacity", 100f, 0f, 100f)
-
- // Background Color
- val bgSliderRow = findViewById(R.id.row_bg_color_custom)
- bindSelector(R.id.row_bg_color, getString(R.string.section_bg_color), "bg_color_idx", colorOptions, 0) { idx ->
- bgSliderRow.visibility = if (idx == 2) View.VISIBLE else View.GONE
- if (idx != 2) updateWidget()
- }
- bindColorSliders(R.id.row_bg_color_custom, "bg_color")
- bgSliderRow.visibility = if (prefs.getInt("bg_color_idx", 0) == 2) View.VISIBLE else View.GONE
-
- // Text Color Primary
- val primarySliderRow = findViewById(R.id.row_text_color_primary_custom)
- bindSelector(R.id.row_text_color_primary, getString(R.string.section_text_color_primary), "text_color_primary_idx", colorOptions, 0) { idx ->
- primarySliderRow.visibility = if (idx == 2) View.VISIBLE else View.GONE
- if (idx != 2) updateWidget()
- }
- bindColorSliders(R.id.row_text_color_primary_custom, "text_color_primary")
- primarySliderRow.visibility = if (prefs.getInt("text_color_primary_idx", 0) == 2) View.VISIBLE else View.GONE
-
- // Text Color Secondary
- val secondarySliderRow = findViewById(R.id.row_text_color_secondary_custom)
- bindSelector(R.id.row_text_color_secondary, getString(R.string.section_text_color_secondary), "text_color_secondary_idx", colorOptions, 0) { idx ->
- secondarySliderRow.visibility = if (idx == 2) View.VISIBLE else View.GONE
- if (idx != 2) updateWidget()
- }
- bindColorSliders(R.id.row_text_color_secondary_custom, "text_color_secondary")
- secondarySliderRow.visibility = if (prefs.getInt("text_color_secondary_idx", 0) == 2) View.VISIBLE else View.GONE
-
- // Date Color
- val dateSliderRow = findViewById(R.id.row_date_color_custom)
- bindSelector(R.id.row_date_color, getString(R.string.section_date_color), "date_color_idx", colorOptions, 0) { idx ->
- dateSliderRow.visibility = if (idx == 2) View.VISIBLE else View.GONE
- if (idx != 2) updateWidget()
- }
- bindColorSliders(R.id.row_date_color_custom, "date_color")
- dateSliderRow.visibility = if (prefs.getInt("date_color_idx", 0) == 2) View.VISIBLE else View.GONE
-
- // Outline Color
- val outlineSliderRow = findViewById(R.id.row_outline_color_custom)
- bindSelector(R.id.row_outline_color, getString(R.string.section_outline_color), "outline_color_idx", colorOptions, 0) { idx ->
- outlineSliderRow.visibility = if (idx == 2) View.VISIBLE else View.GONE
- if (idx != 2) updateWidget()
- }
- bindColorSliders(R.id.row_outline_color_custom, "outline_color")
- outlineSliderRow.visibility = if (prefs.getInt("outline_color_idx", 0) == 2) View.VISIBLE else View.GONE
-
- // Apply initial dynamic colors visibility
- updateColorVisibility(prefs.getBoolean("use_dynamic_colors", true))
-
- // Font selector
- bindSelector(R.id.row_font, getString(R.string.section_font), "font_style", listOf(
- getString(R.string.font_default), getString(R.string.font_serif), getString(R.string.font_monospace), getString(R.string.font_cursive),
- getString(R.string.font_condensed), getString(R.string.font_condensed_light), getString(R.string.font_light), getString(R.string.font_medium),
- getString(R.string.font_black), getString(R.string.font_thin), getString(R.string.font_smallcaps)
- ), 0)
-
- updateToggleAvailability()
- }
-
- private fun bindToggle(
- viewId: Int, title: String, prefShowKey: String, defShow: Boolean,
- isContent: Boolean = false,
- onChanged: ((Boolean) -> Unit)? = null
- ) {
- val row = findViewById(viewId)
- val tvTitle = row.findViewById(R.id.row_label)
- val switch = row.findViewById(R.id.row_switch)
-
- tvTitle.text = title
- if (isContent) contentSwitches.add(switch)
-
- val isShown = prefs.getBoolean(prefShowKey, defShow)
- switch.isChecked = isShown
- onChanged?.invoke(isShown)
-
- switch.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked && !checkLimit()) {
- switch.isChecked = false
- return@setOnCheckedChangeListener
- }
- prefs.edit().putBoolean(prefShowKey, isChecked).apply()
- onChanged?.invoke(isChecked)
- updateWidget()
- if (isContent) updateToggleAvailability()
- }
- }
-
-
- private fun bindSlider(
- viewId: Int, title: String, prefKey: String, defValue: Float,
- minValue: Float, maxValue: Float
- ) {
- val row = findViewById(viewId)
- val tvTitle = row.findViewById(R.id.row_label)
- val slider = row.findViewById(R.id.row_slider)
- val tvValue = row.findViewById(R.id.row_value)
-
- tvTitle.text = title
-
- val currentValue = prefs.getFloat(prefKey, defValue)
- slider.valueFrom = minValue
- slider.valueTo = maxValue
- slider.value = currentValue.coerceIn(minValue, maxValue)
- tvValue.text = "${currentValue.toInt()}%"
-
- slider.addOnChangeListener { _, value, fromUser ->
- if (fromUser) {
- tvValue.text = "${value.toInt()}%"
- prefs.edit().putFloat(prefKey, value).apply()
- updateWidget()
- }
- }
- }
-
- private fun bindSelector(
- viewId: Int, title: String, prefKey: String, options: List,
- defaultIdx: Int, onSelectionChanged: ((Int) -> Unit)? = null
- ) {
- val row = findViewById(viewId)
- val tvTitle = row.findViewById(R.id.row_label)
- val autoCompleteTextView = row.findViewById(R.id.row_value)
-
- tvTitle.text = title
-
- val adapter = android.widget.ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, options)
- autoCompleteTextView.setAdapter(adapter)
-
- if (prefKey == "world_clock_zone_str") {
- val currentVal = prefs.getString(prefKey, "UTC") ?: "UTC"
- autoCompleteTextView.setText(currentVal, false)
- autoCompleteTextView.setOnItemClickListener { _, _, position, _ ->
- val selected = options.getOrElse(position) { "UTC" }
- prefs.edit().putString(prefKey, selected).apply()
- updateWidget()
- }
- } else {
- val currentIdx = prefs.getInt(prefKey, defaultIdx)
- autoCompleteTextView.setText(options.getOrElse(currentIdx) { options[defaultIdx] }, false)
- autoCompleteTextView.setOnItemClickListener { _, _, position, _ ->
- prefs.edit().putInt(prefKey, position).apply()
- updateWidget()
- onSelectionChanged?.invoke(position)
- row.requestFocus()
- autoCompleteTextView.clearFocus()
- }
- }
- }
-
- private fun bindColorSliders(viewId: Int, prefPrefix: String): View {
- val row = findViewById(viewId)
- val sliderRed = row.findViewById(R.id.slider_red)
- val sliderGreen = row.findViewById(R.id.slider_green)
- val sliderBlue = row.findViewById(R.id.slider_blue)
- val valRed = row.findViewById(R.id.val_red)
- val valGreen = row.findViewById(R.id.val_green)
- val valBlue = row.findViewById(R.id.val_blue)
- val preview = row.findViewById(R.id.color_preview)
-
- val r = prefs.getInt("${prefPrefix}_r", 255)
- val g = prefs.getInt("${prefPrefix}_g", 255)
- val b = prefs.getInt("${prefPrefix}_b", 255)
-
- fun updatePreview() {
- val color = android.graphics.Color.rgb(sliderRed.value.toInt(), sliderGreen.value.toInt(), sliderBlue.value.toInt())
- preview.backgroundTintList = android.content.res.ColorStateList.valueOf(color)
- valRed.text = sliderRed.value.toInt().toString()
- valGreen.text = sliderGreen.value.toInt().toString()
- valBlue.text = sliderBlue.value.toInt().toString()
- }
-
- sliderRed.value = r.toFloat()
- sliderGreen.value = g.toFloat()
- sliderBlue.value = b.toFloat()
- updatePreview()
-
- val listener = Slider.OnChangeListener { _, _, fromUser ->
- if (fromUser) {
- updatePreview()
- prefs.edit()
- .putInt("${prefPrefix}_r", sliderRed.value.toInt())
- .putInt("${prefPrefix}_g", sliderGreen.value.toInt())
- .putInt("${prefPrefix}_b", sliderBlue.value.toInt())
- .apply()
- updateWidget()
- }
- }
-
- sliderRed.addOnChangeListener(listener)
- sliderGreen.addOnChangeListener(listener)
- sliderBlue.addOnChangeListener(listener)
-
- return row
- }
-
- private fun updateColorVisibility(useDynamicColors: Boolean) {
- val manualColorIds = listOf(
- R.id.row_bg_color, R.id.row_bg_color_custom,
- R.id.row_text_color_primary, R.id.row_text_color_primary_custom,
- R.id.row_text_color_secondary, R.id.row_text_color_secondary_custom,
- R.id.row_date_color, R.id.row_date_color_custom,
- R.id.row_outline_color, R.id.row_outline_color_custom
- )
- manualColorIds.forEach { id ->
- findViewById(id).visibility = if (useDynamicColors) View.GONE else View.VISIBLE
- }
- }
-
- private fun applyTheme() {
- val useSystemTheme = prefs.getBoolean("use_system_theme", false)
- updateWidget()
- }
-
- private fun checkLimit(): Boolean {
- // Global limit removed per user request
-
- // Subset Limit: Battery, Weather, Temp, Data, Storage (Max 5 allowed now to fit stack)
- val subsetCount = contentSwitches.count {
- it.isChecked && (it.tag == "battery" || it.tag == "weather_condition" || it.tag == "temp" || it.tag == "data" || it.tag == "storage")
- }
-
- if (subsetCount > 5) {
- com.google.android.material.snackbar.Snackbar.make(
- findViewById(R.id.fab_update),
- getString(R.string.error_max_subset_items),
- com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
- ).show()
- return false
- }
-
- return true
- }
-
- // Check usage stats permission
- private fun hasUsageStatsPermission(): Boolean {
- val appOps = getSystemService(Context.APP_OPS_SERVICE) as android.app.AppOpsManager
- val opMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- appOps.unsafeCheckOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS,
- android.os.Process.myUid(), packageName)
- } else {
- @Suppress("DEPRECATION")
- appOps.checkOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS,
- android.os.Process.myUid(), packageName)
- }
- return opMode == android.app.AppOpsManager.MODE_ALLOWED
- }
-
- private fun updateToggleAvailability() {
- // Limit removed
- // Ensure all are enabled
- for (switch in contentSwitches) {
- switch.isEnabled = true
- switch.alpha = 1.0f
- }
- }
-
- private fun isAppInstalled(packageName: String): Boolean {
- return try {
- packageManager.getPackageInfo(packageName, 0)
- true
- } catch (e: PackageManager.NameNotFoundException) {
- false
- }
- }
-
- private fun updateWidget() {
- // Animation: Subtle Outline Shine
- val fab = findViewById(R.id.fab_update)
-
- // Get dynamic colors
- // val colorSurface = com.google.android.material.color.MaterialColors.getColor(fab, com.google.android.material.R.attr.colorSurface)
- val colorPrimary = com.google.android.material.color.MaterialColors.getColor(fab, com.google.android.material.R.attr.colorPrimary)
- val colorTransparent = android.graphics.Color.TRANSPARENT
-
- val strokeAnimator = android.animation.ValueAnimator.ofArgb(colorTransparent, colorPrimary, colorTransparent)
- strokeAnimator.duration = 1000
- strokeAnimator.addUpdateListener { animator ->
- fab.strokeColor = android.content.res.ColorStateList.valueOf(animator.animatedValue as Int)
- }
- strokeAnimator.start()
-
- // Trigger widget update by sending broadcast
- val intent = Intent(this, AwidgetProvider::class.java).apply {
- action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
- // Get all IDs
- val ids = AppWidgetManager.getInstance(application).getAppWidgetIds(ComponentName(application, AwidgetProvider::class.java))
- putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
- }
- sendBroadcast(intent)
- }
-}
diff --git a/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt b/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt
index b00824d..416e051 100644
--- a/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt
+++ b/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt
@@ -59,9 +59,19 @@ class SetupActivity : AppCompatActivity() {
switchKeepAlive.isChecked = prefs.getBoolean("keep_alive", false)
switchKeepAlive.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
+ val neededPermissions = mutableListOf()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) {
+ neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION)
+ }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- requestPermissionLauncher.launch(arrayOf(Manifest.permission.POST_NOTIFICATIONS))
+ neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ if (neededPermissions.isNotEmpty()) {
+ switchKeepAlive.isChecked = false
+ requestPermissionLauncher.launch(neededPermissions.toTypedArray())
+ return@setOnCheckedChangeListener
}
}
prefs.edit().putBoolean("keep_alive", isChecked).apply()
@@ -206,7 +216,10 @@ class SetupActivity : AppCompatActivity() {
val keepAlive = prefs.getBoolean("keep_alive", false)
val showSteps = prefs.getBoolean("show_steps", false)
- if (keepAlive || showSteps) {
+ val hasActivityPerm = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_GRANTED
+
+ if ((keepAlive || showSteps) && hasActivityPerm) {
val serviceIntent = Intent(this, StepCounterService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
diff --git a/app/src/main/java/com/leanbitlab/lwidget/StepCounterService.kt b/app/src/main/java/com/leanbitlab/lwidget/StepCounterService.kt
index 113a6e6..f0e79d3 100644
--- a/app/src/main/java/com/leanbitlab/lwidget/StepCounterService.kt
+++ b/app/src/main/java/com/leanbitlab/lwidget/StepCounterService.kt
@@ -42,6 +42,18 @@ class StepCounterService : Service(), SensorEventListener {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
+
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(NOTIFICATION_ID, createNotification(), android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH)
+ } else {
+ startForeground(NOTIFICATION_ID, createNotification())
+ }
+ } catch (e: SecurityException) {
+ android.util.Log.e("LWidget", "Cannot start foreground service: missing permission", e)
+ stopSelf()
+ return
+ }
val prefs = getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
val showSteps = prefs.getBoolean("show_steps", false)
@@ -56,18 +68,6 @@ class StepCounterService : Service(), SensorEventListener {
}
}
- try {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- startForeground(NOTIFICATION_ID, createNotification(), android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH)
- } else {
- startForeground(NOTIFICATION_ID, createNotification())
- }
- } catch (e: SecurityException) {
- android.util.Log.e("LWidget", "Cannot start foreground service: missing permission", e)
- stopSelf()
- return
- }
-
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index dc98ffe..60067c7 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -95,444 +95,516 @@
android:layout_marginBottom="16dp"
android:clipChildren="false"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ android:textSize="14sp"
+ android:letterSpacing="0.05"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="4dp"/>
+
+ android:orientation="vertical">
-
-
-
-
-
-
+ android:layout_height="wrap_content"/>
+ android:visibility="gone"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:paddingBottom="8dp">
-
+
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="6dp"
+ app:cardBackgroundColor="?attr/colorSurfaceContainerLow">
-
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingStart="14dp"
+ android:paddingEnd="14dp"
+ android:paddingTop="10dp"
+ android:paddingBottom="10dp"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true">
-
+
+
+
+
-
+ android:orientation="vertical"
+ android:visibility="gone"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:paddingBottom="12dp">
+
+
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+ android:orientation="vertical"
+ android:visibility="gone"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:paddingBottom="8dp">
+
+
-
+
-
+
+ android:layout_marginBottom="6dp"
+ app:cardBackgroundColor="?attr/colorSurfaceContainerLow">
-
+
-
+
-
+
-
-
-
+
+
-
-
+
-
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
+
-
-
+
-
+
-
+
-
+
+
+
+
-
+
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="6dp"
+ app:cardBackgroundColor="?attr/colorSurfaceContainerLow">
-
-
-
-
+
-
-
+
-
+
-
+
+
-
+
-
+
+
+
+
-
+
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="6dp"
+ app:cardBackgroundColor="?attr/colorSurfaceContainerLow">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="6dp"
+ app:cardBackgroundColor="?attr/colorSurfaceContainerLow">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:paddingTop="14dp"
+ android:paddingBottom="14dp"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true">
+
+
+
+
+
+
+
-
-
-
-
-
-
+
-
+
-
-
+
-
-
-
+
-
-
+
-
-
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content"/>
+
+
+
-
+
+
-
+
-
+
-
+
-
-
-
-
+
-
-
+ android:layout_height="wrap_content"/>
-
+
+
+
+
-
+
+
-
+
-
-
+
-
+
-
-
-
-
+
-
-
+ android:layout_height="wrap_content"/>
-
+
+
+
+
-
+
+
-
+
+
-
-
+
-
+
-
-
-
-
+
-
-
+
-
+
+
+
+
-
+
+
-
+
-
-
+
-
+
-
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
-
-
+ android:orientation="vertical"
+ android:visibility="gone"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:paddingBottom="8dp">
+
+
+ android:text="Note: Update interval is irrelevant when Keep Alive is toggled on."
+ android:textSize="12sp"
+ android:textColor="?attr/colorOnSurfaceVariant"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:layout_marginBottom="8dp"/>
+
+
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/app/src/main/res/layout/widget_layout.xml b/app/src/main/res/layout/widget_layout.xml
index 03eb365..b1e36e6 100644
--- a/app/src/main/res/layout/widget_layout.xml
+++ b/app/src/main/res/layout/widget_layout.xml
@@ -152,6 +152,10 @@
android:format24Hour="EEEE, MMMM dd"
android:textSize="18sp"
android:textColor="#CCFFFFFF"
+ android:shadowColor="#80000000"
+ android:shadowRadius="3"
+ android:shadowDx="1"
+ android:shadowDy="1"
/>
= android.os.Build.VERSION_CODES.S) {
- // Warm accent for date
- context.getColor(if (useLightTheme) android.R.color.system_accent2_700 else android.R.color.system_accent2_100)
- } else {
-- if (useLightTheme) android.graphics.Color.parseColor("#AA555544") else android.graphics.Color.parseColor("#BBDDDDCC")
-+ when (dateColorIdx) {
-+ 2 -> android.graphics.Color.rgb(prefs.getInt("date_color_r", 255), prefs.getInt("date_color_g", 255), prefs.getInt("date_color_b", 255))
-+ 1 -> if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) context.getColor(android.R.color.system_accent2_500) else android.graphics.Color.YELLOW
-+ else -> if (useLightTheme) android.graphics.Color.parseColor("#AA555544") else android.graphics.Color.parseColor("#BBDDDDCC")
-+ }
- }
- val alarmColor = if (useDynamicColors && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
diff --git a/fix_activity_main.sh b/fix_activity_main.sh
deleted file mode 100644
index 2551867..0000000
--- a/fix_activity_main.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-sed -i '/android:id="@+id\/row_date_color"/,+4d' app/src/main/res/layout/activity_main.xml
-sed -i '/android:id="@+id\/row_date_color_custom"/,+4d' app/src/main/res/layout/activity_main.xml
diff --git a/fix_awidget.sh b/fix_awidget.sh
deleted file mode 100755
index 9b7248d..0000000
--- a/fix_awidget.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-sed -i 's/fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, mode: UpdateMode = UpdateMode.FULL)/fun buildAppWidgetRemoteViews(context: Context, mode: UpdateMode = UpdateMode.FULL): RemoteViews/g' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i '/return tickViews/{n; /return/d}' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i '/return calViews/{n; /return/d}' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i '/return taskViews/{n; /return/d}' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i '/return alarmViews/{n; /return/d}' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i 's/appWidgetManager.partiallyUpdateAppWidget(appWidgetId, tickViews)/return tickViews/g' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i 's/appWidgetManager.partiallyUpdateAppWidget(appWidgetId, calViews)/return calViews/g' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i 's/appWidgetManager.partiallyUpdateAppWidget(appWidgetId, taskViews)/return taskViews/g' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i 's/appWidgetManager.partiallyUpdateAppWidget(appWidgetId, alarmViews)/return alarmViews/g' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-sed -i 's/appWidgetManager.updateAppWidget(appWidgetId, views)/return views\n }\n\n fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, mode: UpdateMode = UpdateMode.FULL) {\n val views = buildAppWidgetRemoteViews(context, mode)\n if (mode == UpdateMode.FULL) {\n appWidgetManager.updateAppWidget(appWidgetId, views)\n } else {\n appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views)\n }/g' app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
diff --git a/main_patch.diff b/main_patch.diff
deleted file mode 100644
index 00559f7..0000000
--- a/main_patch.diff
+++ /dev/null
@@ -1,34 +0,0 @@
---- app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
-+++ app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
-@@ -1187,6 +1187,7 @@
- prefs.edit()
- .putInt("text_color_primary_idx", 0)
- .putInt("text_color_secondary_idx", 0)
-+ .putInt("date_color_idx", 0)
- .putInt("outline_color_idx", 0)
- .putInt("bg_color_idx", 0)
- .apply()
-@@ -1226,6 +1227,15 @@
- bindColorSliders(R.id.row_text_color_secondary_custom, "text_color_secondary")
- secondarySliderRow.visibility = if (prefs.getInt("text_color_secondary_idx", 0) == 2) View.VISIBLE else View.GONE
-
-+ // Date Color
-+ val dateSliderRow = findViewById(R.id.row_date_color_custom)
-+ bindSelector(R.id.row_date_color, getString(R.string.section_date_color), "date_color_idx", colorOptions, 0) { idx ->
-+ dateSliderRow.visibility = if (idx == 2) View.VISIBLE else View.GONE
-+ if (idx != 2) updateWidget()
-+ }
-+ bindColorSliders(R.id.row_date_color_custom, "date_color")
-+ dateSliderRow.visibility = if (prefs.getInt("date_color_idx", 0) == 2) View.VISIBLE else View.GONE
-+
- // Outline Color
- val outlineSliderRow = findViewById(R.id.row_outline_color_custom)
- bindSelector(R.id.row_outline_color, getString(R.string.section_outline_color), "outline_color_idx", colorOptions, 0) { idx ->
-@@ -1400,6 +1410,7 @@
- R.id.row_bg_color, R.id.row_bg_color_custom,
- R.id.row_text_color_primary, R.id.row_text_color_primary_custom,
- R.id.row_text_color_secondary, R.id.row_text_color_secondary_custom,
-+ R.id.row_date_color, R.id.row_date_color_custom,
- R.id.row_outline_color, R.id.row_outline_color_custom
- )
- manualColorIds.forEach { id ->
diff --git a/patch_activity_main.diff b/patch_activity_main.diff
deleted file mode 100644
index aa4a8ff..0000000
--- a/patch_activity_main.diff
+++ /dev/null
@@ -1,17 +0,0 @@
---- app/src/main/res/layout/activity_main.xml
-+++ app/src/main/res/layout/activity_main.xml
-@@ -88,6 +88,14 @@
- android:clipToPadding="false"
- android:paddingBottom="80dp">
-
-+
-+
-+
-
- = android.os.Build.VERSION_CODES.S) {
- // Warm accent for date
- context.getColor(if (useLightTheme) android.R.color.system_accent2_700 else android.R.color.system_accent2_100)
- } else {
-- if (useLightTheme) android.graphics.Color.parseColor("#AA555544") else android.graphics.Color.parseColor("#BBDDDDCC")
-+ when (dateColorIdx) {
-+ 2 -> {
-+ android.graphics.Color.rgb(
-+ prefs.getInt("date_color_r", 255),
-+ prefs.getInt("date_color_g", 255),
-+ prefs.getInt("date_color_b", 255)
-+ )
-+ }
-+ 1 -> {
-+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
-+ context.getColor(android.R.color.system_accent2_500)
-+ } else {
-+ android.graphics.Color.YELLOW
-+ }
-+ }
-+ else -> if (useLightTheme) android.graphics.Color.parseColor("#AA555544") else android.graphics.Color.parseColor("#BBDDDDCC")
-+ }
- }
- val alarmColor = if (useDynamicColors && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
diff --git a/patch_main.diff b/patch_main.diff
deleted file mode 100644
index 3b9161d..0000000
--- a/patch_main.diff
+++ /dev/null
@@ -1,44 +0,0 @@
---- app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
-+++ app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
-@@ -107,6 +107,7 @@
-
- checkAllPermissions()
- setupSections()
-+ updateLivePreview()
-
- // Setup Changelog
- val versionName = try {
-@@ -150,6 +151,7 @@
- val fab = findViewById(R.id.fab_update)
- fab.setOnClickListener {
- updateWidget()
-+ updateLivePreview()
- }
-
- // Apply navigation bar insets to FAB so it doesn't overlap gesture nav
-@@ -187,6 +189,18 @@
- })
- }
-
-+ private fun updateLivePreview() {
-+ val previewContainer = findViewById(R.id.preview_container)
-+ try {
-+ val remoteViews = AwidgetProvider.Companion.buildAppWidgetRemoteViews(this, AwidgetProvider.UpdateMode.FULL)
-+ val view = remoteViews.apply(this, previewContainer)
-+ previewContainer.removeAllViews()
-+ previewContainer.addView(view)
-+ } catch (e: Exception) {
-+ e.printStackTrace()
-+ }
-+ }
-+
- private fun checkAllPermissions() {
- val cardPermissionList = findViewById(R.id.card_permission_list)
- var widgetNeedsUpdate = false
-@@ -1493,6 +1507,7 @@
- }
-
- private fun updateWidget() {
-+ updateLivePreview()
- // Animation: Subtle Outline Shine
- val fab = findViewById(R.id.fab_update)
diff --git a/patch_strings.diff b/patch_strings.diff
deleted file mode 100644
index 6c0fb73..0000000
--- a/patch_strings.diff
+++ /dev/null
@@ -1,10 +0,0 @@
---- app/src/main/res/values/strings.xml
-+++ app/src/main/res/values/strings.xml
-@@ -26,6 +26,7 @@
- Transparency
- Text Color
- Text Color 2
-+ Date Color
- Outline Color
- Font Style
- Background Color
diff --git a/strings_patch.diff b/strings_patch.diff
deleted file mode 100644
index 3421a05..0000000
--- a/strings_patch.diff
+++ /dev/null
@@ -1,10 +0,0 @@
---- app/src/main/res/values/strings.xml
-+++ app/src/main/res/values/strings.xml
-@@ -25,6 +25,7 @@
- Transparency
- Text Color
- Text Color 2
-+ Date Color
- Outline Color
- Font Style
- Background Color
diff --git a/test_app.sh b/test_app.sh
deleted file mode 100644
index 9b70b7f..0000000
--- a/test_app.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/bash
-./gradlew testDebugUnitTest --tests "com.leanbitlab.lwidget.ColorResolverTest"
diff --git a/update_date_color.sh b/update_date_color.sh
deleted file mode 100644
index 2ac2749..0000000
--- a/update_date_color.sh
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/bin/bash
-
-# Update AwidgetProvider.kt
-cat << 'PATCH1' > awidget_patch.diff
---- app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-+++ app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
-@@ -377,10 +377,12 @@
- val primaryColor = resolveColor(textColorPrimaryIdx, true, useLightTheme)
- val secondaryColor = resolveColor(textColorSecondaryIdx, false, useLightTheme)
-
-+ val dateColorIdx = prefs.getInt("date_color_idx", 0)
-+
- // Slightly distinct colors for date and next alarm
- val dateColor = if (useDynamicColors && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- // Warm accent for date
- context.getColor(if (useLightTheme) android.R.color.system_accent2_700 else android.R.color.system_accent2_100)
- } else {
-- if (useLightTheme) android.graphics.Color.parseColor("#AA555544") else android.graphics.Color.parseColor("#BBDDDDCC")
-+ when (dateColorIdx) {
-+ 2 -> android.graphics.Color.rgb(prefs.getInt("date_color_r", 255), prefs.getInt("date_color_g", 255), prefs.getInt("date_color_b", 255))
-+ 1 -> if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) context.getColor(android.R.color.system_accent2_500) else android.graphics.Color.YELLOW
-+ else -> if (useLightTheme) android.graphics.Color.parseColor("#AA555544") else android.graphics.Color.parseColor("#BBDDDDCC")
-+ }
- }
- val alarmColor = if (useDynamicColors && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
-PATCH1
-
-# Update MainActivity.kt
-cat << 'PATCH2' > main_patch.diff
---- app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
-+++ app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
-@@ -1187,6 +1187,7 @@
- prefs.edit()
- .putInt("text_color_primary_idx", 0)
- .putInt("text_color_secondary_idx", 0)
-+ .putInt("date_color_idx", 0)
- .putInt("outline_color_idx", 0)
- .putInt("bg_color_idx", 0)
- .apply()
-@@ -1226,6 +1227,15 @@
- bindColorSliders(R.id.row_text_color_secondary_custom, "text_color_secondary")
- secondarySliderRow.visibility = if (prefs.getInt("text_color_secondary_idx", 0) == 2) View.VISIBLE else View.GONE
-
-+ // Date Color
-+ val dateSliderRow = findViewById(R.id.row_date_color_custom)
-+ bindSelector(R.id.row_date_color, getString(R.string.section_date_color), "date_color_idx", colorOptions, 0) { idx ->
-+ dateSliderRow.visibility = if (idx == 2) View.VISIBLE else View.GONE
-+ if (idx != 2) updateWidget()
-+ }
-+ bindColorSliders(R.id.row_date_color_custom, "date_color")
-+ dateSliderRow.visibility = if (prefs.getInt("date_color_idx", 0) == 2) View.VISIBLE else View.GONE
-+
- // Outline Color
- val outlineSliderRow = findViewById(R.id.row_outline_color_custom)
- bindSelector(R.id.row_outline_color, getString(R.string.section_outline_color), "outline_color_idx", colorOptions, 0) { idx ->
-@@ -1400,6 +1410,7 @@
- R.id.row_bg_color, R.id.row_bg_color_custom,
- R.id.row_text_color_primary, R.id.row_text_color_primary_custom,
- R.id.row_text_color_secondary, R.id.row_text_color_secondary_custom,
-+ R.id.row_date_color, R.id.row_date_color_custom,
- R.id.row_outline_color, R.id.row_outline_color_custom
- )
- manualColorIds.forEach { id ->
-PATCH2
-
-# Update strings.xml
-cat << 'PATCH3' > strings_patch.diff
---- app/src/main/res/values/strings.xml
-+++ app/src/main/res/values/strings.xml
-@@ -25,6 +25,7 @@
- Transparency
- Text Color
- Text Color 2
-+ Date Color
- Outline Color
- Font Style
- Background Color
-PATCH3
-
-# Update activity_main.xml
-cat << 'PATCH4' > activity_main_patch.diff
---- app/src/main/res/layout/activity_main.xml
-+++ app/src/main/res/layout/activity_main.xml
-@@ -1246,6 +1246,16 @@
- android:layout_height="wrap_content"/>
-
-
-+
-+
-+
-+
-PATCH4
-
-patch -p0 < awidget_patch.diff
-patch -p0 < main_patch.diff
-patch -p0 < strings_patch.diff
-patch -p0 < activity_main_patch.diff