diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 58f0f652..63eea704 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -101,6 +101,17 @@ android:resource="@xml/battery_widget_info" /> + + + + + + + + profiles.firstOrNull { it.id == pid }?.label + } + + val widthCells = getCellsForSize(widthDp.value.toInt()) + val heightCells = getCellsForSize(heightDp.value.toInt()) + val layout = when { + widthCells <= 1 && heightCells <= 1 -> AncLayout.QUAD_CORNERS + heightCells <= 1 -> AncLayout.ROW_ICONS + widthCells <= 1 -> AncLayout.COLUMN_ICONS + widthCells > 2 -> AncLayout.ROW + heightCells > 3 -> AncLayout.COLUMN + else -> AncLayout.GRID_2X2 + } + + AncWidgetRenderStateMapper.map( + context = context, + device = device, + theme = theme, + isPro = isPro, + hasConfiguredProfile = profileId != null, + profileLabel = profileLabel, + layout = layout, + ) + } catch (e: Exception) { + log(TAG, ERROR) { "provideGlance failed: ${e.asLog()}" } + AncWidgetRenderState.Message( + theme = WidgetTheme.DEFAULT, + resolvedBgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT), + resolvedTextColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT), + resolvedIconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT), + primaryText = context.getString(eu.darken.capod.R.string.widget_error_loading_label), + ) + } + + GlanceAncWidgetContent(state = state, context = context, appWidgetId = appWidgetId) + } + } + + override suspend fun providePreview(context: Context, widgetCategory: Int) { + val previewState = AncWidgetRenderState.previewActive( + context = context, + bgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT), + textColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT), + iconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT), + activeColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorSecondaryContainer), + onActiveColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorOnSecondaryContainer), + ) + + provideContent { + GlanceAncWidgetContent( + state = previewState, + context = context, + appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID, + ) + } + } + + private fun appWidgetId(context: Context, id: GlanceId): Int { + return try { + GlanceAppWidgetManager(context).getAppWidgetId(id) + } catch (_: Exception) { + AppWidgetManager.INVALID_APPWIDGET_ID + } + } + + private fun getCellsForSize(size: Int): Int { + var n = 2 + while (70 * n - 30 < size) { + ++n + } + return n - 1 + } + + companion object { + val TAG = logTag("Widget", "ANC", "Glance") + } +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/AncModeActionCallback.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/AncModeActionCallback.kt new file mode 100644 index 00000000..03924771 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/AncModeActionCallback.kt @@ -0,0 +1,91 @@ +package eu.darken.capod.main.ui.widget + +import android.content.Context +import androidx.annotation.Keep +import androidx.glance.GlanceId +import androidx.glance.action.ActionParameters +import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.updateAll +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR +import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.capod.common.debug.logging.asLog +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.monitor.core.DeviceMonitor +import eu.darken.capod.pods.core.apple.aap.AapConnectionManager +import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +@Keep +class AncModeActionCallback : ActionCallback { + + @Keep + @EntryPoint + @InstallIn(SingletonComponent::class) + interface AncModeCallbackEntryPoint { + fun aapConnectionManager(): AapConnectionManager + fun widgetSettings(): WidgetSettings + fun deviceMonitor(): DeviceMonitor + } + + override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + val modeName = parameters[ANC_MODE_KEY] ?: run { + log(TAG, ERROR) { "onAction: missing ANC_MODE_KEY" } + return + } + val widgetId = parameters[WIDGET_ID_KEY] ?: run { + log(TAG, ERROR) { "onAction: missing WIDGET_ID_KEY" } + return + } + + log(TAG, VERBOSE) { "onAction(mode=$modeName, widgetId=$widgetId)" } + + val mode = try { + enumValueOf(modeName) + } catch (e: IllegalArgumentException) { + log(TAG, ERROR) { "onAction: invalid mode name: $modeName" } + return + } + + val ep = EntryPointAccessors.fromApplication(context, AncModeCallbackEntryPoint::class.java) + + val profileId = ep.widgetSettings().getWidgetProfile(widgetId) + if (profileId == null) { + log(TAG, ERROR) { "onAction: no profile for widgetId=$widgetId" } + return + } + + val device = ep.deviceMonitor().getDeviceForProfile(profileId) + if (device == null) { + log(TAG, ERROR) { "onAction: no device for profileId=$profileId" } + AncGlanceWidget().updateAll(context) + return + } + + val address = device.address + if (address == null) { + log(TAG, ERROR) { "onAction: device has no address" } + AncGlanceWidget().updateAll(context) + return + } + + try { + ep.aapConnectionManager().sendCommand(address, AapCommand.SetAncMode(mode)) + log(TAG, VERBOSE) { "onAction: sent SetAncMode($mode) to $address" } + } catch (e: Exception) { + log(TAG, ERROR) { "onAction: sendCommand failed: ${e.asLog()}" } + } + + AncGlanceWidget().updateAll(context) + } + + companion object { + val ANC_MODE_KEY = ActionParameters.Key("anc_mode") + val WIDGET_ID_KEY = ActionParameters.Key("widget_id") + private val TAG = logTag("Widget", "ANC", "Callback") + } +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetProvider.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetProvider.kt new file mode 100644 index 00000000..05391aeb --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetProvider.kt @@ -0,0 +1,31 @@ +package eu.darken.capod.main.ui.widget + +import android.content.Context +import androidx.annotation.Keep +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import dagger.hilt.android.EntryPointAccessors +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag + +@Keep +class AncWidgetProvider : GlanceAppWidgetReceiver() { + + override val glanceAppWidget: GlanceAppWidget = AncGlanceWidget() + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + log(TAG) { "onDeleted(appWidgetIds=${appWidgetIds.toList()})" } + super.onDeleted(context, appWidgetIds) + val ep = EntryPointAccessors.fromApplication( + context.applicationContext, + AncGlanceWidget.AncWidgetEntryPoint::class.java, + ) + appWidgetIds.forEach { widgetId -> + ep.widgetSettings().removeWidget(widgetId) + } + } + + companion object { + private val TAG = logTag("Widget", "ANC", "Provider") + } +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetRenderState.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetRenderState.kt new file mode 100644 index 00000000..751d686a --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetRenderState.kt @@ -0,0 +1,91 @@ +package eu.darken.capod.main.ui.widget + +import android.content.Context +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import eu.darken.capod.R +import eu.darken.capod.main.ui.components.shortLabel +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +sealed class AncWidgetRenderState { + abstract val theme: WidgetTheme + @get:ColorInt abstract val resolvedBgColor: Int + @get:ColorInt abstract val resolvedTextColor: Int + @get:ColorInt abstract val resolvedIconColor: Int + + data class Active( + override val theme: WidgetTheme, + @ColorInt override val resolvedBgColor: Int, + @ColorInt override val resolvedTextColor: Int, + @ColorInt override val resolvedIconColor: Int, + @ColorInt val resolvedActiveColor: Int, + @ColorInt val resolvedOnActiveColor: Int, + val modes: List, + val deviceLabel: String?, + val layout: AncLayout, + ) : AncWidgetRenderState() + + data class Message( + override val theme: WidgetTheme, + @ColorInt override val resolvedBgColor: Int, + @ColorInt override val resolvedTextColor: Int, + @ColorInt override val resolvedIconColor: Int, + val primaryText: String, + val secondaryText: String? = null, + ) : AncWidgetRenderState() + + companion object { + fun previewActive( + context: Context, + theme: WidgetTheme = WidgetTheme.DEFAULT, + @ColorInt bgColor: Int = 0xFFFFFFFF.toInt(), + @ColorInt textColor: Int = 0xFF1E1E1E.toInt(), + @ColorInt iconColor: Int = 0xFF1E1E1E.toInt(), + @ColorInt activeColor: Int = 0xFFCCE5FF.toInt(), + @ColorInt onActiveColor: Int = 0xFF001E30.toInt(), + layout: AncLayout = AncLayout.GRID_2X2, + modes: List = AapSetting.AncMode.Value.entries, + activeMode: AapSetting.AncMode.Value = AapSetting.AncMode.Value.ON, + deviceLabel: String? = null, + ): Active = Active( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + resolvedActiveColor = activeColor, + resolvedOnActiveColor = onActiveColor, + modes = modes.map { mode -> + ModeItem( + mode = mode, + label = mode.shortLabel(context), + iconRes = mode.previewIconRes(), + state = if (mode == activeMode) ButtonState.ACTIVE else ButtonState.INACTIVE, + ) + }, + deviceLabel = deviceLabel, + layout = layout, + ) + } +} + +data class ModeItem( + val mode: AapSetting.AncMode.Value, + val label: String, + @DrawableRes val iconRes: Int, + val state: ButtonState, +) + +enum class ButtonState { + ACTIVE, PENDING, INACTIVE, +} + +enum class AncLayout { + QUAD_CORNERS, ROW_ICONS, COLUMN_ICONS, GRID_2X2, ROW, COLUMN, +} + +private fun AapSetting.AncMode.Value.previewIconRes(): Int = when (this) { + AapSetting.AncMode.Value.OFF -> R.drawable.ic_anc_off + AapSetting.AncMode.Value.ON -> R.drawable.ic_anc_on + AapSetting.AncMode.Value.TRANSPARENCY -> R.drawable.ic_anc_transparency + AapSetting.AncMode.Value.ADAPTIVE -> R.drawable.ic_anc_adaptive +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetRenderStateMapper.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetRenderStateMapper.kt new file mode 100644 index 00000000..4359a578 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/AncWidgetRenderStateMapper.kt @@ -0,0 +1,130 @@ +package eu.darken.capod.main.ui.widget + +import android.content.Context +import eu.darken.capod.R +import eu.darken.capod.main.ui.components.shortLabel +import eu.darken.capod.monitor.core.PodDevice +import eu.darken.capod.monitor.core.visibleAncModes +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +object AncWidgetRenderStateMapper { + + fun map( + context: Context, + device: PodDevice?, + theme: WidgetTheme, + isPro: Boolean, + hasConfiguredProfile: Boolean, + profileLabel: String?, + layout: AncLayout, + ): AncWidgetRenderState { + val bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme) + val textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme) + val iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme) + val activeColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorSecondaryContainer) + val onActiveColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorOnSecondaryContainer) + + return when { + !isPro -> AncWidgetRenderState.Message( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + primaryText = context.getString(R.string.upgrade_capod_label), + secondaryText = context.getString(R.string.upgrade_capod_description), + ) + + device == null -> { + val messageRes = if (hasConfiguredProfile) { + R.string.widget_no_data_label + } else { + R.string.overview_nomaindevice_label + } + AncWidgetRenderState.Message( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + primaryText = context.getString(messageRes), + ) + } + + !device.hasAncControl -> AncWidgetRenderState.Message( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + primaryText = context.getString(R.string.anc_widget_no_anc_support_label), + ) + + !device.isAapConnected -> AncWidgetRenderState.Message( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + primaryText = context.getString(R.string.anc_widget_aap_not_connected_label), + ) + + !device.isAapReady -> AncWidgetRenderState.Message( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + primaryText = context.getString(R.string.anc_widget_aap_connecting_label), + ) + + else -> { + val ancMode = device.ancMode ?: return AncWidgetRenderState.Message( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + primaryText = context.getString(R.string.anc_widget_aap_connecting_label), + ) + + val currentMode = ancMode.current + val pendingMode = device.pendingAncMode + + val filteredModes = device.visibleAncModes + + // If the pending mode has been filtered out of visibleAncModes (e.g. device + // reports AllowOff=false while OFF is still pending), fall back to currentMode + // for the ACTIVE highlight so the widget never renders with zero active buttons. + val pendingVisible = pendingMode != null && filteredModes.contains(pendingMode) + val displayMode = if (pendingVisible) pendingMode else currentMode + + val modeItems = filteredModes.map { mode -> + ModeItem( + mode = mode, + label = mode.shortLabel(context), + iconRes = mode.iconDrawableRes(), + state = when { + pendingVisible && mode == pendingMode -> ButtonState.PENDING + mode == displayMode -> ButtonState.ACTIVE + else -> ButtonState.INACTIVE + }, + ) + } + + AncWidgetRenderState.Active( + theme = theme, + resolvedBgColor = bgColor, + resolvedTextColor = textColor, + resolvedIconColor = iconColor, + resolvedActiveColor = activeColor, + resolvedOnActiveColor = onActiveColor, + modes = modeItems, + deviceLabel = profileLabel ?: device.getLabel(context), + layout = layout, + ) + } + } + } +} + +private fun AapSetting.AncMode.Value.iconDrawableRes(): Int = when (this) { + AapSetting.AncMode.Value.OFF -> R.drawable.ic_anc_off + AapSetting.AncMode.Value.ON -> R.drawable.ic_anc_on + AapSetting.AncMode.Value.TRANSPARENCY -> R.drawable.ic_anc_transparency + AapSetting.AncMode.Value.ADAPTIVE -> R.drawable.ic_anc_adaptive +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/ComposeAncWidgetPreview.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/ComposeAncWidgetPreview.kt new file mode 100644 index 00000000..acb5735c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/ComposeAncWidgetPreview.kt @@ -0,0 +1,142 @@ +package eu.darken.capod.main.ui.widget + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun ComposeAncWidgetPreview( + state: AncWidgetRenderState, + modifier: Modifier = Modifier, +) { + when (state) { + is AncWidgetRenderState.Active -> ActiveAncPreview(state, modifier) + is AncWidgetRenderState.Message -> MessageAncPreview(state, modifier) + } +} + +@Composable +private fun ActiveAncPreview( + state: AncWidgetRenderState.Active, + modifier: Modifier, +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(Color(state.resolvedBgColor)) + .padding(horizontal = 12.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + AncGridRow(state.modes.take(2), state.resolvedIconColor, state.resolvedBgColor) + if (state.modes.size > 2) { + AncGridRow(state.modes.drop(2), state.resolvedIconColor, state.resolvedBgColor) + } + if (state.theme.showDeviceLabel && state.deviceLabel != null) { + Text( + text = state.deviceLabel, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = Color(state.resolvedTextColor), + textAlign = TextAlign.Center, + maxLines = 1, + ) + } + } +} + +@Composable +private fun AncGridRow( + items: List, + resolvedIconColor: Int, + resolvedBgColor: Int, +) { + Row( + horizontalArrangement = Arrangement.Center, + ) { + items.forEach { item -> + AncPreviewButton(item, resolvedIconColor, resolvedBgColor) + } + } +} + +@Composable +private fun AncPreviewButton( + item: ModeItem, + resolvedIconColor: Int, + resolvedBgColor: Int, +) { + val bgColor = when (item.state) { + ButtonState.ACTIVE -> Color(resolvedIconColor) + ButtonState.PENDING -> Color(resolvedIconColor).copy(alpha = 0.6f) + ButtonState.INACTIVE -> Color(resolvedIconColor).copy(alpha = 0.12f) + } + val tint = when (item.state) { + ButtonState.ACTIVE, ButtonState.PENDING -> Color(resolvedBgColor) + ButtonState.INACTIVE -> Color(resolvedIconColor) + } + Box( + modifier = Modifier + .padding(4.dp) + .size(48.dp) + .clip(RoundedCornerShape(12.dp)) + .background(bgColor), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(item.iconRes), + contentDescription = item.label, + colorFilter = ColorFilter.tint(tint), + modifier = Modifier.size(24.dp), + ) + } +} + +@Composable +private fun MessageAncPreview( + state: AncWidgetRenderState.Message, + modifier: Modifier, +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(Color(state.resolvedBgColor)) + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = state.primaryText, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(state.resolvedTextColor), + textAlign = TextAlign.Center, + ) + if (state.secondaryText != null) { + Text( + text = state.secondaryText, + fontSize = 12.sp, + color = Color(state.resolvedTextColor), + textAlign = TextAlign.Center, + ) + } + } +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/GlanceAncWidgetContent.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/GlanceAncWidgetContent.kt new file mode 100644 index 00000000..60642535 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/GlanceAncWidgetContent.kt @@ -0,0 +1,569 @@ +package eu.darken.capod.main.ui.widget + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.ColorFilter +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.action.ActionParameters +import androidx.glance.action.clickable +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.action.actionStartActivity +import androidx.glance.appwidget.cornerRadius +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.ColumnScope +import androidx.glance.layout.Row +import androidx.glance.layout.RowScope +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxHeight +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.layout.width +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextAlign +import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider +import eu.darken.capod.main.ui.MainActivity + +@Composable +fun GlanceAncWidgetContent( + state: AncWidgetRenderState, + context: android.content.Context, + appWidgetId: Int, +) { + val openApp = actionStartActivity( + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + ) + + when (state) { + is AncWidgetRenderState.Active -> GlanceAncActive(state, appWidgetId) + is AncWidgetRenderState.Message -> GlanceAncMessage(state, GlanceModifier.clickable(openApp)) + } +} + +@Composable +private fun GlanceAncActive( + state: AncWidgetRenderState.Active, + appWidgetId: Int, +) { + val textColor = fixedAncColor(state.resolvedTextColor) + val iconColor = fixedAncColor(state.resolvedIconColor) + val showDeviceLabel = state.layout != AncLayout.ROW_ICONS + && state.layout != AncLayout.COLUMN_ICONS + && state.layout != AncLayout.QUAD_CORNERS + + AncWidgetRoot(state.resolvedBgColor) { + when (state.layout) { + AncLayout.GRID_2X2 -> AncGrid(state.modes, iconColor, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId) + AncLayout.ROW -> AncRow(state.modes, iconColor, textColor, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId) + AncLayout.COLUMN -> AncColumn(state.modes, iconColor, textColor, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId) + AncLayout.ROW_ICONS -> AncRowIcons(state.modes, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId) + AncLayout.COLUMN_ICONS -> AncColumnIcons(state.modes, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId) + AncLayout.QUAD_CORNERS -> AncQuadCorners(state.modes, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId) + } + if (showDeviceLabel) { + GlanceAncDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor) + } + } +} + +@Composable +private fun AncRowIcons( + modes: List, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + val dividerColor = fixedAncColor(applyAncAlpha(resolvedIconColor, 60)) + Row( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + ) { + modes.forEachIndexed { index, item -> + if (index > 0) { + Spacer(modifier = GlanceModifier.width(1.dp).fillMaxHeight().background(dividerColor)) + } + DividerRowCell(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + } +} + +@Composable +private fun AncColumnIcons( + modes: List, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + val dividerColor = fixedAncColor(applyAncAlpha(resolvedIconColor, 60)) + Column( + modifier = GlanceModifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + modes.forEachIndexed { index, item -> + if (index > 0) { + Spacer(modifier = GlanceModifier.fillMaxWidth().height(1.dp).background(dividerColor)) + } + DividerColumnCell(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + } +} + +@Composable +private fun RowScope.DividerRowCell( + item: ModeItem, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + Box( + modifier = GlanceModifier + .defaultWeight() + .fillMaxHeight() + .clickable(ancModeAction(item, appWidgetId)), + contentAlignment = Alignment.Center, + ) { + DividerCellIcon(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor) + } +} + +@Composable +private fun ColumnScope.DividerColumnCell( + item: ModeItem, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + Box( + modifier = GlanceModifier + .defaultWeight() + .fillMaxWidth() + .clickable(ancModeAction(item, appWidgetId)), + contentAlignment = Alignment.Center, + ) { + DividerCellIcon(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor) + } +} + +@Composable +private fun DividerCellIcon( + item: ModeItem, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, +) { + val bgModifier = when (item.state) { + ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor)) + ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153))) + ButtonState.INACTIVE -> GlanceModifier + } + val tint = when (item.state) { + ButtonState.ACTIVE, ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor)) + ButtonState.INACTIVE -> ColorFilter.tint(fixedAncColor(applyAncAlpha(resolvedIconColor, 160))) + } + Box( + modifier = bgModifier + .size(36.dp) + .cornerRadius(18.dp), + contentAlignment = Alignment.Center, + ) { + Image( + provider = ImageProvider(item.iconRes), + contentDescription = item.label, + modifier = GlanceModifier.size(22.dp), + colorFilter = tint, + ) + } +} + +@Composable +private fun AncQuadCorners( + modes: List, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + val dividerColor = fixedAncColor(applyAncAlpha(resolvedIconColor, 60)) + + Column(modifier = GlanceModifier.fillMaxSize()) { + Row(modifier = GlanceModifier.fillMaxWidth().defaultWeight()) { + QuadCornerCell(modes.getOrNull(0), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + Spacer(modifier = GlanceModifier.width(1.dp).fillMaxHeight().background(dividerColor)) + QuadCornerCell(modes.getOrNull(1), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + Spacer(modifier = GlanceModifier.fillMaxWidth().height(1.dp).background(dividerColor)) + Row(modifier = GlanceModifier.fillMaxWidth().defaultWeight()) { + QuadCornerCell(modes.getOrNull(2), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + Spacer(modifier = GlanceModifier.width(1.dp).fillMaxHeight().background(dividerColor)) + QuadCornerCell(modes.getOrNull(3), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + } +} + +@Composable +private fun RowScope.QuadCornerCell( + item: ModeItem?, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + val cellModifier = GlanceModifier.defaultWeight().fillMaxHeight() + val clickable = if (item != null) cellModifier.clickable(ancModeAction(item, appWidgetId)) else cellModifier + Box( + modifier = clickable, + contentAlignment = Alignment.Center, + ) { + if (item != null) { + DividerCellIcon(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor) + } + } +} + +@Composable +private fun AncGrid( + modes: List, + iconColor: ColorProvider, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + val firstRow = modes.take(2) + val secondRow = modes.drop(2) + + Column( + modifier = GlanceModifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = GlanceModifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + firstRow.forEachIndexed { index, item -> + if (index > 0) Spacer(modifier = GlanceModifier.width(10.dp)) + AncGridButton(item, iconColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + } + if (secondRow.isNotEmpty()) { + Spacer(modifier = GlanceModifier.height(10.dp)) + Row( + modifier = GlanceModifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + secondRow.forEachIndexed { index, item -> + if (index > 0) Spacer(modifier = GlanceModifier.width(10.dp)) + AncGridButton(item, iconColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + } + } + } +} + +@Composable +private fun AncGridButton( + item: ModeItem, + iconColor: ColorProvider, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + val bgModifier = when (item.state) { + ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor)) + ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153))) + ButtonState.INACTIVE -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedIconColor, 30))) + } + val tint = when (item.state) { + ButtonState.ACTIVE -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor)) + ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor)) + ButtonState.INACTIVE -> ColorFilter.tint(iconColor) + } + + Box( + modifier = bgModifier + .padding(4.dp) + .size(48.dp) + .cornerRadius(12.dp) + .clickable(ancModeAction(item, appWidgetId)), + contentAlignment = Alignment.Center, + ) { + Image( + provider = ImageProvider(item.iconRes), + contentDescription = item.label, + modifier = GlanceModifier.size(24.dp), + colorFilter = tint, + ) + } +} + +@Composable +private fun AncRow( + modes: List, + iconColor: ColorProvider, + textColor: ColorProvider, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + Row( + modifier = GlanceModifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) { + modes.forEachIndexed { index, item -> + if (index > 0) Spacer(modifier = GlanceModifier.width(6.dp)) + AncLabeledButton(item, iconColor, textColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + } +} + +@Composable +private fun AncColumn( + modes: List, + iconColor: ColorProvider, + textColor: ColorProvider, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + Column( + modifier = GlanceModifier.fillMaxSize().padding(vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + modes.forEachIndexed { index, item -> + if (index > 0) Spacer(modifier = GlanceModifier.height(6.dp)) + AncColumnItem(item, iconColor, textColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId) + } + } +} + +@Composable +private fun ColumnScope.AncColumnItem( + item: ModeItem, + iconColor: ColorProvider, + textColor: ColorProvider, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, +) { + val bgModifier = when (item.state) { + ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor)) + ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153))) + ButtonState.INACTIVE -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedIconColor, 30))) + } + val tint = when (item.state) { + ButtonState.ACTIVE, ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor)) + ButtonState.INACTIVE -> ColorFilter.tint(iconColor) + } + val labelColor = when (item.state) { + ButtonState.ACTIVE, ButtonState.PENDING -> fixedAncColor(resolvedOnActiveColor) + ButtonState.INACTIVE -> textColor + } + Row( + modifier = bgModifier + .fillMaxWidth() + .defaultWeight() + .cornerRadius(12.dp) + .clickable(ancModeAction(item, appWidgetId)) + .padding(horizontal = 12.dp, vertical = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + provider = ImageProvider(item.iconRes), + contentDescription = null, + modifier = GlanceModifier.size(18.dp), + colorFilter = tint, + ) + Spacer(modifier = GlanceModifier.width(6.dp)) + Text( + text = item.label, + style = TextStyle(color = labelColor, fontSize = 12.sp, fontWeight = FontWeight.Medium), + ) + } +} + +@Composable +private fun AncLabeledButton( + item: ModeItem, + iconColor: ColorProvider, + textColor: ColorProvider, + resolvedIconColor: Int, + resolvedActiveColor: Int, + resolvedOnActiveColor: Int, + appWidgetId: Int, + horizontal: Boolean = false, +) { + val bgModifier = when (item.state) { + ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor)) + ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153))) + ButtonState.INACTIVE -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedIconColor, 30))) + } + val tint = when (item.state) { + ButtonState.ACTIVE -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor)) + ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor)) + ButtonState.INACTIVE -> ColorFilter.tint(iconColor) + } + val labelColor = when (item.state) { + ButtonState.ACTIVE -> fixedAncColor(resolvedOnActiveColor) + ButtonState.PENDING -> fixedAncColor(resolvedOnActiveColor) + ButtonState.INACTIVE -> textColor + } + + if (horizontal) { + Row( + modifier = bgModifier + .fillMaxWidth() + .cornerRadius(12.dp) + .clickable(ancModeAction(item, appWidgetId)) + .padding(horizontal = 12.dp, vertical = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + provider = ImageProvider(item.iconRes), + contentDescription = null, + modifier = GlanceModifier.size(18.dp), + colorFilter = tint, + ) + Spacer(modifier = GlanceModifier.width(6.dp)) + Text( + text = item.label, + style = TextStyle(color = labelColor, fontSize = 12.sp, fontWeight = FontWeight.Medium), + ) + } + } else { + Column( + modifier = bgModifier + .padding(horizontal = 14.dp, vertical = 10.dp) + .cornerRadius(14.dp) + .clickable(ancModeAction(item, appWidgetId)), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + provider = ImageProvider(item.iconRes), + contentDescription = null, + modifier = GlanceModifier.size(22.dp), + colorFilter = tint, + ) + Spacer(modifier = GlanceModifier.height(2.dp)) + Text( + text = item.label, + style = TextStyle(color = labelColor, fontSize = 12.sp, fontWeight = FontWeight.Medium), + maxLines = 1, + ) + } + } +} + +@Composable +private fun GlanceAncMessage( + state: AncWidgetRenderState.Message, + clickModifier: GlanceModifier, +) { + val textStyle = TextStyle( + color = fixedAncColor(state.resolvedTextColor), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + + AncWidgetRoot(state.resolvedBgColor, clickModifier) { + Text( + text = state.primaryText, + style = textStyle, + modifier = GlanceModifier.fillMaxWidth(), + ) + if (state.secondaryText != null) { + Text( + text = state.secondaryText, + style = TextStyle( + color = fixedAncColor(state.resolvedTextColor), + fontSize = 12.sp, + textAlign = TextAlign.Center, + ), + modifier = GlanceModifier.fillMaxWidth(), + maxLines = 4, + ) + } + } +} + +@Composable +private fun AncWidgetRoot( + bgColor: Int, + extraModifier: GlanceModifier = GlanceModifier, + content: @Composable () -> Unit, +) { + Column( + modifier = extraModifier + .fillMaxSize() + .background(fixedAncColor(bgColor)) + .padding(horizontal = 8.dp, vertical = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) { + content() + } +} + +@Composable +private fun GlanceAncDeviceLabel( + label: String?, + visible: Boolean, + textColor: Int, +) { + if (visible && label != null) { + Text( + text = label, + style = TextStyle( + color = fixedAncColor(textColor), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ), + maxLines = 1, + modifier = GlanceModifier.padding(top = 8.dp), + ) + } +} + +private fun fixedAncColor(argb: Int): ColorProvider = ColorProvider(Color(argb)) + +private fun applyAncAlpha(color: Int, alpha: Int): Int { + return android.graphics.Color.argb( + alpha, + android.graphics.Color.red(color), + android.graphics.Color.green(color), + android.graphics.Color.blue(color), + ) +} + +private fun ancModeAction(item: ModeItem, appWidgetId: Int) = actionRunCallback( + parameters = androidx.glance.action.actionParametersOf( + AncModeActionCallback.ANC_MODE_KEY to item.mode.name, + AncModeActionCallback.WIDGET_ID_KEY to appWidgetId, + ) +) diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt index 4de33873..8fdb54ef 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt @@ -74,6 +74,7 @@ class WidgetConfigurationActivity : Activity2() { state?.let { currentState -> WidgetConfigurationScreen( state = currentState, + showAapRequiredHint = currentState.isAncWidget, onSelectProfile = { profile -> vm.selectProfile(profile.id) }, onSelectPreset = { preset -> vm.selectPreset(preset) }, onEnterCustomMode = { bg, fg -> vm.enterCustomMode(bg, fg) }, @@ -84,7 +85,7 @@ class WidgetConfigurationActivity : Activity2() { onReset = { vm.resetToDefaults() }, onConfirm = { if (currentState.isPro) { - confirmSelection() + confirmSelection(currentState.isAncWidget) } else { startActivity( Intent(this@WidgetConfigurationActivity, MainActivity::class.java).apply { @@ -100,7 +101,7 @@ class WidgetConfigurationActivity : Activity2() { } } - private fun confirmSelection() { + private fun confirmSelection(isAncWidget: Boolean) { vm.confirmSelection() val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) @@ -109,7 +110,11 @@ class WidgetConfigurationActivity : Activity2() { lifecycleScope.launch { val manager = GlanceAppWidgetManager(appContext) val glanceId = manager.getGlanceIdBy(widgetId) - BatteryGlanceWidget().update(appContext, glanceId) + if (isAncWidget) { + AncGlanceWidget().update(appContext, glanceId) + } else { + BatteryGlanceWidget().update(appContext, glanceId) + } finish() } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationScreen.kt index 6c0ddc22..c49e6748 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationScreen.kt @@ -84,6 +84,7 @@ import eu.darken.capod.profiles.core.DeviceProfile @Composable fun WidgetConfigurationScreen( state: WidgetConfigurationViewModel.State, + showAapRequiredHint: Boolean = false, onSelectProfile: (DeviceProfile) -> Unit, onSelectPreset: (WidgetTheme.Preset) -> Unit, onEnterCustomMode: (defaultBg: Int, defaultFg: Int) -> Unit, @@ -118,6 +119,26 @@ fun WidgetConfigurationScreen( .padding(start = screenHPad, end = screenHPad, bottom = 16.dp), ) + if (showAapRequiredHint) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = screenHPad), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + ), + shape = RoundedCornerShape(12.dp), + ) { + Text( + text = stringResource(R.string.anc_widget_config_aap_required_hint), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.padding(16.dp), + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } + // Card A — Select Device Card( modifier = Modifier @@ -186,6 +207,17 @@ fun WidgetConfigurationScreen( WidgetConfigPreview( theme = state.theme, deviceLabel = state.profiles.firstOrNull { it.id == state.selectedProfile }?.label, + isAncWidget = state.isAncWidget, + ) + + Text( + text = stringResource(R.string.widget_config_preview_resize_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), ) Spacer(modifier = Modifier.height(16.dp)) @@ -435,20 +467,12 @@ private fun ProfileSelectionItem( private fun WidgetConfigPreview( theme: WidgetTheme, deviceLabel: String?, + isAncWidget: Boolean, modifier: Modifier = Modifier, ) { val context = LocalContext.current val hasTransparency = theme.backgroundColor != null && theme.backgroundAlpha < 255 - val previewState = remember(theme, deviceLabel) { - WidgetRenderState.previewDualPod( - theme = theme, - bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme), - textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme), - iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme), - ).copy(deviceLabel = deviceLabel) - } - Surface( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), @@ -461,15 +485,39 @@ private fun WidgetConfigPreview( .padding(24.dp), contentAlignment = Alignment.Center, ) { + val previewContent: @Composable (Modifier) -> Unit = { innerModifier -> + if (isAncWidget) { + val ancState = remember(theme, deviceLabel, context) { + AncWidgetRenderState.previewActive( + context = context, + theme = theme, + bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme), + textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme), + iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme), + activeColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorSecondaryContainer), + onActiveColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorOnSecondaryContainer), + deviceLabel = deviceLabel, + ) + } + ComposeAncWidgetPreview(state = ancState, modifier = innerModifier) + } else { + val batteryState = remember(theme, deviceLabel) { + WidgetRenderState.previewDualPod( + theme = theme, + bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme), + textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme), + iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme), + ).copy(deviceLabel = deviceLabel) + } + ComposeWidgetPreview(state = batteryState, modifier = innerModifier) + } + } if (hasTransparency) { CheckerboardBackground { - ComposeWidgetPreview( - state = previewState, - modifier = Modifier.padding(6.dp), - ) + previewContent(Modifier.padding(6.dp)) } } else { - ComposeWidgetPreview(state = previewState) + previewContent(Modifier) } } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt index 165a1191..bb1084da 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt @@ -1,6 +1,7 @@ package eu.darken.capod.main.ui.widget import android.appwidget.AppWidgetManager +import android.content.ComponentName import android.content.Context import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel @@ -16,6 +17,7 @@ import eu.darken.capod.profiles.core.DeviceProfile import eu.darken.capod.profiles.core.DeviceProfilesRepo import eu.darken.capod.profiles.core.ProfileId import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map import javax.inject.Inject @HiltViewModel @@ -33,8 +35,15 @@ class WidgetConfigurationViewModel @Inject constructor( val widgetId: Int get() = savedStateHandle.get(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID + private val isAncWidget: Boolean = run { + if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@run false + val info = appWidgetManager.getAppWidgetInfo(widgetId) ?: return@run false + val ancProvider = ComponentName(context, AncWidgetProvider::class.java) + info.provider == ancProvider + } + init { - log(TAG) { "ViewModel init(widgetId=$widgetId)" } + log(TAG) { "ViewModel init(widgetId=$widgetId, isAncWidget=$isAncWidget)" } if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { log(TAG) { "Invalid widget ID" } @@ -52,14 +61,18 @@ class WidgetConfigurationViewModel @Inject constructor( private val currentTheme = MutableStateFlow(initialTheme) + private val visibleProfiles = deviceProfilesRepo.profiles.map { profiles -> + if (isAncWidget) profiles.filter { it.model.features.hasAncControl } else profiles + } + val state = combine( selectedProfile, currentTheme, forceCustomMode, - deviceProfilesRepo.profiles, + visibleProfiles, upgradeRepo.upgradeInfo, ) { selected, theme, forceCustom, profiles, upgradeInfo -> - log(TAG) { "state update: profile=$selected, theme=$theme, forceCustom=$forceCustom" } + log(TAG) { "state update: profile=$selected, theme=$theme, forceCustom=$forceCustom, profiles=${profiles.size}" } val activePreset = if (forceCustom) null else WidgetTheme.matchPreset(theme) State( @@ -69,6 +82,7 @@ class WidgetConfigurationViewModel @Inject constructor( theme = theme, activePreset = activePreset, isCustomMode = activePreset == null, + isAncWidget = isAncWidget, ) }.asLiveState() @@ -79,8 +93,9 @@ class WidgetConfigurationViewModel @Inject constructor( val theme: WidgetTheme = WidgetTheme.DEFAULT, val activePreset: WidgetTheme.Preset? = WidgetTheme.Preset.MATERIAL_YOU, val isCustomMode: Boolean = false, + val isAncWidget: Boolean = false, ) { - val canConfirm: Boolean = selectedProfile != null + val canConfirm: Boolean = selectedProfile != null && profiles.any { it.id == selectedProfile } } fun selectProfile(profileId: ProfileId) { @@ -125,12 +140,6 @@ class WidgetConfigurationViewModel @Inject constructor( currentTheme.value = currentTheme.value.copy(backgroundAlpha = alpha.coerceIn(0, 255)) } - fun toggleDeviceLabel() { - val newValue = !currentTheme.value.showDeviceLabel - log(TAG, INFO) { "toggleDeviceLabel(showDeviceLabel=$newValue)" } - currentTheme.value = currentTheme.value.copy(showDeviceLabel = newValue) - } - fun setShowDeviceLabel(show: Boolean) { log(TAG, INFO) { "setShowDeviceLabel(show=$show)" } currentTheme.value = currentTheme.value.copy(showDeviceLabel = show) diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetManager.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetManager.kt index ad10a6b0..1cf51e35 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetManager.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetManager.kt @@ -18,6 +18,7 @@ class WidgetManager @Inject constructor( suspend fun refreshWidgets() { log(TAG, VERBOSE) { "refreshWidgets()" } BatteryGlanceWidget().updateAll(context) + AncGlanceWidget().updateAll(context) } companion object { diff --git a/app/src/main/res/drawable/ic_anc_adaptive.xml b/app/src/main/res/drawable/ic_anc_adaptive.xml new file mode 100644 index 00000000..3cd32bce --- /dev/null +++ b/app/src/main/res/drawable/ic_anc_adaptive.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_anc_off.xml b/app/src/main/res/drawable/ic_anc_off.xml new file mode 100644 index 00000000..3f808341 --- /dev/null +++ b/app/src/main/res/drawable/ic_anc_off.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_anc_on.xml b/app/src/main/res/drawable/ic_anc_on.xml new file mode 100644 index 00000000..86c4cd56 --- /dev/null +++ b/app/src/main/res/drawable/ic_anc_on.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_anc_transparency.xml b/app/src/main/res/drawable/ic_anc_transparency.xml new file mode 100644 index 00000000..f7011b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_anc_transparency.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/anc_widget_preview_layout.xml b/app/src/main/res/layout/anc_widget_preview_layout.xml new file mode 100644 index 00000000..34ca5483 --- /dev/null +++ b/app/src/main/res/layout/anc_widget_preview_layout.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e8e4fdd..d624b230 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -173,6 +173,7 @@ Background transparency Show device name Reset to defaults + You can resize the widget after placing it on your hoem screen. Custom Material You Dark diff --git a/app/src/main/res/xml/anc_widget_info.xml b/app/src/main/res/xml/anc_widget_info.xml new file mode 100644 index 00000000..68f0373a --- /dev/null +++ b/app/src/main/res/xml/anc_widget_info.xml @@ -0,0 +1,16 @@ + +