diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java index b2afd4bec..9a352004f 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java @@ -52,6 +52,11 @@ public class MainActivity extends AppCompatActivity */ public static final String EXTRA_OPEN_SERVERS = "org.strongswan.android.extra.OPEN_SERVERS"; + /** + * Set as an intent extra to connect the fastest server (the «Fastest» button of the widget) + */ + public static final String EXTRA_CONNECT_FASTEST = "org.strongswan.android.extra.CONNECT_FASTEST"; + /** * Use "bring your own device" (BYOD) features */ @@ -121,5 +126,10 @@ public class MainActivity extends AppCompatActivity intent.removeExtra(EXTRA_OPEN_SERVERS); ShxNavigation.INSTANCE.request(ShxTab.PROFILES); } + if (intent != null && intent.getBooleanExtra(EXTRA_CONNECT_FASTEST, false)) + { + intent.removeExtra(EXTRA_CONNECT_FASTEST); + ShxNavigation.INSTANCE.requestFastest(); + } } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt index 1f1199af4..a4728e75e 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt @@ -121,6 +121,16 @@ fun ShxApp(viewModel: HomeViewModel = viewModel()) ShxNavigation.consumeAddServer() } } + val fastestRequested by ShxNavigation.fastest.collectAsStateWithLifecycle() + LaunchedEffect(fastestRequested, state.profiles.isNotEmpty()) + { + if (fastestRequested && state.profiles.isNotEmpty()) + { + viewModel.setTab(ShxTab.HOME) + viewModel.connectFastest(context) + ShxNavigation.consumeFastest() + } + } val editProfile: (VpnProfile) -> Unit = { profile -> val intent = Intent(context, VpnProfileDetailActivity::class.java) intent.putExtra(VpnProfileDataSource.KEY_UUID, profile.getUUID().toString()) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt index f571b7455..e8ac5306d 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt @@ -49,4 +49,18 @@ object ShxNavigation { _addServer.value = false } + + /* «connect the fastest server» request (widget) */ + private val _fastest = MutableStateFlow(false) + val fastest: StateFlow = _fastest + + fun requestFastest() + { + _fastest.value = true + } + + fun consumeFastest() + { + _fastest.value = false + } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/ShxWidget.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/ShxWidget.kt index b02b6d60c..f40b1d25c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/ShxWidget.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/ShxWidget.kt @@ -11,31 +11,51 @@ package org.strongswan.android.ui.widget import android.content.Context import android.content.Intent +import android.os.SystemClock +import android.widget.RemoteViews +import androidx.annotation.DrawableRes import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.glance.Button +import androidx.glance.ColorFilter import androidx.glance.GlanceId import androidx.glance.GlanceModifier -import androidx.glance.GlanceTheme -import androidx.glance.appwidget.action.actionStartActivity +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalSize +import androidx.glance.action.Action +import androidx.glance.action.clickable +import androidx.glance.appwidget.AndroidRemoteViews import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.GlanceAppWidgetReceiver import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.action.actionSendBroadcast +import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.appWidgetBackground import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.provideContent import androidx.glance.appwidget.updateAll import androidx.glance.background +import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment +import androidx.glance.layout.Box import androidx.glance.layout.Column import androidx.glance.layout.Row +import androidx.glance.layout.Spacer 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.TextStyle +import androidx.glance.unit.ColorProvider import androidx.preference.PreferenceManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -44,16 +64,22 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.strongswan.android.R import org.strongswan.android.data.VpnProfileSource +import org.strongswan.android.logic.PauseManager +import org.strongswan.android.logic.PauseReceiver import org.strongswan.android.logic.VpnStateService +import org.strongswan.android.ui.MainActivity import org.strongswan.android.ui.VpnProfileControlActivity import org.strongswan.android.utils.DefaultVpnProfile /** What the widget shows; persisted by [ShxWidgetState] so it survives process death. */ internal data class WidgetSnapshot( val state: String, - val error: Boolean, + /* ErrorState name, null without an error */ + val error: String?, val profileName: String?, val profileUuid: String?, + /* wall-clock start of the running connection, 0 if none */ + val connectedSince: Long, ) /** @@ -63,9 +89,10 @@ internal data class WidgetSnapshot( object ShxWidgetState { private const val KEY_STATE = "widget_state" - private const val KEY_ERROR = "widget_error" + private const val KEY_ERROR = "widget_error_state" private const val KEY_NAME = "widget_profile_name" private const val KEY_UUID = "widget_profile_uuid" + private const val KEY_SINCE = "widget_connected_since" private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -79,9 +106,13 @@ object ShxWidgetState ) { val app = context.applicationContext - val editor = PreferenceManager.getDefaultSharedPreferences(app).edit() + val prefs = PreferenceManager.getDefaultSharedPreferences(app) + val connected = state == VpnStateService.State.CONNECTED && error == VpnStateService.ErrorState.NO_ERROR + val since = prefs.getLong(KEY_SINCE, 0L) + val editor = prefs.edit() .putString(KEY_STATE, state.name) - .putBoolean(KEY_ERROR, error != VpnStateService.ErrorState.NO_ERROR) + .putString(KEY_ERROR, if (error == VpnStateService.ErrorState.NO_ERROR) null else error.name) + .putLong(KEY_SINCE, if (!connected) 0L else if (since == 0L) System.currentTimeMillis() else since) if (profileUuid != null) { editor.putString(KEY_NAME, profileName).putString(KEY_UUID, profileUuid) @@ -123,87 +154,295 @@ object ShxWidgetState } } val state = prefs.getString(KEY_STATE, null) ?: VpnStateService.State.DISABLED.name - WidgetSnapshot(state, prefs.getBoolean(KEY_ERROR, false), name, uuid) + WidgetSnapshot(state, prefs.getString(KEY_ERROR, null), name, uuid, prefs.getLong(KEY_SINCE, 0L)) } } -/** 2x1 widget: protection status, current profile and a connect / disconnect button. */ +/* the sizes of F_Widgets: 1×1 status icon, 2×1 status and button, 4×2 detailed */ +private val ICON = DpSize(56.dp, 56.dp) +private val SMALL = DpSize(150.dp, 64.dp) +private val LARGE = DpSize(250.dp, 150.dp) + +/** Home screen widget as on F_Widgets, in three sizes. */ class ShxWidget : GlanceAppWidget() { - override val sizeMode = SizeMode.Single + override val sizeMode = SizeMode.Responsive(setOf(ICON, SMALL, LARGE)) override suspend fun provideGlance(context: Context, id: GlanceId) { val snapshot = ShxWidgetState.snapshot(context) provideContent { - GlanceTheme { - WidgetContent(context, snapshot) + WidgetContent(context, snapshot) + } + } +} + +/* the mockup palette, light and dark */ +private fun cp(day: Long, night: Long): ColorProvider = ColorProvider(Color(day), Color(night)) +private val BG = cp(0xFFFCF8FF, 0xFF1F1D24) +private val TEXT = cp(0xFF1C1B20, 0xFFE5E1E9) +private val SUB = cp(0xFF5E5A7D, 0xFFA9A3BF) +private val ACCENT = cp(0xFF6D4AFF, 0xFF6D4AFF) +private val WHITE = cp(0xFFFFFFFF, 0xFFFFFFFF) +private val TILE = cp(0xFFF0ECF4, 0xFF2B2930) +private val ON_TILE = cp(0xFF2A2640, 0xFFE0DAF5) +private val TILE_ICON = cp(0xFF49454F, 0xFFCAC4D0) +private val ACCENT_CONTAINER = cp(0xFFE7DFFF, 0xFF3B2C88) +private val ON_ACCENT_CONTAINER = cp(0xFF6D4AFF, 0xFFB69DFF) +private val WARN_BG = cp(0xFFFFF4E0, 0xFF3A2A12) +private val WARN_TITLE = cp(0xFF6B3A10, 0xFFFFB869) +private val WARN_SUB = cp(0xFF7A3A06, 0xFFFFB869) +private val WARN_ICON_BG = cp(0xFFFFE2B8, 0xFF4A3416) +private val DOT_ON = cp(0xFF148A45, 0xFF7CE3B0) +private val DOT_OFF = cp(0xFFA39EAD, 0xFF79767F) +private val GREY = cp(0xFF79767F, 0xFF79767F) +private val WARN_STRONG = cp(0xFFB45309, 0xFFB45309) + +private enum class Look { ON, OFF, CONNECTING, PROBLEM } + +@Composable +private fun WidgetContent(context: Context, s: WidgetSnapshot) +{ + val look = when + { + s.error != null -> Look.PROBLEM + s.state == VpnStateService.State.CONNECTED.name -> Look.ON + s.state == VpnStateService.State.CONNECTING.name || s.state == VpnStateService.State.DISCONNECTING.name -> Look.CONNECTING + else -> Look.OFF + } + /* the round button: on / off, the same entry point as the quick settings tile */ + val toggle = Intent(context, VpnProfileControlActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (look == Look.ON || look == Look.CONNECTING) + { + toggle.action = VpnProfileControlActivity.DISCONNECT + } + else + { + toggle.action = VpnProfileControlActivity.START_PROFILE + toggle.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, s.profileUuid) + } + /* the text: open the app */ + val open = Intent(context, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + val size = LocalSize.current + when + { + size.width < SMALL.width -> IconWidget(look, actionStartActivity(open)) + size.width >= LARGE.width && size.height >= LARGE.height -> LargeWidget(context, s, look, actionStartActivity(toggle), actionStartActivity(open)) + else -> SmallWidget(context, s, look, actionStartActivity(toggle), actionStartActivity(open)) + } +} + +private fun title(context: Context, look: Look, error: String?): String = when (look) +{ + Look.ON -> context.getString(R.string.status_protected) + Look.OFF -> context.getString(R.string.widget_off) + Look.CONNECTING -> context.getString(R.string.widget_connecting) + Look.PROBLEM -> context.getString( + when (error) + { + VpnStateService.ErrorState.UNREACHABLE.name, VpnStateService.ErrorState.LOOKUP_FAILED.name -> R.string.widget_no_answer + VpnStateService.ErrorState.AUTH_FAILED.name, VpnStateService.ErrorState.PEER_AUTH_FAILED.name, + VpnStateService.ErrorState.PASSWORD_MISSING.name -> R.string.widget_auth_error + VpnStateService.ErrorState.CERTIFICATE_UNAVAILABLE.name -> R.string.widget_cert_error + else -> R.string.widget_problem + } + ) +} + +/* the 2-letter server code of the 2×1 widget («NL · 01:24») */ +private fun code(name: String?): String = name.orEmpty().filter { it.isLetterOrDigit() }.take(2).uppercase().ifEmpty { "VPN" } + +/* the connected time as a live Chronometer (Glance itself cannot tick) */ +private fun chrono(context: Context, since: Long, layout: Int): RemoteViews +{ + val views = RemoteViews(context.packageName, layout) + val base = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - since).coerceAtLeast(0L) + views.setChronometer(R.id.widget_chrono, base, null, true) + return views +} + +private data class ButtonLook(val bg: ColorProvider, val fg: ColorProvider, val radius: Dp, @DrawableRes val icon: Int) + +@Composable +private fun ToggleButton(look: Look, onClick: Action) +{ + val b = when (look) + { + Look.ON -> ButtonLook(ACCENT, WHITE, 22.dp, R.drawable.ic_widget_power) + Look.OFF -> ButtonLook(TILE, TILE_ICON, 22.dp, R.drawable.ic_widget_power) + Look.CONNECTING -> ButtonLook(ACCENT_CONTAINER, ON_ACCENT_CONTAINER, 22.dp, R.drawable.ic_widget_power) + Look.PROBLEM -> ButtonLook(WARN_ICON_BG, WARN_SUB, 16.dp, R.drawable.ic_widget_alert) + } + Box( + contentAlignment = Alignment.Center, + modifier = GlanceModifier.size(44.dp).cornerRadius(b.radius).background(b.bg).clickable(onClick), + ) + { + Image(ImageProvider(b.icon), contentDescription = null, colorFilter = ColorFilter.tint(b.fg), modifier = GlanceModifier.size(22.dp)) + } +} + +@Composable +private fun Card(look: Look, content: @Composable () -> Unit) +{ + Box( + modifier = GlanceModifier.fillMaxSize().appWidgetBackground().cornerRadius(28.dp) + .background(if (look == Look.PROBLEM) WARN_BG else BG).padding(14.dp), + ) + { + content() + } +} + +@Composable +private fun Title(text: String, problem: Boolean) +{ + Text(text, maxLines = 1, style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Bold, color = if (problem) WARN_TITLE else TEXT)) +} + +@Composable +private fun Sub(text: String, problem: Boolean) +{ + Text(text, maxLines = 1, style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium, color = if (problem) WARN_SUB else SUB)) +} + +/** 2×1: title, subtitle and the round on / off button. */ +@Composable +private fun SmallWidget(context: Context, s: WidgetSnapshot, look: Look, toggle: Action, open: Action) +{ + val problem = look == Look.PROBLEM + Card(look) + { + Row(GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) + { + Column(GlanceModifier.defaultWeight().clickable(open)) + { + Title(title(context, look, s.error), problem) + Spacer(GlanceModifier.height(2.dp)) + when + { + look == Look.ON && s.connectedSince > 0 -> Row(verticalAlignment = Alignment.CenterVertically) + { + Sub("${code(s.profileName)} · ", false) + AndroidRemoteViews(chrono(context, s.connectedSince, R.layout.shx_widget_chrono_sub)) + } + look == Look.OFF && s.profileName != null -> Sub(context.getString(R.string.widget_tap_to, s.profileName), false) + else -> Sub(s.profileName ?: context.getString(R.string.status_choose_profile), problem) + } + } + Spacer(GlanceModifier.width(8.dp)) + ToggleButton(look, toggle) + } + } +} + +/** 4×2: logo, title with the live time, server and button; Pause / Fastest / Change below. */ +@Composable +private fun LargeWidget(context: Context, s: WidgetSnapshot, look: Look, toggle: Action, open: Action) +{ + val problem = look == Look.PROBLEM + Card(look) + { + Column(GlanceModifier.fillMaxSize()) + { + Row(GlanceModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) + { + Box( + contentAlignment = Alignment.Center, + modifier = GlanceModifier.size(32.dp).cornerRadius(10.dp).background(ACCENT).clickable(open), + ) + { + Image(ImageProvider(R.drawable.ic_widget_logo_accent), contentDescription = null, modifier = GlanceModifier.size(22.dp)) + } + Spacer(GlanceModifier.width(10.dp)) + Column(GlanceModifier.defaultWeight().clickable(open)) + { + val t = title(context, look, s.error) + if (look == Look.ON && s.connectedSince > 0) + { + Row(verticalAlignment = Alignment.CenterVertically) + { + Title("$t · ", false) + AndroidRemoteViews(chrono(context, s.connectedSince, R.layout.shx_widget_chrono)) + } + } + else + { + Title(t, problem) + } + Spacer(GlanceModifier.height(2.dp)) + Sub(s.profileName ?: context.getString(R.string.status_choose_profile), problem) + } + Spacer(GlanceModifier.width(8.dp)) + ToggleButton(look, toggle) + } + Spacer(GlanceModifier.defaultWeight()) + Row(GlanceModifier.fillMaxWidth()) + { + val pause = Intent(context, PauseReceiver::class.java).setAction(PauseManager.ACTION_PAUSE) + val fastest = Intent(context, MainActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(MainActivity.EXTRA_CONNECT_FASTEST, true) + val change = Intent(context, MainActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(MainActivity.EXTRA_OPEN_SERVERS, true) + /* pausing only makes sense while connected; otherwise the app opens */ + Chip( + R.drawable.ic_widget_pause, context.getString(R.string.widget_pause), + if (look == Look.ON) actionSendBroadcast(pause) else open, GlanceModifier.defaultWeight(), + ) + Spacer(GlanceModifier.width(8.dp)) + Chip(R.drawable.ic_widget_bolt, context.getString(R.string.home_action_fastest), actionStartActivity(fastest), GlanceModifier.defaultWeight()) + Spacer(GlanceModifier.width(8.dp)) + Chip(R.drawable.ic_widget_swap, context.getString(R.string.notif_change), actionStartActivity(change), GlanceModifier.defaultWeight()) } } } } @Composable -private fun WidgetContent(context: Context, snapshot: WidgetSnapshot) +private fun Chip(@DrawableRes icon: Int, label: String, onClick: Action, modifier: GlanceModifier) { - val connected = snapshot.state == VpnStateService.State.CONNECTED.name && !snapshot.error - val connecting = snapshot.state == VpnStateService.State.CONNECTING.name && !snapshot.error - /* not "Unprotected" with a Connect button while the tunnel is going down */ - val disconnecting = snapshot.state == VpnStateService.State.DISCONNECTING.name && !snapshot.error - val status = context.getString( - when - { - connected -> R.string.status_protected - connecting -> R.string.status_connecting - disconnecting -> R.string.state_disconnecting - else -> R.string.status_unprotected - } - ) - val buttonLabel = context.getString(if (connected || connecting || disconnecting) R.string.disconnect else R.string.connect) - /* same entry point as the quick settings tile */ - val intent = Intent(context, VpnProfileControlActivity::class.java) - if (connected || connecting || disconnecting) - { - intent.action = VpnProfileControlActivity.DISCONNECT - } - else - { - intent.action = VpnProfileControlActivity.START_PROFILE - intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, snapshot.profileUuid) - } - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - Row( - modifier = GlanceModifier - .fillMaxSize() - .appWidgetBackground() - .background(GlanceTheme.colors.widgetBackground) - .cornerRadius(24.dp) - .padding(12.dp), - verticalAlignment = Alignment.Vertical.CenterVertically, + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.height(44.dp).cornerRadius(14.dp).background(TILE).clickable(onClick).padding(horizontal = 4.dp), ) { - Column(modifier = GlanceModifier.defaultWeight()) + Image(ImageProvider(icon), contentDescription = null, colorFilter = ColorFilter.tint(ON_TILE), modifier = GlanceModifier.size(16.dp)) + Spacer(GlanceModifier.width(6.dp)) + Text(label, maxLines = 1, style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Bold, color = ON_TILE)) + } +} + +/** 1×1: the round logo with the status dot. */ +@Composable +private fun IconWidget(look: Look, open: Action) +{ + val (bg, logo, dot) = when (look) + { + Look.ON -> Triple(ACCENT, R.drawable.ic_widget_logo_accent, DOT_ON) + Look.CONNECTING -> Triple(ACCENT, R.drawable.ic_widget_logo_accent, DOT_OFF) + Look.OFF -> Triple(GREY, R.drawable.ic_widget_logo_grey, DOT_OFF) + Look.PROBLEM -> Triple(WARN_STRONG, R.drawable.ic_widget_logo_warning, WARN_STRONG) + } + Box(GlanceModifier.fillMaxSize().appWidgetBackground().clickable(open), contentAlignment = Alignment.Center) + { + Box(GlanceModifier.size(64.dp)) { - Text( - text = status, - style = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = GlanceTheme.colors.onSurface, - ), - maxLines = 1, - ) - Text( - text = snapshot.profileName ?: context.getString(R.string.status_choose_profile), - style = TextStyle(fontSize = 12.sp, color = GlanceTheme.colors.onSurfaceVariant), - maxLines = 1, - ) + Box(GlanceModifier.size(64.dp).cornerRadius(32.dp).background(bg), contentAlignment = Alignment.Center) + { + Image(ImageProvider(logo), contentDescription = null, modifier = GlanceModifier.size(34.dp)) + } + Box(GlanceModifier.size(64.dp).padding(2.dp), contentAlignment = Alignment.BottomEnd) + { + /* the 3dp ring in the widget background around the dot */ + Box(GlanceModifier.size(16.dp).cornerRadius(8.dp).background(BG).padding(3.dp)) + { + Box(GlanceModifier.size(10.dp).cornerRadius(5.dp).background(dot)) {} + } + } } - Button( - text = buttonLabel, - onClick = actionStartActivity(intent), - ) } } diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_alert.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_alert.xml new file mode 100644 index 000000000..0982f73a4 --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_alert.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_bolt.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_bolt.xml new file mode 100644 index 000000000..dda59c3e3 --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_bolt.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_accent.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_accent.xml new file mode 100644 index 000000000..067e7f6a6 --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_accent.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_grey.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_grey.xml new file mode 100644 index 000000000..ab26d9d43 --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_grey.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_warning.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_warning.xml new file mode 100644 index 000000000..8456d2797 --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_logo_warning.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_pause.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_pause.xml new file mode 100644 index 000000000..95e4731c5 --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_pause.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_power.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_power.xml new file mode 100644 index 000000000..a02b68068 --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_power.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/frontends/android/app/src/main/res/drawable/ic_widget_swap.xml b/src/frontends/android/app/src/main/res/drawable/ic_widget_swap.xml new file mode 100644 index 000000000..25c024d6d --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable/ic_widget_swap.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/frontends/android/app/src/main/res/layout/shx_widget_chrono.xml b/src/frontends/android/app/src/main/res/layout/shx_widget_chrono.xml new file mode 100644 index 000000000..19f555dea --- /dev/null +++ b/src/frontends/android/app/src/main/res/layout/shx_widget_chrono.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/frontends/android/app/src/main/res/layout/shx_widget_chrono_sub.xml b/src/frontends/android/app/src/main/res/layout/shx_widget_chrono_sub.xml new file mode 100644 index 000000000..db0bcb0d5 --- /dev/null +++ b/src/frontends/android/app/src/main/res/layout/shx_widget_chrono_sub.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/frontends/android/app/src/main/res/values-night/colors.xml b/src/frontends/android/app/src/main/res/values-night/colors.xml index f99154b53..79d0cc1cb 100644 --- a/src/frontends/android/app/src/main/res/values-night/colors.xml +++ b/src/frontends/android/app/src/main/res/values-night/colors.xml @@ -13,4 +13,6 @@ #F87171 #FBBF24 #28A671 + #FFE5E1E9 + #FFA9A3BF diff --git a/src/frontends/android/app/src/main/res/values-ru/strings.xml b/src/frontends/android/app/src/main/res/values-ru/strings.xml index 6fb1b6e57..0c00d330d 100644 --- a/src/frontends/android/app/src/main/res/values-ru/strings.xml +++ b/src/frontends/android/app/src/main/res/values-ru/strings.xml @@ -1013,4 +1013,12 @@ Пауза 15 м Сменить RU-сайты идут напрямую + Выключен + Подключаемся + Нет ответа + Ошибка входа + Сертификат + Проблема + тап — %1$s + Пауза diff --git a/src/frontends/android/app/src/main/res/values/colors.xml b/src/frontends/android/app/src/main/res/values/colors.xml index 02087dfa2..b43e57a32 100644 --- a/src/frontends/android/app/src/main/res/values/colors.xml +++ b/src/frontends/android/app/src/main/res/values/colors.xml @@ -15,4 +15,6 @@ #DC2626 #B45309 #148A45 + #FF1C1B20 + #FF5E5A7D diff --git a/src/frontends/android/app/src/main/res/values/strings.xml b/src/frontends/android/app/src/main/res/values/strings.xml index d1a383dcd..ee4d72774 100644 --- a/src/frontends/android/app/src/main/res/values/strings.xml +++ b/src/frontends/android/app/src/main/res/values/strings.xml @@ -998,4 +998,12 @@ Pause 15 min Change RU sites go direct + Off + Connecting + No answer + Sign-in failed + Certificate + Problem + tap — %1$s + Pause diff --git a/src/frontends/android/app/src/main/res/xml/shx_widget_info.xml b/src/frontends/android/app/src/main/res/xml/shx_widget_info.xml index de0e3dd04..50e7c5786 100644 --- a/src/frontends/android/app/src/main/res/xml/shx_widget_info.xml +++ b/src/frontends/android/app/src/main/res/xml/shx_widget_info.xml @@ -1,11 +1,15 @@