From 34314ec8da159e82dc14afe148fa4b44d1fd6cfd Mon Sep 17 00:00:00 2001 From: md sohail Date: Sun, 8 Feb 2026 18:50:25 +0530 Subject: [PATCH 1/5] Sync local changes with remote --- app/src/main/AndroidManifest.xml | 39 ++++ .../wlednativeandroid/model/wledapi/Preset.kt | 12 ++ .../service/api/DeviceApi.kt | 12 ++ .../widget/PresetWidgetConfigureActivity.kt | 144 ++++++++++++++ .../widget/PresetWidgetProvider.kt | 149 +++++++++++++++ .../widget/PresetWidgetService.kt | 179 ++++++++++++++++++ .../widget/SmallPresetWidgetProvider.kt | 27 +++ .../main/res/drawable/ic_dot_indicator.xml | 8 + .../main/res/drawable/widget_background.xml | 11 ++ .../res/drawable/widget_button_selected.xml | 11 ++ .../drawable/widget_preset_small_preview.xml | 34 ++++ app/src/main/res/drawable/widget_preview.xml | 11 ++ app/src/main/res/layout/widget_preset.xml | 39 ++++ .../res/layout/widget_preset_button_item.xml | 26 +++ .../res/layout/widget_preset_horizontal.xml | 28 +++ .../main/res/layout/widget_preset_item.xml | 28 +++ app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/widget_preset_info.xml | 10 + .../main/res/xml/widget_preset_small_info.xml | 12 ++ gradlew | 0 21 files changed, 785 insertions(+) create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Preset.kt create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt create mode 100644 app/src/main/res/drawable/ic_dot_indicator.xml create mode 100644 app/src/main/res/drawable/widget_background.xml create mode 100644 app/src/main/res/drawable/widget_button_selected.xml create mode 100644 app/src/main/res/drawable/widget_preset_small_preview.xml create mode 100644 app/src/main/res/drawable/widget_preview.xml create mode 100644 app/src/main/res/layout/widget_preset.xml create mode 100644 app/src/main/res/layout/widget_preset_button_item.xml create mode 100644 app/src/main/res/layout/widget_preset_horizontal.xml create mode 100644 app/src/main/res/layout/widget_preset_item.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/xml/widget_preset_info.xml create mode 100644 app/src/main/res/xml/widget_preset_small_info.xml mode change 100755 => 100644 gradlew diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7dd27bc1..a4401c82 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Preset.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Preset.kt new file mode 100644 index 00000000..fbba691a --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Preset.kt @@ -0,0 +1,12 @@ +package ca.cgagnier.wlednativeandroid.model.wledapi + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Preset( + @Json(name = "n") val name: String = "", + @Json(name = "on") val on: Boolean? = null, + @Json(name = "bri") val brightness: Int? = null, + @Json(name = "mainseg") val mainSegment: Int? = null +) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt index 4ff6eb96..dc66cc10 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt @@ -2,6 +2,8 @@ package ca.cgagnier.wlednativeandroid.service.api import ca.cgagnier.wlednativeandroid.model.Device import ca.cgagnier.wlednativeandroid.model.wledapi.Info +import ca.cgagnier.wlednativeandroid.model.wledapi.Preset +import ca.cgagnier.wlednativeandroid.model.wledapi.State import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.ResponseBody @@ -12,12 +14,22 @@ import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part +import retrofit2.http.Body import java.util.concurrent.TimeUnit interface DeviceApi { @GET("json/info") suspend fun getInfo(): Response + @GET("presets.json") + suspend fun getPresets(): Response> + + @GET("json/state") + suspend fun getState(): Response + + @POST("json/state") + suspend fun postState(@Body state: State): Response + @Multipart @POST("update") suspend fun updateDevice( diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt new file mode 100644 index 00000000..7560f717 --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt @@ -0,0 +1,144 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ca.cgagnier.wlednativeandroid.model.Device +import ca.cgagnier.wlednativeandroid.repository.DeviceRepository +import ca.cgagnier.wlednativeandroid.ui.theme.WLEDNativeTheme +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import androidx.lifecycle.ViewModel + +@AndroidEntryPoint +class PresetWidgetConfigureActivity : ComponentActivity() { + + private val viewModel: PresetWidgetConfigureViewModel by viewModels() + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setResult(RESULT_CANCELED) + + val intent = intent + val extras = intent.extras + if (extras != null) { + appWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + setContent { + WLEDNativeTheme { + DeviceSelectionScreen(viewModel) { device -> + saveDevicePref(this, appWidgetId, device) + + val resultValue = Intent() + resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + setResult(RESULT_OK, resultValue) + finish() + } + } + } + } + + companion object { + private const val PREFS_NAME = "ca.cgagnier.wlednativeandroid.widget.PresetWidget" + private const val PREF_PREFIX_KEY = "appwidget_" + + internal fun saveDevicePref(context: Context, appWidgetId: Int, device: Device) { + val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit() + prefs.putString(PREF_PREFIX_KEY + appWidgetId, device.address) + prefs.putString(PREF_PREFIX_KEY + appWidgetId + "_name", device.name) + prefs.apply() + } + + internal fun loadDeviceAddress(context: Context, appWidgetId: Int): String? { + val prefs = context.getSharedPreferences(PREFS_NAME, 0) + return prefs.getString(PREF_PREFIX_KEY + appWidgetId, null) + } + + internal fun loadDeviceName(context: Context, appWidgetId: Int): String { + val prefs = context.getSharedPreferences(PREFS_NAME, 0) + return prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_name", "WLED Device") ?: "WLED Device" + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeviceSelectionScreen(viewModel: PresetWidgetConfigureViewModel, onDeviceSelected: (Device) -> Unit) { + val devices by viewModel.allDevices.collectAsState(initial = emptyList()) + + Scaffold( + topBar = { + TopAppBar(title = { Text("Select WLED Device") }) + } + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + items(devices) { device -> + DeviceItem(device, onDeviceSelected) + } + } + } +} + +@Composable +fun DeviceItem(device: Device, onClick: (Device) -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .clickable { onClick(device) } + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text(text = device.name, style = MaterialTheme.typography.titleMedium) + Text(text = device.address, style = MaterialTheme.typography.bodyMedium) + } + } +} + +@HiltViewModel +class PresetWidgetConfigureViewModel @Inject constructor( + private val deviceRepository: DeviceRepository +) : ViewModel() { + val allDevices: Flow> = deviceRepository.allDevices +} + +val Device.name: String + get() = if (customName.isNotEmpty()) customName else if (originalName.isNotEmpty()) originalName else address diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt new file mode 100644 index 00000000..38f0019d --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt @@ -0,0 +1,149 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.Log +import android.widget.RemoteViews +import ca.cgagnier.wlednativeandroid.R +import ca.cgagnier.wlednativeandroid.model.wledapi.State +import ca.cgagnier.wlednativeandroid.service.api.DeviceApiFactory +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class PresetWidgetProvider : AppWidgetProvider() { + + @Inject + lateinit var deviceApiFactory: DeviceApiFactory + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + for (appWidgetId in appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + if (intent.action == ACTION_TRIGGER_PRESET) { + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + val presetId = intent.getIntExtra(EXTRA_PRESET_ID, -1) + val deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS) + val listId = intent.getIntExtra(EXTRA_LIST_ID, R.id.preset_list) + + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && presetId != -1 && deviceAddress != null) { + triggerPreset(context, deviceAddress, presetId, appWidgetId, listId) + } + } + + if (intent.action == ACTION_REFRESH) { + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + val listId = intent.getIntExtra(EXTRA_LIST_ID, R.id.preset_list) + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + val appWidgetManager = AppWidgetManager.getInstance(context) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) + } + } + } + + private fun triggerPreset(context: Context, deviceAddress: String, presetId: Int, appWidgetId: Int, listId: Int) { + CoroutineScope(Dispatchers.IO).launch { + try { + val api = deviceApiFactory.create(deviceAddress) + val response = api.postState(State(selectedPresetId = presetId)) + + if (response.isSuccessful) { + // Add a small delay to allow device state to update internally if needed + // kotlinx.coroutines.delay(200) + // Wait, I need to import delay if I use it. + // Or just trigger update immediately. + val appWidgetManager = AppWidgetManager.getInstance(context) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) + Log.d(TAG, "Triggered widget update for $appWidgetId") + } + } catch (e: Exception) { + Log.e(TAG, "Error triggering preset", e) + } + } + } + + companion object { + const val ACTION_TRIGGER_PRESET = "ca.cgagnier.wlednativeandroid.widget.ACTION_TRIGGER_PRESET" + const val ACTION_REFRESH = "ca.cgagnier.wlednativeandroid.widget.ACTION_REFRESH" + const val EXTRA_PRESET_ID = "ca.cgagnier.wlednativeandroid.widget.EXTRA_PRESET_ID" + const val EXTRA_DEVICE_ADDRESS = "ca.cgagnier.wlednativeandroid.widget.EXTRA_DEVICE_ADDRESS" + const val EXTRA_LIST_LIMIT = "ca.cgagnier.wlednativeandroid.widget.EXTRA_LIST_LIMIT" + const val EXTRA_LAYOUT_ID = "ca.cgagnier.wlednativeandroid.widget.EXTRA_LAYOUT_ID" + const val EXTRA_LIST_ID = "ca.cgagnier.wlednativeandroid.widget.EXTRA_LIST_ID" + private const val TAG = "PresetWidgetProvider" + + fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + limit: Int = 0, + layoutId: Int = R.layout.widget_preset, + listId: Int = R.id.preset_list, + itemLayoutId: Int = R.layout.widget_preset_item + ) { + val deviceName = PresetWidgetConfigureActivity.loadDeviceName(context, appWidgetId) + val deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + + if (deviceAddress == null) { + return + } + + // Construct the RemoteViews object + val views = RemoteViews(context.packageName, layoutId) + views.setTextViewText(R.id.appwidget_text, deviceName) + + // Set up the collection + val intent = Intent(context, PresetWidgetService::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_LIST_LIMIT, limit) + putExtra(EXTRA_LAYOUT_ID, itemLayoutId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + views.setRemoteAdapter(listId, intent) + views.setEmptyView(listId, R.id.empty_view) + + val refreshIntent = Intent(context, PresetWidgetProvider::class.java).apply { + action = ACTION_REFRESH + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_LIST_ID, listId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + val refreshPendingIntent = PendingIntent.getBroadcast( + context, appWidgetId, refreshIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + views.setOnClickPendingIntent(R.id.empty_view, refreshPendingIntent) + + // Set up pending intent template for items + val toastIntent = Intent(context, PresetWidgetProvider::class.java).apply { + action = ACTION_TRIGGER_PRESET + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_LIST_ID, listId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + val toastPendingIntent = PendingIntent.getBroadcast( + context, 0, toastIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + views.setPendingIntentTemplate(listId, toastPendingIntent) + + appWidgetManager.updateAppWidget(appWidgetId, views) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) + } + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt new file mode 100644 index 00000000..e86c6a81 --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt @@ -0,0 +1,179 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.content.Context +import android.content.Intent +import android.util.Log +import android.widget.RemoteViews +import android.widget.RemoteViewsService +import ca.cgagnier.wlednativeandroid.R +import ca.cgagnier.wlednativeandroid.model.wledapi.Preset +import ca.cgagnier.wlednativeandroid.service.api.DeviceApiFactory +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.runBlocking + +class PresetWidgetService : RemoteViewsService() { + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { + return PresetWidgetRemoteViewsFactory(this.applicationContext, intent) + } +} + +class PresetWidgetRemoteViewsFactory( + private val context: Context, + intent: Intent +) : RemoteViewsService.RemoteViewsFactory { + + private val appWidgetId: Int = intent.getIntExtra( + android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID, + android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID + ) + private val limit: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LIST_LIMIT, 0) + private val itemLayoutId: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LAYOUT_ID, R.layout.widget_preset_item) + private var presets: List> = emptyList() + private val deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface PresetWidgetServiceEntryPoint { + fun deviceApiFactory(): DeviceApiFactory + } + + override fun onCreate() { + // Data loading should ideally happen in onDataSetChanged + } + + private var selectedPresetId: Int = -1 + + override fun onDataSetChanged() { + if (deviceAddress == null) { + Log.e(TAG, "Device address is null") + return + } + + val entryPoint = EntryPointAccessors.fromApplication( + context, + PresetWidgetServiceEntryPoint::class.java + ) + val deviceApiFactory = entryPoint.deviceApiFactory() + + runBlocking { + try { + Log.d(TAG, "Fetching presets for $deviceAddress") + val api = deviceApiFactory.create(deviceAddress) + + // Fetch state to know selected preset + val stateResponse = api.getState() + if (stateResponse.isSuccessful) { + val state = stateResponse.body() + if (state != null) { + selectedPresetId = state.selectedPresetId ?: -1 + // If playlist is active, maybe show that? + // For now just preset. + if (selectedPresetId == -1 && state.selectedPlaylistId != null && state.selectedPlaylistId > 0) { + selectedPresetId = state.selectedPlaylistId + } + } + } + + // Fetch Presets + val response = api.getPresets() + if (response.isSuccessful) { + val presetsMap = response.body() + Log.d(TAG, "Presets fetched: ${presetsMap?.size}") + if (presetsMap != null) { + // Filter out "0" if it exists, as it's usually not a real preset or handled differently? + // Actually presets.json keys are usually "1", "2", etc. + var sortedPresets = presetsMap.entries + .filter { it.key != "0" } + .map { it.key to it.value } + .sortedBy { it.first.toIntOrNull() ?: Int.MAX_VALUE } + + if (limit > 0) { + sortedPresets = sortedPresets.take(limit) + } + presets = sortedPresets + + + } + } else { + Log.e(TAG, "Error fetching presets: ${response.code()}") + } + } catch (e: Exception) { + Log.e(TAG, "Error fetching presets", e) + } + } + } + + // ... (rest of the file) + + + override fun onDestroy() { + presets = emptyList() + } + + override fun getCount(): Int { + return presets.size + } + + + + // ... (rest of class) + + override fun getViewAt(position: Int): RemoteViews { + if (position >= presets.size) return RemoteViews(context.packageName, itemLayoutId) + + val (id, preset) = presets[position] + val views = RemoteViews(context.packageName, itemLayoutId) + + var name = preset.name + if (name.isEmpty()) { + name = "Preset $id" + } + + views.setTextViewText(R.id.preset_name, name) + + if (id.toIntOrNull() == selectedPresetId) { + if (itemLayoutId == R.layout.widget_preset_button_item) { + views.setInt(R.id.widget_item, "setBackgroundResource", R.drawable.widget_button_selected) + } else { + views.setViewVisibility(R.id.preset_indicator, android.view.View.VISIBLE) + } + } else { + if (itemLayoutId == R.layout.widget_preset_button_item) { + views.setInt(R.id.widget_item, "setBackgroundResource", R.drawable.widget_background) + } else { + views.setViewVisibility(R.id.preset_indicator, android.view.View.GONE) + } + } + + val fillInIntent = Intent().apply { + putExtra(PresetWidgetProvider.EXTRA_PRESET_ID, id.toIntOrNull() ?: -1) + putExtra(PresetWidgetProvider.EXTRA_DEVICE_ADDRESS, deviceAddress) + } + views.setOnClickFillInIntent(R.id.widget_item, fillInIntent) + + return views + } + + override fun getLoadingView(): RemoteViews? { + return null + } + + override fun getViewTypeCount(): Int { + return 1 + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun hasStableIds(): Boolean { + return true + } + + companion object { + private const val TAG = "PresetWidgetService" + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt new file mode 100644 index 00000000..c95979cb --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt @@ -0,0 +1,27 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import ca.cgagnier.wlednativeandroid.R + +class SmallPresetWidgetProvider : AppWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + for (appWidgetId in appWidgetIds) { + PresetWidgetProvider.updateAppWidget( + context, + appWidgetManager, + appWidgetId, + limit = 3, + layoutId = R.layout.widget_preset_horizontal, + listId = R.id.preset_grid, + itemLayoutId = R.layout.widget_preset_button_item + ) + } + } +} diff --git a/app/src/main/res/drawable/ic_dot_indicator.xml b/app/src/main/res/drawable/ic_dot_indicator.xml new file mode 100644 index 00000000..136435fa --- /dev/null +++ b/app/src/main/res/drawable/ic_dot_indicator.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/widget_background.xml b/app/src/main/res/drawable/widget_background.xml new file mode 100644 index 00000000..8fce07ac --- /dev/null +++ b/app/src/main/res/drawable/widget_background.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/widget_button_selected.xml b/app/src/main/res/drawable/widget_button_selected.xml new file mode 100644 index 00000000..2c223c3a --- /dev/null +++ b/app/src/main/res/drawable/widget_button_selected.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/widget_preset_small_preview.xml b/app/src/main/res/drawable/widget_preset_small_preview.xml new file mode 100644 index 00000000..cfb04639 --- /dev/null +++ b/app/src/main/res/drawable/widget_preset_small_preview.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/widget_preview.xml b/app/src/main/res/drawable/widget_preview.xml new file mode 100644 index 00000000..ed550e79 --- /dev/null +++ b/app/src/main/res/drawable/widget_preview.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/layout/widget_preset.xml b/app/src/main/res/layout/widget_preset.xml new file mode 100644 index 00000000..34f20e11 --- /dev/null +++ b/app/src/main/res/layout/widget_preset.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/widget_preset_button_item.xml b/app/src/main/res/layout/widget_preset_button_item.xml new file mode 100644 index 00000000..8520bca2 --- /dev/null +++ b/app/src/main/res/layout/widget_preset_button_item.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/layout/widget_preset_horizontal.xml b/app/src/main/res/layout/widget_preset_horizontal.xml new file mode 100644 index 00000000..75247a7e --- /dev/null +++ b/app/src/main/res/layout/widget_preset_horizontal.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/layout/widget_preset_item.xml b/app/src/main/res/layout/widget_preset_item.xml new file mode 100644 index 00000000..6e253da8 --- /dev/null +++ b/app/src/main/res/layout/widget_preset_item.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..1aad025b --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 8dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8dfc145e..ec41e28c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,4 +113,6 @@ Open Dismiss Some of your devices are hidden + Presets + No presets found diff --git a/app/src/main/res/xml/widget_preset_info.xml b/app/src/main/res/xml/widget_preset_info.xml new file mode 100644 index 00000000..17fbfa41 --- /dev/null +++ b/app/src/main/res/xml/widget_preset_info.xml @@ -0,0 +1,10 @@ + + diff --git a/app/src/main/res/xml/widget_preset_small_info.xml b/app/src/main/res/xml/widget_preset_small_info.xml new file mode 100644 index 00000000..718522f4 --- /dev/null +++ b/app/src/main/res/xml/widget_preset_small_info.xml @@ -0,0 +1,12 @@ + + diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 From c5483b71e103230e000ed3026ba1c52fc360ad2a Mon Sep 17 00:00:00 2001 From: Md Sohail <83342804+sohailxi@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:04:42 +0530 Subject: [PATCH 2/5] Update README.md Signed-off-by: Md Sohail <83342804+sohailxi@users.noreply.github.com> --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 126fdaf2..1d73c9df 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,6 @@ Google and Google specifically disclaims all warranties as to its quality, merchantability, or fitness for a particular purpose. Google Play and the Google Play logo are trademarks of Google LLC. + + +I just added some home screen widgets so that i can control my wled light just from my home screen without even opening the app. From df6837e2ea2af332d0a968aeec33116d114471b7 Mon Sep 17 00:00:00 2001 From: Antigravity AI Date: Fri, 1 May 2026 11:59:45 +0530 Subject: [PATCH 3/5] feat: Restore widgets and improve preset widget robustness --- .gitignore | 1 + app/src/main/AndroidManifest.xml | 37 +++++++++ .../service/api/DeviceApi.kt | 5 +- .../widget/PresetWidgetConfigureActivity.kt | 4 +- .../widget/PresetWidgetService.kt | 78 +++++++++++++++---- .../widget/WledWidgetConfigureActivity.kt | 4 +- gradlew | 0 7 files changed, 107 insertions(+), 22 deletions(-) mode change 100644 => 100755 gradlew diff --git a/.gitignore b/.gitignore index a05276e7..fb8e9138 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ out/ # Gradle files .gradle/ +.gradle_home/ build/ # Local configuration file (sdk path, etc) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 34d21f24..4cb363a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,30 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt index ceee487e..794f0c4a 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt @@ -3,6 +3,7 @@ package ca.cgagnier.wlednativeandroid.service.api import ca.cgagnier.wlednativeandroid.model.Device import ca.cgagnier.wlednativeandroid.model.wledapi.Info import ca.cgagnier.wlednativeandroid.model.wledapi.JsonPost +import ca.cgagnier.wlednativeandroid.model.wledapi.Preset import ca.cgagnier.wlednativeandroid.model.wledapi.State import okhttp3.MultipartBody import okhttp3.OkHttpClient @@ -15,7 +16,6 @@ import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part -import retrofit2.http.Body import java.util.concurrent.TimeUnit interface DeviceApi { @@ -31,6 +31,9 @@ interface DeviceApi { @POST("json/state") suspend fun postState(@Body state: State): Response + @POST("json/state") + suspend fun postJson(@Body jsonPost: JsonPost): Response + @Multipart @POST("update") suspend fun updateDevice(@Part binaryFile: MultipartBody.Part): Response diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt index 7560f717..e3dd236e 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt @@ -98,7 +98,7 @@ class PresetWidgetConfigureActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DeviceSelectionScreen(viewModel: PresetWidgetConfigureViewModel, onDeviceSelected: (Device) -> Unit) { +private fun DeviceSelectionScreen(viewModel: PresetWidgetConfigureViewModel, onDeviceSelected: (Device) -> Unit) { val devices by viewModel.allDevices.collectAsState(initial = emptyList()) Scaffold( @@ -119,7 +119,7 @@ fun DeviceSelectionScreen(viewModel: PresetWidgetConfigureViewModel, onDeviceSel } @Composable -fun DeviceItem(device: Device, onClick: (Device) -> Unit) { +private fun DeviceItem(device: Device, onClick: (Device) -> Unit) { Card( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt index e86c6a81..2455456c 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt @@ -12,6 +12,8 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import kotlinx.coroutines.runBlocking class PresetWidgetService : RemoteViewsService() { @@ -32,12 +34,13 @@ class PresetWidgetRemoteViewsFactory( private val limit: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LIST_LIMIT, 0) private val itemLayoutId: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LAYOUT_ID, R.layout.widget_preset_item) private var presets: List> = emptyList() - private val deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + private var deviceAddress: String? = null @EntryPoint @InstallIn(SingletonComponent::class) interface PresetWidgetServiceEntryPoint { fun deviceApiFactory(): DeviceApiFactory + fun moshi(): Moshi } override fun onCreate() { @@ -47,6 +50,7 @@ class PresetWidgetRemoteViewsFactory( private var selectedPresetId: Int = -1 override fun onDataSetChanged() { + deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) if (deviceAddress == null) { Log.e(TAG, "Device address is null") return @@ -57,11 +61,12 @@ class PresetWidgetRemoteViewsFactory( PresetWidgetServiceEntryPoint::class.java ) val deviceApiFactory = entryPoint.deviceApiFactory() + val moshi = entryPoint.moshi() runBlocking { try { Log.d(TAG, "Fetching presets for $deviceAddress") - val api = deviceApiFactory.create(deviceAddress) + val api = deviceApiFactory.create(deviceAddress!!) // Fetch state to know selected preset val stateResponse = api.getState() @@ -69,8 +74,6 @@ class PresetWidgetRemoteViewsFactory( val state = stateResponse.body() if (state != null) { selectedPresetId = state.selectedPresetId ?: -1 - // If playlist is active, maybe show that? - // For now just preset. if (selectedPresetId == -1 && state.selectedPlaylistId != null && state.selectedPlaylistId > 0) { selectedPresetId = state.selectedPlaylistId } @@ -83,26 +86,64 @@ class PresetWidgetRemoteViewsFactory( val presetsMap = response.body() Log.d(TAG, "Presets fetched: ${presetsMap?.size}") if (presetsMap != null) { - // Filter out "0" if it exists, as it's usually not a real preset or handled differently? - // Actually presets.json keys are usually "1", "2", etc. - var sortedPresets = presetsMap.entries - .filter { it.key != "0" } - .map { it.key to it.value } - .sortedBy { it.first.toIntOrNull() ?: Int.MAX_VALUE } - - if (limit > 0) { - sortedPresets = sortedPresets.take(limit) - } - presets = sortedPresets - - + savePresetsToCache(presetsMap, moshi) + processPresets(presetsMap) } } else { Log.e(TAG, "Error fetching presets: ${response.code()}") + loadPresetsFromCache(moshi) } } catch (e: Exception) { Log.e(TAG, "Error fetching presets", e) + loadPresetsFromCache(moshi) + } + } + } + + private fun processPresets(presetsMap: Map) { + var sortedPresets = presetsMap.entries + .filter { it.key != "0" } + .map { it.key to it.value } + .sortedBy { it.first.toIntOrNull() ?: Int.MAX_VALUE } + + if (limit > 0) { + sortedPresets = sortedPresets.take(limit) + } + presets = sortedPresets + } + + private fun savePresetsToCache(presetsMap: Map, moshi: Moshi) { + try { + val type = Types.newParameterizedType(Map::class.java, String::class.java, Preset::class.java) + val adapter = moshi.adapter>(type) + val json = adapter.toJson(presetsMap) + + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit() + prefs.putString(PREF_CACHE_PRESETS + appWidgetId, json) + prefs.putInt(PREF_CACHE_SELECTED_ID + appWidgetId, selectedPresetId) + prefs.apply() + } catch (e: Exception) { + Log.e(TAG, "Failed to save presets to cache", e) + } + } + + private fun loadPresetsFromCache(moshi: Moshi) { + try { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val json = prefs.getString(PREF_CACHE_PRESETS + appWidgetId, null) + selectedPresetId = prefs.getInt(PREF_CACHE_SELECTED_ID + appWidgetId, -1) + + if (json != null) { + val type = Types.newParameterizedType(Map::class.java, String::class.java, Preset::class.java) + val adapter = moshi.adapter>(type) + val presetsMap = adapter.fromJson(json) + if (presetsMap != null) { + Log.d(TAG, "Loaded presets from cache for $appWidgetId") + processPresets(presetsMap) + } } + } catch (e: Exception) { + Log.e(TAG, "Failed to load presets from cache", e) } } @@ -175,5 +216,8 @@ class PresetWidgetRemoteViewsFactory( companion object { private const val TAG = "PresetWidgetService" + private const val PREFS_NAME = "ca.cgagnier.wlednativeandroid.widget.PresetWidgetCache" + private const val PREF_CACHE_PRESETS = "cache_presets_" + private const val PREF_CACHE_SELECTED_ID = "cache_selected_id_" } } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt index 6374a0ea..2b495586 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt @@ -114,7 +114,7 @@ class WledWidgetConfigureActivity : ComponentActivity() { // TODO: Style the widget selection screen better so it looks more like the rest of the WLED app. @Composable -fun ConfigurationScreen(devices: List, onDeviceSelected: (Device) -> Unit) { +private fun ConfigurationScreen(devices: List, onDeviceSelected: (Device) -> Unit) { Scaffold( topBar = { TopAppBar( @@ -138,7 +138,7 @@ fun ConfigurationScreen(devices: List, onDeviceSelected: (Device) -> Uni } @Composable -fun DeviceItem(device: Device, onClick: () -> Unit) { +private fun DeviceItem(device: Device, onClick: () -> Unit) { Column( modifier = Modifier .fillMaxWidth() diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 1504d94720007b6308d4f182470ab7e7a1184b16 Mon Sep 17 00:00:00 2001 From: Md Sohail Date: Wed, 27 May 2026 21:33:01 +0530 Subject: [PATCH 4/5] fixed only the power button widget working --- .../service/api/DeviceApi.kt | 18 +++ .../widget/PresetWidgetConfigureActivity.kt | 46 ++++-- .../widget/PresetWidgetProvider.kt | 68 ++++++--- .../widget/PresetWidgetService.kt | 142 +++++++++++------- .../widget/SmallPresetWidgetProvider.kt | 21 ++- .../widget/WledWidgetManager.kt | 119 +++++++++++++++ 6 files changed, 312 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt index 794f0c4a..39be5369 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt @@ -64,6 +64,24 @@ class DeviceApiFactory(private val client: OkHttpClient) { return createForDeviceAndClient(baseUrl, client) } + /** + * Create a new DeviceApi instance with a custom timeout. + * + * @param address The address of a device to create the API for. + * @param timeout The custom timeout in seconds. + */ + fun create(address: String, timeout: Long): DeviceApi { + val baseUrl = if (!address.startsWith("http://") && !address.startsWith("https://")) { + "http://$address/" + } else { + address + } + val customClient = client.newBuilder().connectTimeout(timeout, TimeUnit.SECONDS) + .readTimeout(timeout, TimeUnit.SECONDS).writeTimeout(timeout, TimeUnit.SECONDS).build() + + return createForDeviceAndClient(baseUrl, customClient) + } + /** * Create a new DeviceApi instance for a device. * diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt index e3dd236e..f4b88338 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt @@ -26,6 +26,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import ca.cgagnier.wlednativeandroid.R import ca.cgagnier.wlednativeandroid.model.Device import ca.cgagnier.wlednativeandroid.repository.DeviceRepository import ca.cgagnier.wlednativeandroid.ui.theme.WLEDNativeTheme @@ -33,7 +35,6 @@ import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import javax.inject.Inject -import androidx.lifecycle.ViewModel @AndroidEntryPoint class PresetWidgetConfigureActivity : ComponentActivity() { @@ -50,7 +51,8 @@ class PresetWidgetConfigureActivity : ComponentActivity() { val extras = intent.extras if (extras != null) { appWidgetId = extras.getInt( - AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID, ) } @@ -64,6 +66,24 @@ class PresetWidgetConfigureActivity : ComponentActivity() { DeviceSelectionScreen(viewModel) { device -> saveDevicePref(this, appWidgetId, device) + val appWidgetManager = AppWidgetManager.getInstance(this) + val info = appWidgetManager.getAppWidgetInfo(appWidgetId) + val providerClass = info?.provider?.className + if (providerClass == SmallPresetWidgetProvider::class.java.name) { + PresetWidgetProvider.updateAppWidget( + this, + appWidgetManager, + appWidgetId, + limit = 3, + layoutId = R.layout.widget_preset_horizontal, + listId = R.id.preset_grid, + itemLayoutId = R.layout.widget_preset_button_item, + titleViewId = null, + ) + } else { + PresetWidgetProvider.updateAppWidget(this, appWidgetManager, appWidgetId) + } + val resultValue = Intent() resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) setResult(RESULT_OK, resultValue) @@ -88,9 +108,9 @@ class PresetWidgetConfigureActivity : ComponentActivity() { val prefs = context.getSharedPreferences(PREFS_NAME, 0) return prefs.getString(PREF_PREFIX_KEY + appWidgetId, null) } - + internal fun loadDeviceName(context: Context, appWidgetId: Int): String { - val prefs = context.getSharedPreferences(PREFS_NAME, 0) + val prefs = context.getSharedPreferences(PREFS_NAME, 0) return prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_name", "WLED Device") ?: "WLED Device" } } @@ -104,12 +124,12 @@ private fun DeviceSelectionScreen(viewModel: PresetWidgetConfigureViewModel, onD Scaffold( topBar = { TopAppBar(title = { Text("Select WLED Device") }) - } + }, ) { padding -> LazyColumn( modifier = Modifier .fillMaxSize() - .padding(padding) + .padding(padding), ) { items(devices) { device -> DeviceItem(device, onDeviceSelected) @@ -124,7 +144,7 @@ private fun DeviceItem(device: Device, onClick: (Device) -> Unit) { modifier = Modifier .fillMaxWidth() .padding(8.dp) - .clickable { onClick(device) } + .clickable { onClick(device) }, ) { Column(modifier = Modifier.padding(16.dp)) { Text(text = device.name, style = MaterialTheme.typography.titleMedium) @@ -134,11 +154,15 @@ private fun DeviceItem(device: Device, onClick: (Device) -> Unit) { } @HiltViewModel -class PresetWidgetConfigureViewModel @Inject constructor( - private val deviceRepository: DeviceRepository -) : ViewModel() { +class PresetWidgetConfigureViewModel @Inject constructor(private val deviceRepository: DeviceRepository) : ViewModel() { val allDevices: Flow> = deviceRepository.allDevices } val Device.name: String - get() = if (customName.isNotEmpty()) customName else if (originalName.isNotEmpty()) originalName else address + get() = if (customName.isNotEmpty()) { + customName + } else if (originalName.isNotEmpty()) { + originalName + } else { + address + } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt index 38f0019d..565ee817 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt @@ -23,11 +23,7 @@ class PresetWidgetProvider : AppWidgetProvider() { @Inject lateinit var deviceApiFactory: DeviceApiFactory - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } @@ -36,7 +32,10 @@ class PresetWidgetProvider : AppWidgetProvider() { override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) if (intent.action == ACTION_TRIGGER_PRESET) { - val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + val appWidgetId = intent.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID, + ) val presetId = intent.getIntExtra(EXTRA_PRESET_ID, -1) val deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS) val listId = intent.getIntExtra(EXTRA_LIST_ID, R.id.preset_list) @@ -47,15 +46,20 @@ class PresetWidgetProvider : AppWidgetProvider() { } if (intent.action == ACTION_REFRESH) { - val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + val appWidgetId = intent.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID, + ) val listId = intent.getIntExtra(EXTRA_LIST_ID, R.id.preset_list) if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + PresetWidgetRemoteViewsFactory.forceRefresh(appWidgetId) val appWidgetManager = AppWidgetManager.getInstance(context) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) } } } + @Suppress("TooGenericExceptionCaught") private fun triggerPreset(context: Context, deviceAddress: String, presetId: Int, appWidgetId: Int, listId: Int) { CoroutineScope(Dispatchers.IO).launch { try { @@ -63,13 +67,18 @@ class PresetWidgetProvider : AppWidgetProvider() { val response = api.postState(State(selectedPresetId = presetId)) if (response.isSuccessful) { - // Add a small delay to allow device state to update internally if needed - // kotlinx.coroutines.delay(200) - // Wait, I need to import delay if I use it. - // Or just trigger update immediately. - val appWidgetManager = AppWidgetManager.getInstance(context) - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) - Log.d(TAG, "Triggered widget update for $appWidgetId") + val prefs = context.getSharedPreferences( + "ca.cgagnier.wlednativeandroid.widget.PresetWidgetCache", + Context.MODE_PRIVATE, + ).edit() + prefs.putInt("cache_selected_id_" + appWidgetId, presetId) + prefs.apply() + + PresetWidgetRemoteViewsFactory.forceRefresh(appWidgetId) + + val appWidgetManager = AppWidgetManager.getInstance(context) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) + Log.d(TAG, "Triggered widget update for $appWidgetId") } } catch (e: Exception) { Log.e(TAG, "Error triggering preset", e) @@ -87,6 +96,7 @@ class PresetWidgetProvider : AppWidgetProvider() { const val EXTRA_LIST_ID = "ca.cgagnier.wlednativeandroid.widget.EXTRA_LIST_ID" private const val TAG = "PresetWidgetProvider" + @Suppress("LongParameterList") fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, @@ -94,24 +104,34 @@ class PresetWidgetProvider : AppWidgetProvider() { limit: Int = 0, layoutId: Int = R.layout.widget_preset, listId: Int = R.id.preset_list, - itemLayoutId: Int = R.layout.widget_preset_item + itemLayoutId: Int = R.layout.widget_preset_item, + titleViewId: Int? = R.id.appwidget_text, ) { val deviceName = PresetWidgetConfigureActivity.loadDeviceName(context, appWidgetId) val deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + // Construct the RemoteViews object + val views = RemoteViews(context.packageName, layoutId) + if (deviceAddress == null) { + titleViewId?.let { + views.setTextViewText(it, context.getString(R.string.widget_please_configure)) + } + views.setEmptyView(listId, R.id.empty_view) + appWidgetManager.updateAppWidget(appWidgetId, views) return } - // Construct the RemoteViews object - val views = RemoteViews(context.packageName, layoutId) - views.setTextViewText(R.id.appwidget_text, deviceName) + titleViewId?.let { + views.setTextViewText(it, deviceName) + } // Set up the collection val intent = Intent(context, PresetWidgetService::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_LIST_LIMIT, limit) putExtra(EXTRA_LAYOUT_ID, itemLayoutId) + putExtra(EXTRA_LIST_ID, listId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } views.setRemoteAdapter(listId, intent) @@ -124,8 +144,10 @@ class PresetWidgetProvider : AppWidgetProvider() { data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } val refreshPendingIntent = PendingIntent.getBroadcast( - context, appWidgetId, refreshIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + context, + appWidgetId, + refreshIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE, ) views.setOnClickPendingIntent(R.id.empty_view, refreshPendingIntent) @@ -137,8 +159,10 @@ class PresetWidgetProvider : AppWidgetProvider() { data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } val toastPendingIntent = PendingIntent.getBroadcast( - context, 0, toastIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + context, + appWidgetId, + toastIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE, ) views.setPendingIntentTemplate(listId, toastPendingIntent) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt index 2455456c..8c355767 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt @@ -8,31 +8,34 @@ import android.widget.RemoteViewsService import ca.cgagnier.wlednativeandroid.R import ca.cgagnier.wlednativeandroid.model.wledapi.Preset import ca.cgagnier.wlednativeandroid.service.api.DeviceApiFactory +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking class PresetWidgetService : RemoteViewsService() { - override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { - return PresetWidgetRemoteViewsFactory(this.applicationContext, intent) - } + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = + PresetWidgetRemoteViewsFactory(this.applicationContext, intent) } -class PresetWidgetRemoteViewsFactory( - private val context: Context, - intent: Intent -) : RemoteViewsService.RemoteViewsFactory { +@Suppress("TooGenericExceptionCaught", "MagicNumber") +class PresetWidgetRemoteViewsFactory(private val context: Context, intent: Intent) : + RemoteViewsService.RemoteViewsFactory { private val appWidgetId: Int = intent.getIntExtra( android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID, - android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID + android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID, ) private val limit: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LIST_LIMIT, 0) - private val itemLayoutId: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LAYOUT_ID, R.layout.widget_preset_item) + private val itemLayoutId: Int = intent.getIntExtra( + PresetWidgetProvider.EXTRA_LAYOUT_ID, + R.layout.widget_preset_item, + ) private var presets: List> = emptyList() private var deviceAddress: String? = null @@ -49,6 +52,7 @@ class PresetWidgetRemoteViewsFactory( private var selectedPresetId: Int = -1 + @Suppress("LongMethod", "CyclomaticComplexMethod") override fun onDataSetChanged() { deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) if (deviceAddress == null) { @@ -58,44 +62,74 @@ class PresetWidgetRemoteViewsFactory( val entryPoint = EntryPointAccessors.fromApplication( context, - PresetWidgetServiceEntryPoint::class.java + PresetWidgetServiceEntryPoint::class.java, ) val deviceApiFactory = entryPoint.deviceApiFactory() val moshi = entryPoint.moshi() + // Always load from cache first to ensure immediate display of existing data + loadPresetsFromCache(moshi) + + // Throttle check + val currentTime = System.currentTimeMillis() + val lastFetchTime = lastFetchTimeMap[appWidgetId] ?: 0L + val isForce = forceRefreshMap[appWidgetId] ?: false + + if (!isForce && currentTime - lastFetchTime < 15000L) { + Log.d(TAG, "Skipping network fetch for $appWidgetId, throttled") + return + } + + // Reset force flag and update last fetch time + forceRefreshMap[appWidgetId] = false + lastFetchTimeMap[appWidgetId] = currentTime + runBlocking { try { Log.d(TAG, "Fetching presets for $deviceAddress") - val api = deviceApiFactory.create(deviceAddress!!) - - // Fetch state to know selected preset - val stateResponse = api.getState() - if (stateResponse.isSuccessful) { + val api = deviceApiFactory.create(deviceAddress!!, 4L) + + val stateDeferred = async(Dispatchers.IO) { api.getState() } + val presetsDeferred = async(Dispatchers.IO) { api.getPresets() } + + val stateResponse = try { + stateDeferred.await() + } catch (e: Exception) { + Log.e(TAG, "Error fetching state concurrently", e) + null + } + + if (stateResponse != null && stateResponse.isSuccessful) { val state = stateResponse.body() if (state != null) { selectedPresetId = state.selectedPresetId ?: -1 - if (selectedPresetId == -1 && state.selectedPlaylistId != null && state.selectedPlaylistId > 0) { - selectedPresetId = state.selectedPlaylistId + if (selectedPresetId == -1 && state.selectedPlaylistId != null && + state.selectedPlaylistId > 0 + ) { + selectedPresetId = state.selectedPlaylistId } } } - // Fetch Presets - val response = api.getPresets() - if (response.isSuccessful) { - val presetsMap = response.body() + val presetsResponse = try { + presetsDeferred.await() + } catch (e: Exception) { + Log.e(TAG, "Error fetching presets concurrently", e) + null + } + + if (presetsResponse != null && presetsResponse.isSuccessful) { + val presetsMap = presetsResponse.body() Log.d(TAG, "Presets fetched: ${presetsMap?.size}") if (presetsMap != null) { - savePresetsToCache(presetsMap, moshi) - processPresets(presetsMap) + savePresetsToCache(presetsMap, moshi) + processPresets(presetsMap) } } else { - Log.e(TAG, "Error fetching presets: ${response.code()}") - loadPresetsFromCache(moshi) + Log.e(TAG, "Error fetching presets: ${presetsResponse?.code() ?: "null response"}") } } catch (e: Exception) { Log.e(TAG, "Error fetching presets", e) - loadPresetsFromCache(moshi) } } } @@ -105,11 +139,11 @@ class PresetWidgetRemoteViewsFactory( .filter { it.key != "0" } .map { it.key to it.value } .sortedBy { it.first.toIntOrNull() ?: Int.MAX_VALUE } - - if (limit > 0) { - sortedPresets = sortedPresets.take(limit) - } - presets = sortedPresets + + if (limit > 0) { + sortedPresets = sortedPresets.take(limit) + } + presets = sortedPresets } private fun savePresetsToCache(presetsMap: Map, moshi: Moshi) { @@ -117,7 +151,7 @@ class PresetWidgetRemoteViewsFactory( val type = Types.newParameterizedType(Map::class.java, String::class.java, Preset::class.java) val adapter = moshi.adapter>(type) val json = adapter.toJson(presetsMap) - + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit() prefs.putString(PREF_CACHE_PRESETS + appWidgetId, json) prefs.putInt(PREF_CACHE_SELECTED_ID + appWidgetId, selectedPresetId) @@ -132,7 +166,7 @@ class PresetWidgetRemoteViewsFactory( val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val json = prefs.getString(PREF_CACHE_PRESETS + appWidgetId, null) selectedPresetId = prefs.getInt(PREF_CACHE_SELECTED_ID + appWidgetId, -1) - + if (json != null) { val type = Types.newParameterizedType(Map::class.java, String::class.java, Preset::class.java) val adapter = moshi.adapter>(type) @@ -146,19 +180,14 @@ class PresetWidgetRemoteViewsFactory( Log.e(TAG, "Failed to load presets from cache", e) } } - - // ... (rest of the file) + // ... (rest of the file) override fun onDestroy() { presets = emptyList() } - override fun getCount(): Int { - return presets.size - } - - + override fun getCount(): Int = presets.size // ... (rest of class) @@ -167,14 +196,14 @@ class PresetWidgetRemoteViewsFactory( val (id, preset) = presets[position] val views = RemoteViews(context.packageName, itemLayoutId) - + var name = preset.name if (name.isEmpty()) { name = "Preset $id" } - + views.setTextViewText(R.id.preset_name, name) - + if (id.toIntOrNull() == selectedPresetId) { if (itemLayoutId == R.layout.widget_preset_button_item) { views.setInt(R.id.widget_item, "setBackgroundResource", R.drawable.widget_button_selected) @@ -198,26 +227,25 @@ class PresetWidgetRemoteViewsFactory( return views } - override fun getLoadingView(): RemoteViews? { - return null - } + override fun getLoadingView(): RemoteViews? = null - override fun getViewTypeCount(): Int { - return 1 - } + override fun getViewTypeCount(): Int = 1 - override fun getItemId(position: Int): Long { - return position.toLong() - } + override fun getItemId(position: Int): Long = position.toLong() - override fun hasStableIds(): Boolean { - return true - } + override fun hasStableIds(): Boolean = true companion object { private const val TAG = "PresetWidgetService" private const val PREFS_NAME = "ca.cgagnier.wlednativeandroid.widget.PresetWidgetCache" private const val PREF_CACHE_PRESETS = "cache_presets_" private const val PREF_CACHE_SELECTED_ID = "cache_selected_id_" + + private val lastFetchTimeMap = java.util.concurrent.ConcurrentHashMap() + private val forceRefreshMap = java.util.concurrent.ConcurrentHashMap() + + fun forceRefresh(appWidgetId: Int) { + forceRefreshMap[appWidgetId] = true + } } } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt index c95979cb..1eab808b 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt @@ -7,20 +7,17 @@ import ca.cgagnier.wlednativeandroid.R class SmallPresetWidgetProvider : AppWidgetProvider() { - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { for (appWidgetId in appWidgetIds) { PresetWidgetProvider.updateAppWidget( - context, - appWidgetManager, - appWidgetId, - limit = 3, - layoutId = R.layout.widget_preset_horizontal, - listId = R.id.preset_grid, - itemLayoutId = R.layout.widget_preset_button_item + context, + appWidgetManager, + appWidgetId, + limit = 3, + layoutId = R.layout.widget_preset_horizontal, + listId = R.id.preset_grid, + itemLayoutId = R.layout.widget_preset_button_item, + titleViewId = null, ) } } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt index aabbdec3..3e0b1b0e 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt @@ -1,5 +1,7 @@ package ca.cgagnier.wlednativeandroid.widget +import android.appwidget.AppWidgetManager +import android.content.ComponentName import android.content.Context import android.util.Log import androidx.glance.GlanceId @@ -53,6 +55,9 @@ class WledWidgetManager @Inject constructor( Log.d(TAG, "Updating widgets from websocket for MAC ${deviceWithState.device.macAddress}") updateWidgetsForMacAddress(context, deviceWithState.device.macAddress, newData) + + // Also update standard preset widgets + updatePresetWidgetsForDevice(context, deviceWithState.device.address) } suspend fun updateAllWidgets(context: Context) { @@ -152,6 +157,38 @@ class WledWidgetManager @Inject constructor( saveStateAndPush(context, glanceId, newData) } } + + // Update name and configurations for any preset widgets matching the device + val appWidgetManager = AppWidgetManager.getInstance(context) + val presetIds = appWidgetManager.getAppWidgetIds( + ComponentName(context, PresetWidgetProvider::class.java), + ) + for (appWidgetId in presetIds) { + val cachedAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (cachedAddress == device.address) { + PresetWidgetConfigureActivity.saveDevicePref(context, appWidgetId, device) + PresetWidgetProvider.updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + val smallPresetIds = appWidgetManager.getAppWidgetIds( + ComponentName(context, SmallPresetWidgetProvider::class.java), + ) + for (appWidgetId in smallPresetIds) { + val cachedAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (cachedAddress == device.address) { + PresetWidgetConfigureActivity.saveDevicePref(context, appWidgetId, device) + PresetWidgetProvider.updateAppWidget( + context, + appWidgetManager, + appWidgetId, + limit = 3, + layoutId = R.layout.widget_preset_horizontal, + listId = R.id.preset_grid, + itemLayoutId = R.layout.widget_preset_button_item, + ) + } + } } /** @@ -178,6 +215,52 @@ class WledWidgetManager @Inject constructor( } } } + + // Also handle preset widgets for the deleted device + val device = deviceRepository.findDeviceByMacAddress(macAddress) + val address = device?.address + if (address != null) { + val appWidgetManager = AppWidgetManager.getInstance(context) + val presetPrefsName = "ca.cgagnier.wlednativeandroid.widget.PresetWidget" + + // Standard Preset Widgets + val presetIds = appWidgetManager.getAppWidgetIds( + ComponentName(context, PresetWidgetProvider::class.java), + ) + for (appWidgetId in presetIds) { + val cachedAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (cachedAddress == address) { + val prefs = context.getSharedPreferences(presetPrefsName, 0).edit() + prefs.remove("appwidget_$appWidgetId") + prefs.remove("appwidget_${appWidgetId}_name") + prefs.apply() + PresetWidgetProvider.updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + // Small Preset Widgets + val smallPresetIds = appWidgetManager.getAppWidgetIds( + ComponentName(context, SmallPresetWidgetProvider::class.java), + ) + for (appWidgetId in smallPresetIds) { + val cachedAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (cachedAddress == address) { + val prefs = context.getSharedPreferences(presetPrefsName, 0).edit() + prefs.remove("appwidget_$appWidgetId") + prefs.remove("appwidget_${appWidgetId}_name") + prefs.apply() + PresetWidgetProvider.updateAppWidget( + context, + appWidgetManager, + appWidgetId, + limit = 3, + layoutId = R.layout.widget_preset_horizontal, + listId = R.id.preset_grid, + itemLayoutId = R.layout.widget_preset_button_item, + ) + } + } + } } private suspend fun getWidgetState(context: Context, glanceId: GlanceId): WidgetStateData? { @@ -200,4 +283,40 @@ class WledWidgetManager @Inject constructor( } WledWidget().update(context, glanceId) } + + private fun updatePresetWidgetsForDevice(context: Context, deviceAddress: String) { + val appWidgetManager = AppWidgetManager.getInstance(context) + + // Standard Preset Widgets + val presetIds = appWidgetManager.getAppWidgetIds( + ComponentName(context, PresetWidgetProvider::class.java), + ) + for (appWidgetId in presetIds) { + val address = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (address == deviceAddress) { + PresetWidgetRemoteViewsFactory.forceRefresh(appWidgetId) + PresetWidgetProvider.updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + // Small Preset Widgets + val smallPresetIds = appWidgetManager.getAppWidgetIds( + ComponentName(context, SmallPresetWidgetProvider::class.java), + ) + for (appWidgetId in smallPresetIds) { + val address = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (address == deviceAddress) { + PresetWidgetRemoteViewsFactory.forceRefresh(appWidgetId) + PresetWidgetProvider.updateAppWidget( + context, + appWidgetManager, + appWidgetId, + limit = 3, + layoutId = R.layout.widget_preset_horizontal, + listId = R.id.preset_grid, + itemLayoutId = R.layout.widget_preset_button_item, + ) + } + } + } } From 0d1504ad7453e0417a84360a79987e0089d52100 Mon Sep 17 00:00:00 2001 From: Md Sohail Date: Sun, 31 May 2026 12:59:49 +0530 Subject: [PATCH 5/5] fixed small widgets not workign issue --- app/src/main/AndroidManifest.xml | 2 ++ .../widget/PresetWidgetConfigureActivity.kt | 1 + .../widget/PresetWidgetProvider.kt | 29 ++++++++++++++++--- .../widget/PresetWidgetService.kt | 28 +++++++++++++----- .../widget/SmallPresetWidgetProvider.kt | 8 +++-- .../widget/WledWidgetManager.kt | 3 ++ 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4cb363a6..2063e3ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,6 +85,7 @@ + + = PresetWidgetProvider::class.java, ) { val deviceName = PresetWidgetConfigureActivity.loadDeviceName(context, appWidgetId) val deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) @@ -113,17 +118,33 @@ class PresetWidgetProvider : AppWidgetProvider() { // Construct the RemoteViews object val views = RemoteViews(context.packageName, layoutId) + val configureIntent = Intent(context, PresetWidgetConfigureActivity::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + val configurePendingIntent = PendingIntent.getActivity( + context, + appWidgetId, + configureIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + if (deviceAddress == null) { titleViewId?.let { views.setTextViewText(it, context.getString(R.string.widget_please_configure)) } views.setEmptyView(listId, R.id.empty_view) + views.setOnClickPendingIntent(R.id.empty_view, configurePendingIntent) + titleViewId?.let { + views.setOnClickPendingIntent(it, configurePendingIntent) + } appWidgetManager.updateAppWidget(appWidgetId, views) return } titleViewId?.let { views.setTextViewText(it, deviceName) + views.setOnClickPendingIntent(it, configurePendingIntent) } // Set up the collection @@ -137,7 +158,7 @@ class PresetWidgetProvider : AppWidgetProvider() { views.setRemoteAdapter(listId, intent) views.setEmptyView(listId, R.id.empty_view) - val refreshIntent = Intent(context, PresetWidgetProvider::class.java).apply { + val refreshIntent = Intent(context, providerClass).apply { action = ACTION_REFRESH putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_LIST_ID, listId) @@ -152,7 +173,7 @@ class PresetWidgetProvider : AppWidgetProvider() { views.setOnClickPendingIntent(R.id.empty_view, refreshPendingIntent) // Set up pending intent template for items - val toastIntent = Intent(context, PresetWidgetProvider::class.java).apply { + val toastIntent = Intent(context, providerClass).apply { action = ACTION_TRIGGER_PRESET putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_LIST_ID, listId) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt index 8c355767..198329c1 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt @@ -54,9 +54,10 @@ class PresetWidgetRemoteViewsFactory(private val context: Context, intent: Inten @Suppress("LongMethod", "CyclomaticComplexMethod") override fun onDataSetChanged() { + Log.d(TAG, "onDataSetChanged called for widget $appWidgetId") deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) if (deviceAddress == null) { - Log.e(TAG, "Device address is null") + Log.e(TAG, "Device address is null for widget $appWidgetId") return } @@ -69,6 +70,7 @@ class PresetWidgetRemoteViewsFactory(private val context: Context, intent: Inten // Always load from cache first to ensure immediate display of existing data loadPresetsFromCache(moshi) + Log.d(TAG, "Loaded ${presets.size} presets from cache for widget $appWidgetId") // Throttle check val currentTime = System.currentTimeMillis() @@ -85,8 +87,15 @@ class PresetWidgetRemoteViewsFactory(private val context: Context, intent: Inten lastFetchTimeMap[appWidgetId] = currentTime runBlocking { + val address = deviceAddress ?: PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (address == null) { + Log.e(TAG, "Device address is null in runBlocking (widget $appWidgetId)") + return@runBlocking + } + deviceAddress = address + try { - Log.d(TAG, "Fetching presets for $deviceAddress") + Log.d(TAG, "Fetching presets for $deviceAddress (widget $appWidgetId)") val api = deviceApiFactory.create(deviceAddress!!, 4L) val stateDeferred = async(Dispatchers.IO) { api.getState() } @@ -95,12 +104,13 @@ class PresetWidgetRemoteViewsFactory(private val context: Context, intent: Inten val stateResponse = try { stateDeferred.await() } catch (e: Exception) { - Log.e(TAG, "Error fetching state concurrently", e) + Log.e(TAG, "Error fetching state concurrently (widget $appWidgetId)", e) null } if (stateResponse != null && stateResponse.isSuccessful) { val state = stateResponse.body() + Log.d(TAG, "State fetched successfully for widget $appWidgetId") if (state != null) { selectedPresetId = state.selectedPresetId ?: -1 if (selectedPresetId == -1 && state.selectedPlaylistId != null && @@ -109,27 +119,29 @@ class PresetWidgetRemoteViewsFactory(private val context: Context, intent: Inten selectedPresetId = state.selectedPlaylistId } } + } else { + Log.e(TAG, "State response failed for widget $appWidgetId: ${stateResponse?.code()}") } val presetsResponse = try { presetsDeferred.await() } catch (e: Exception) { - Log.e(TAG, "Error fetching presets concurrently", e) + Log.e(TAG, "Error fetching presets concurrently (widget $appWidgetId)", e) null } if (presetsResponse != null && presetsResponse.isSuccessful) { val presetsMap = presetsResponse.body() - Log.d(TAG, "Presets fetched: ${presetsMap?.size}") + Log.d(TAG, "Presets fetched: ${presetsMap?.size} for widget $appWidgetId") if (presetsMap != null) { savePresetsToCache(presetsMap, moshi) processPresets(presetsMap) } } else { - Log.e(TAG, "Error fetching presets: ${presetsResponse?.code() ?: "null response"}") + Log.e(TAG, "Error fetching presets for widget $appWidgetId: ${presetsResponse?.code() ?: "null response"}") } } catch (e: Exception) { - Log.e(TAG, "Error fetching presets", e) + Log.e(TAG, "Error fetching presets for widget $appWidgetId", e) } } } @@ -172,7 +184,7 @@ class PresetWidgetRemoteViewsFactory(private val context: Context, intent: Inten val adapter = moshi.adapter>(type) val presetsMap = adapter.fromJson(json) if (presetsMap != null) { - Log.d(TAG, "Loaded presets from cache for $appWidgetId") + Log.d(TAG, "Loaded presets from cache for $appWidgetId. Selected: $selectedPresetId") processPresets(presetsMap) } } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt index 1eab808b..7f8d1f50 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt @@ -1,15 +1,16 @@ package ca.cgagnier.wlednativeandroid.widget import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider import android.content.Context import ca.cgagnier.wlednativeandroid.R +import dagger.hilt.android.AndroidEntryPoint -class SmallPresetWidgetProvider : AppWidgetProvider() { +@AndroidEntryPoint +class SmallPresetWidgetProvider : PresetWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { for (appWidgetId in appWidgetIds) { - PresetWidgetProvider.updateAppWidget( + updateAppWidget( context, appWidgetManager, appWidgetId, @@ -18,6 +19,7 @@ class SmallPresetWidgetProvider : AppWidgetProvider() { listId = R.id.preset_grid, itemLayoutId = R.layout.widget_preset_button_item, titleViewId = null, + providerClass = SmallPresetWidgetProvider::class.java, ) } } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt index 3e0b1b0e..19243fea 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetManager.kt @@ -186,6 +186,7 @@ class WledWidgetManager @Inject constructor( layoutId = R.layout.widget_preset_horizontal, listId = R.id.preset_grid, itemLayoutId = R.layout.widget_preset_button_item, + providerClass = SmallPresetWidgetProvider::class.java, ) } } @@ -257,6 +258,7 @@ class WledWidgetManager @Inject constructor( layoutId = R.layout.widget_preset_horizontal, listId = R.id.preset_grid, itemLayoutId = R.layout.widget_preset_button_item, + providerClass = SmallPresetWidgetProvider::class.java, ) } } @@ -315,6 +317,7 @@ class WledWidgetManager @Inject constructor( layoutId = R.layout.widget_preset_horizontal, listId = R.id.preset_grid, itemLayoutId = R.layout.widget_preset_button_item, + providerClass = SmallPresetWidgetProvider::class.java, ) } }