From 9104157fd7208395c6a88587d29452b053bd5011 Mon Sep 17 00:00:00 2001
From: LeanBitLab <245915690+LeanBitLab@users.noreply.github.com>
Date: Wed, 13 May 2026 17:03:02 +0000
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20RAM=20usage=20feature=20mirro?=
=?UTF-8?q?ring=20Storage=20functionality?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add `section_ram` to translations and update subset limit
- Add RAM UI toggles and settings to `MainActivity`
- Update all 11 `widget_layout*.xml` files to include `text_ram`
- Dynamically fetch device free RAM in `AwidgetProvider` via `ActivityManager`
- Fix Robolectric unit test flake in `StepCounterServiceTest`
---
.../com/leanbitlab/lwidget/AwidgetProvider.kt | 40 ++++++++++++++++
.../com/leanbitlab/lwidget/MainActivity.kt | 40 +++++++++++-----
app/src/main/res/layout/activity_main.xml | 47 +++++++++++++++++++
app/src/main/res/layout/widget_layout.xml | 11 +++++
.../main/res/layout/widget_layout_black.xml | 11 +++++
.../res/layout/widget_layout_condensed.xml | 11 +++++
.../layout/widget_layout_condensed_light.xml | 11 +++++
.../main/res/layout/widget_layout_cursive.xml | 11 +++++
.../main/res/layout/widget_layout_light.xml | 11 +++++
.../main/res/layout/widget_layout_medium.xml | 11 +++++
.../main/res/layout/widget_layout_mono.xml | 11 +++++
.../main/res/layout/widget_layout_serif.xml | 11 +++++
.../res/layout/widget_layout_smallcaps.xml | 11 +++++
.../main/res/layout/widget_layout_thin.xml | 11 +++++
app/src/main/res/values/strings.xml | 3 +-
.../lwidget/StepCounterServiceTest.kt | 14 +++++-
16 files changed, 250 insertions(+), 15 deletions(-)
diff --git a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt b/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
index 40ccbb4..435833a 100644
--- a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
+++ b/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt
@@ -269,6 +269,9 @@ class AwidgetProvider : AppWidgetProvider() {
val showStorage = prefs.getBoolean("show_storage", false)
val sizeStorage = prefs.getFloat("size_storage", 14f)
+ val showRam = prefs.getBoolean("show_ram", false)
+ val sizeRam = prefs.getFloat("size_ram", 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
@@ -477,6 +480,7 @@ class AwidgetProvider : AppWidgetProvider() {
}
if (showData) updateDataUsage(context, tickViews, prefs)
if (showStorage) updateStorageStats(context, tickViews, prefs)
+ if (showRam) updateRamStats(context, tickViews, prefs)
return tickViews
} else if (mode == UpdateMode.CALENDAR_ONLY) {
val calViews = RemoteViews(context.packageName, layoutId)
@@ -686,6 +690,14 @@ class AwidgetProvider : AppWidgetProvider() {
updateStorageStats(context, views, prefs)
}
+ // --- RAM ---
+ views.setViewVisibility(R.id.text_ram, if (showRam) android.view.View.VISIBLE else android.view.View.GONE)
+ if (showRam) {
+ views.setTextViewTextSize(R.id.text_ram, android.util.TypedValue.COMPLEX_UNIT_SP, sizeRam)
+ views.setTextColor(R.id.text_ram, secondaryColor)
+ updateRamStats(context, views, prefs)
+ }
+
// --- Step Counter ---
views.setViewVisibility(R.id.text_steps, if (showSteps) android.view.View.VISIBLE else android.view.View.GONE)
if (showSteps) {
@@ -751,6 +763,7 @@ class AwidgetProvider : AppWidgetProvider() {
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_ram, showRam, sizeRam, "show_ram"),
StackEntry(R.id.text_steps, showSteps, sizeSteps, "show_steps"),
StackEntry(R.id.text_screen_time, showScreenTime, sizeScreenTime, "show_screen_time")
)
@@ -810,6 +823,10 @@ class AwidgetProvider : AppWidgetProvider() {
val storagePendingIntent = PendingIntent.getActivity(context, 3, storageIntent, PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.text_storage, storagePendingIntent)
+ // fallback to internal storage settings if memory card settings not available or device specific
+ val ramPendingIntent = PendingIntent.getActivity(context, 10, Intent(android.provider.Settings.ACTION_INTERNAL_STORAGE_SETTINGS), PendingIntent.FLAG_IMMUTABLE)
+ views.setOnClickPendingIntent(R.id.text_ram, ramPendingIntent)
+
val dataIntent = Intent(android.provider.Settings.ACTION_DATA_USAGE_SETTINGS)
val dataPendingIntent = PendingIntent.getActivity(context, 4, dataIntent, PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.text_data_usage, dataPendingIntent)
@@ -1294,6 +1311,29 @@ class AwidgetProvider : AppWidgetProvider() {
}
}
+ private fun updateRamStats(context: Context, views: RemoteViews, prefs: android.content.SharedPreferences) {
+ try {
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
+ val memoryInfo = android.app.ActivityManager.MemoryInfo()
+ activityManager.getMemoryInfo(memoryInfo)
+ val freeBytes = memoryInfo.availMem
+
+ val gb = freeBytes / (1024f * 1024f * 1024f)
+
+ val gbStr = String.format("%.1f", 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_ram", 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_ram, span)
+ } catch (e: Exception) {
+ views.setTextViewText(R.id.text_ram, "Err")
+ }
+ }
+
private fun loadStepCount(context: Context, views: RemoteViews, prefs: android.content.SharedPreferences) {
try {
val totalSteps = prefs.getFloat("last_total_steps", 0f)
diff --git a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt b/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
index 0b6b125..449d9ae 100644
--- a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
+++ b/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt
@@ -711,6 +711,7 @@ class MainActivity : AppCompatActivity() {
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", false)),
+ ReorderItem("show_ram", getString(R.string.section_ram), prefs.getBoolean("show_ram", false)),
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))
)
@@ -853,6 +854,7 @@ class MainActivity : AppCompatActivity() {
setupWeatherSection()
setupDataUsageSection()
setupStorageSection()
+ setupRamSection()
setupStepsSection()
setupScreenTimeSection()
setupKeepAliveSection()
@@ -1024,6 +1026,18 @@ class MainActivity : AppCompatActivity() {
checkAllPermissions()
}
}
+ private fun setupRamSection() {
+ // RAM
+ bindFoldedSection(
+ R.id.header_ram, R.drawable.ic_storage, getString(R.string.section_ram),
+ R.id.content_ram, R.id.row_ram_toggle,
+ "show_ram", false,
+ sizeRowId = R.id.row_ram_size, prefSizeKey = "size_ram", defSize = 14f, minSize = 10f, maxSize = 74f,
+ isContent = true
+ ).also { it.tag = "ram" }
+ bindToggle(R.id.row_ram_bold, "Bold Text", "bold_ram", false)
+ }
+
private fun setupStorageSection() {
// Storage
bindFoldedSection(
@@ -1394,7 +1408,7 @@ class MainActivity : AppCompatActivity() {
"show_time" to true, "size_time" to 58f,
"show_date" to true, "size_date" to 16f,
"show_battery" to false, "show_temp" to false,
- "show_storage" to false, "show_data_usage" to false,
+ "show_storage" to false, "show_ram" to false, "show_data_usage" to false,
"show_steps" to false, "show_screen_time" to false,
"show_next_alarm" to false, "show_world_clock" to false,
"show_events" to false, "show_tasks" to false,
@@ -1412,7 +1426,7 @@ class MainActivity : AppCompatActivity() {
"show_date" to true, "size_date" to 14f,
"show_battery" to true, "size_battery" to 28f, "bold_battery" to true,
"show_temp" to true, "size_temp" to 18f, "bold_temp" to true,
- "show_storage" to false, "show_data_usage" to false,
+ "show_storage" to false, "show_ram" to false, "show_data_usage" to false,
"show_steps" to false, "show_screen_time" to false,
"show_next_alarm" to true, "size_next_alarm" to 12f,
"show_world_clock" to false,
@@ -1425,7 +1439,7 @@ class MainActivity : AppCompatActivity() {
"date_color_idx" to 2, "date_color_r" to 255, "date_color_g" to 0, "date_color_b" to 180,
"outline_color_idx" to 2, "outline_color_r" to 0, "outline_color_g" to 200, "outline_color_b" to 255,
"bg_color_idx" to 2, "bg_color_r" to 10, "bg_color_g" to 10, "bg_color_b" to 20,
- "widget_right_column_order" to "show_battery,show_temp,show_weather_condition,show_data_usage,show_storage,show_steps,show_screen_time"
+ "widget_right_column_order" to "show_battery,show_temp,show_weather_condition,show_data_usage,show_storage,show_ram,show_steps,show_screen_time"
)),
// Cockpit: green on dark, monospace, info-heavy, terminal look
Preset("cockpit", "Cockpit", mapOf(
@@ -1433,7 +1447,7 @@ class MainActivity : AppCompatActivity() {
"show_date" to true, "size_date" to 14f,
"show_battery" to true, "size_battery" to 18f, "bold_battery" to false,
"show_temp" to true, "size_temp" to 16f, "bold_temp" to false,
- "show_storage" to true, "size_storage" to 14f, "bold_storage" to false,
+ "show_storage" to true, "size_storage" to 14f, "bold_storage" to false, "show_ram" to false, "size_ram" to 14f, "bold_ram" to false,
"show_data_usage" to true, "size_data" to 14f, "bold_data_usage" to false,
"show_steps" to false, "show_screen_time" to false,
"show_next_alarm" to true, "size_next_alarm" to 14f,
@@ -1447,14 +1461,14 @@ class MainActivity : AppCompatActivity() {
"date_color_idx" to 2, "date_color_r" to 0, "date_color_g" to 200, "date_color_b" to 80,
"outline_color_idx" to 2, "outline_color_r" to 0, "outline_color_g" to 120, "outline_color_b" to 40,
"bg_color_idx" to 2, "bg_color_r" to 5, "bg_color_g" to 15, "bg_color_b" to 5,
- "widget_right_column_order" to "show_battery,show_storage,show_data_usage,show_temp,show_weather_condition,show_steps,show_screen_time"
+ "widget_right_column_order" to "show_battery,show_storage,show_ram,show_data_usage,show_temp,show_weather_condition,show_steps,show_screen_time"
)),
// Sunset: warm oranges/gold, serif font, elegant minimal
Preset("sunset", "Sunset", mapOf(
"show_time" to true, "size_time" to 54f,
"show_date" to true, "size_date" to 18f,
"show_battery" to true, "size_battery" to 24f, "bold_battery" to true,
- "show_temp" to false, "show_storage" to false,
+ "show_temp" to false, "show_storage" to false, "show_ram" to false,
"show_data_usage" to false, "show_steps" to false,
"show_screen_time" to false,
"show_next_alarm" to true, "size_next_alarm" to 14f,
@@ -1467,14 +1481,14 @@ class MainActivity : AppCompatActivity() {
"text_color_secondary_idx" to 2, "text_color_secondary_r" to 230, "text_color_secondary_g" to 140, "text_color_secondary_b" to 60,
"date_color_idx" to 2, "date_color_r" to 255, "date_color_g" to 120, "date_color_b" to 50,
"bg_color_idx" to 2, "bg_color_r" to 30, "bg_color_g" to 15, "bg_color_b" to 8,
- "widget_right_column_order" to "show_battery,show_temp,show_weather_condition,show_data_usage,show_storage,show_steps,show_screen_time"
+ "widget_right_column_order" to "show_battery,show_temp,show_weather_condition,show_data_usage,show_storage,show_ram,show_steps,show_screen_time"
)),
// Monochrome: white outline, all white text, medium font, classic layout
Preset("monochrome", "Monochrome", mapOf(
"show_time" to true, "size_time" to 48f,
"show_date" to true, "size_date" to 14f,
"show_battery" to true, "size_battery" to 22f, "bold_battery" to false,
- "show_temp" to false, "show_storage" to true, "size_storage" to 14f,
+ "show_temp" to false, "show_storage" to true, "size_storage" to 14f, "show_ram" to false, "size_ram" to 14f,
"show_data_usage" to false, "show_steps" to false,
"show_screen_time" to false,
"show_next_alarm" to true, "size_next_alarm" to 14f,
@@ -1488,14 +1502,14 @@ class MainActivity : AppCompatActivity() {
"date_color_idx" to 2, "date_color_r" to 200, "date_color_g" to 200, "date_color_b" to 200,
"outline_color_idx" to 2, "outline_color_r" to 100, "outline_color_g" to 100, "outline_color_b" to 100,
"bg_color_idx" to 2, "bg_color_r" to 25, "bg_color_g" to 25, "bg_color_b" to 25,
- "widget_right_column_order" to "show_battery,show_storage,show_temp,show_weather_condition,show_data_usage,show_steps,show_screen_time"
+ "widget_right_column_order" to "show_battery,show_storage,show_ram,show_temp,show_weather_condition,show_data_usage,show_steps,show_screen_time"
)),
// Snowfall: icy blues, light font, airy feel
Preset("snowfall", "Snowfall", mapOf(
"show_time" to true, "size_time" to 60f,
"show_date" to true, "size_date" to 16f,
"show_battery" to false, "show_temp" to true, "size_temp" to 20f, "bold_temp" to false,
- "show_storage" to false, "show_data_usage" to false,
+ "show_storage" to false, "show_ram" to false, "show_data_usage" to false,
"show_steps" to false, "show_screen_time" to false,
"show_next_alarm" to false,
"show_world_clock" to false,
@@ -1507,7 +1521,7 @@ class MainActivity : AppCompatActivity() {
"text_color_secondary_idx" to 2, "text_color_secondary_r" to 130, "text_color_secondary_g" to 180, "text_color_secondary_b" to 230,
"date_color_idx" to 2, "date_color_r" to 100, "date_color_g" to 170, "date_color_b" to 255,
"bg_color_idx" to 2, "bg_color_r" to 10, "bg_color_g" to 20, "bg_color_b" to 40,
- "widget_right_column_order" to "show_temp,show_battery,show_weather_condition,show_data_usage,show_storage,show_steps,show_screen_time"
+ "widget_right_column_order" to "show_temp,show_battery,show_weather_condition,show_data_usage,show_storage,show_ram,show_steps,show_screen_time"
))
)
@@ -1704,10 +1718,10 @@ class MainActivity : AppCompatActivity() {
// 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")
+ it.isChecked && (it.tag == "battery" || it.tag == "weather_condition" || it.tag == "temp" || it.tag == "data" || it.tag == "storage" || it.tag == "ram")
}
- if (subsetCount > 5) {
+ if (subsetCount > 6) {
com.google.android.material.snackbar.Snackbar.make(
findViewById(R.id.fab_update),
getString(R.string.error_max_subset_items),
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 60067c7..4b33c83 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1119,6 +1119,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Next Alarm
World Clock
Storage
+ RAM
Tasks
Step Counter
Keep Alive
@@ -42,7 +43,7 @@
Please grant Usage Access for Lwidget
- Max 5 of Battery, Weather, Temp, Data, Storage allowed.
+ Max 6 of Battery, Weather, Temp, Data, Storage, RAM allowed.
No Perm
Err
diff --git a/app/src/test/java/com/leanbitlab/lwidget/StepCounterServiceTest.kt b/app/src/test/java/com/leanbitlab/lwidget/StepCounterServiceTest.kt
index efdbf96..45dc41e 100644
--- a/app/src/test/java/com/leanbitlab/lwidget/StepCounterServiceTest.kt
+++ b/app/src/test/java/com/leanbitlab/lwidget/StepCounterServiceTest.kt
@@ -26,7 +26,10 @@ class StepCounterServiceTest {
val context = ApplicationProvider.getApplicationContext()
prefs = context.getSharedPreferences("com.leanbitlab.lwidget.PREFS", Context.MODE_PRIVATE)
prefs.edit().clear().apply()
+ // Create service but don't call onCreate yet so tests can set initial SharedPreferences
+ }
+ private fun startService() {
service = Robolectric.buildService(StepCounterService::class.java).create().get()
}
@@ -34,7 +37,11 @@ class StepCounterServiceTest {
val constructor = SensorEvent::class.java.declaredConstructors.first { it.parameterCount == 1 }
constructor.isAccessible = true
val event = constructor.newInstance(1) as SensorEvent
- event.values[0] = steps
+ val valuesField = android.hardware.SensorEvent::class.java.getField("values")
+ valuesField.isAccessible = true
+ val values = FloatArray(1)
+ values[0] = steps
+ valuesField.set(event, values)
return event
}
@@ -49,6 +56,7 @@ class StepCounterServiceTest {
.apply()
// Hardware rebooted, now sensor says 50 steps
+ startService()
val event = createMockSensorEvent(50f)
service.onSensorChanged(event)
@@ -67,6 +75,7 @@ class StepCounterServiceTest {
.apply()
// First reboot, sensor goes from 1000 -> 50
+ startService()
service.onSensorChanged(createMockSensorEvent(50f))
// Expected: baseline = 50 - (1000 - 200) = -750
@@ -98,6 +107,7 @@ class StepCounterServiceTest {
.putFloat("step_baseline", 100f)
.apply()
+ startService()
val event = createMockSensorEvent(600f)
service.onSensorChanged(event)
@@ -117,6 +127,7 @@ class StepCounterServiceTest {
.apply()
// Step increases to 1050
+ startService()
val event = createMockSensorEvent(1050f)
service.onSensorChanged(event)
@@ -135,6 +146,7 @@ class StepCounterServiceTest {
.putBoolean("was_called", false) // marker to check if prefs was edited
.apply()
+ startService()
service.onSensorChanged(null)
// Ensure nothing was updated or changed