Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
sohailxi marked this conversation as resolved.
45 changes: 24 additions & 21 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,42 +46,45 @@
<data android:scheme="http" android:host="4.3.2.1" />
</intent-filter>
</activity>
<activity
android:name=".widget.PresetWidgetConfigureActivity"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

<receiver
Comment thread
sohailxi marked this conversation as resolved.
android:name=".widget.WledWidgetReceiver"
android:name=".widget.PresetWidgetProvider"
android:exported="true"
android:label="@string/widget_single_device_name">
android:label="@string/appwidget_text">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="ca.cgagnier.wlednativeandroid.widget.ACTION_TRIGGER_PRESET" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/wled_widget_info" />
android:resource="@xml/widget_preset_info" />
</receiver>

<activity
android:name=".widget.WledWidgetConfigureActivity"
<receiver
android:name=".widget.SmallPresetWidgetProvider"
android:exported="true"
android:taskAffinity=""
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
android:label="WLED Small Presets">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="ca.cgagnier.wlednativeandroid.widget.ACTION_TRIGGER_PRESET" />
</intent-filter>
</activity>

<!-- Device Controls (Quick Access) service for Android 11+ -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_preset_small_info" />
</receiver>

<service
android:name=".service.controls.WledControlsService"
android:label="@string/app_name"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true"
tools:targetApi="30">
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
android:name=".widget.PresetWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@ 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<Info>

@GET("presets.json")
suspend fun getPresets(): Response<Map<String, Preset>>

@GET("json/state")
suspend fun getState(): Response<State>

@POST("json/state")
suspend fun postJson(@Body state: JsonPost): Response<State>
suspend fun postState(@Body state: State): Response<State>

@Multipart
@POST("update")
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
sohailxi marked this conversation as resolved.
}
}
}

@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<List<Device>> = deviceRepository.allDevices
}

val Device.name: String
get() = if (customName.isNotEmpty()) customName else if (originalName.isNotEmpty()) originalName else address
Loading