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