implement home page widgets
This commit is contained in:
@@ -32,6 +32,79 @@
|
||||
android:name=".background.MonitorJobService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<activity
|
||||
android:name=".widget.ServerWidgetConfigureActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.NGXHttpMonitoringClient" />
|
||||
<activity
|
||||
android:name=".widget.MetricWidgetConfigureActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.NGXHttpMonitoringClient" />
|
||||
<activity
|
||||
android:name=".widget.GraphWidgetConfigureActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.NGXHttpMonitoringClient" />
|
||||
|
||||
<receiver
|
||||
android:name=".widget.FleetWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_fleet_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_fleet_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.ServerWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_server_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_server_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.MetricWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_metric_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_metric_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.GraphWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_graph_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_graph_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.IncidentsWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_incidents_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_incidents_info" />
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import net.rodakot.ngxhttpmonitoringclient.data.MonitorRepository
|
||||
import net.rodakot.ngxhttpmonitoringclient.widget.MonitorWidgetUpdater
|
||||
|
||||
class MonitorJobService : JobService() {
|
||||
private var scope: CoroutineScope? = null
|
||||
@@ -37,5 +38,6 @@ class MonitorJobService : JobService() {
|
||||
MonitorNotifications.showAlert(applicationContext, server, alert)
|
||||
}
|
||||
}
|
||||
MonitorWidgetUpdater.updateAll(applicationContext, refreshNetwork = false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package net.rodakot.ngxhttpmonitoringclient.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private val widgetScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
class FleetWidgetProvider : AppWidgetProvider() {
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
widgetScope.launch { MonitorWidgetUpdater.updateFleet(context.applicationContext, appWidgetIds, refreshNetwork = false) }
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == MonitorWidgetUpdater.ActionRefreshFleet) {
|
||||
val pending = goAsync()
|
||||
widgetScope.launch {
|
||||
try {
|
||||
val id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
val ids = if (id == AppWidgetManager.INVALID_APPWIDGET_ID) intArrayOf() else intArrayOf(id)
|
||||
MonitorWidgetUpdater.updateFleet(context.applicationContext, ids, refreshNetwork = true)
|
||||
} finally {
|
||||
pending.finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ServerWidgetProvider : AppWidgetProvider() {
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
widgetScope.launch { MonitorWidgetUpdater.updateServers(context.applicationContext, appWidgetIds, refreshNetwork = false) }
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == MonitorWidgetUpdater.ActionRefreshServer) {
|
||||
val pending = goAsync()
|
||||
widgetScope.launch {
|
||||
try {
|
||||
val id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
val ids = if (id == AppWidgetManager.INVALID_APPWIDGET_ID) intArrayOf() else intArrayOf(id)
|
||||
MonitorWidgetUpdater.updateServers(context.applicationContext, ids, refreshNetwork = true)
|
||||
} finally {
|
||||
pending.finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
WidgetPreferences.delete(context, appWidgetIds)
|
||||
}
|
||||
}
|
||||
|
||||
class MetricWidgetProvider : AppWidgetProvider() {
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
widgetScope.launch { MonitorWidgetUpdater.updateMetrics(context.applicationContext, appWidgetIds, refreshNetwork = false) }
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == MonitorWidgetUpdater.ActionRefreshMetric) {
|
||||
val pending = goAsync()
|
||||
widgetScope.launch {
|
||||
try {
|
||||
val id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
val ids = if (id == AppWidgetManager.INVALID_APPWIDGET_ID) intArrayOf() else intArrayOf(id)
|
||||
MonitorWidgetUpdater.updateMetrics(context.applicationContext, ids, refreshNetwork = true)
|
||||
} finally {
|
||||
pending.finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
WidgetPreferences.delete(context, appWidgetIds)
|
||||
}
|
||||
}
|
||||
|
||||
class GraphWidgetProvider : AppWidgetProvider() {
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
widgetScope.launch { MonitorWidgetUpdater.updateGraphs(context.applicationContext, appWidgetIds, refreshNetwork = false) }
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == MonitorWidgetUpdater.ActionRefreshGraph) {
|
||||
val pending = goAsync()
|
||||
widgetScope.launch {
|
||||
try {
|
||||
val id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
val ids = if (id == AppWidgetManager.INVALID_APPWIDGET_ID) intArrayOf() else intArrayOf(id)
|
||||
MonitorWidgetUpdater.updateGraphs(context.applicationContext, ids, refreshNetwork = true)
|
||||
} finally {
|
||||
pending.finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
WidgetPreferences.delete(context, appWidgetIds)
|
||||
}
|
||||
}
|
||||
|
||||
class IncidentsWidgetProvider : AppWidgetProvider() {
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
widgetScope.launch { MonitorWidgetUpdater.updateIncidents(context.applicationContext, appWidgetIds, refreshNetwork = false) }
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == MonitorWidgetUpdater.ActionRefreshIncidents) {
|
||||
val pending = goAsync()
|
||||
widgetScope.launch {
|
||||
try {
|
||||
val id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
val ids = if (id == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
AppWidgetManager.getInstance(context).getAppWidgetIds(
|
||||
android.content.ComponentName(context, IncidentsWidgetProvider::class.java),
|
||||
)
|
||||
} else {
|
||||
intArrayOf(id)
|
||||
}
|
||||
MonitorWidgetUpdater.updateIncidents(context.applicationContext, ids, refreshNetwork = true)
|
||||
} finally {
|
||||
pending.finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
package net.rodakot.ngxhttpmonitoringclient.widget
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import android.widget.RemoteViews
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import net.rodakot.ngxhttpmonitoringclient.MainActivity
|
||||
import net.rodakot.ngxhttpmonitoringclient.R
|
||||
import net.rodakot.ngxhttpmonitoringclient.data.MonitorRepository
|
||||
import net.rodakot.ngxhttpmonitoringclient.model.MetricSummary
|
||||
import net.rodakot.ngxhttpmonitoringclient.model.ServerProfile
|
||||
import net.rodakot.ngxhttpmonitoringclient.model.ServerStatus
|
||||
|
||||
object MonitorWidgetUpdater {
|
||||
const val ActionRefreshFleet = "net.rodakot.ngxhttpmonitoringclient.widget.REFRESH_FLEET"
|
||||
const val ActionRefreshServer = "net.rodakot.ngxhttpmonitoringclient.widget.REFRESH_SERVER"
|
||||
const val ActionRefreshMetric = "net.rodakot.ngxhttpmonitoringclient.widget.REFRESH_METRIC"
|
||||
const val ActionRefreshGraph = "net.rodakot.ngxhttpmonitoringclient.widget.REFRESH_GRAPH"
|
||||
const val ActionRefreshIncidents = "net.rodakot.ngxhttpmonitoringclient.widget.REFRESH_INCIDENTS"
|
||||
|
||||
private val Green = Color.rgb(53, 211, 139)
|
||||
private val Cyan = Color.rgb(76, 201, 240)
|
||||
private val Amber = Color.rgb(255, 184, 77)
|
||||
private val Red = Color.rgb(255, 90, 82)
|
||||
private val Text = Color.rgb(234, 240, 247)
|
||||
private val Muted = Color.rgb(154, 166, 182)
|
||||
private val Panel = Color.rgb(32, 39, 51)
|
||||
private val Grid = Color.rgb(55, 68, 84)
|
||||
|
||||
fun updateAll(context: Context, refreshNetwork: Boolean = false) {
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
updateFleet(context, manager.idsFor(context, FleetWidgetProvider::class.java), refreshNetwork)
|
||||
updateServers(context, manager.idsFor(context, ServerWidgetProvider::class.java), refreshNetwork)
|
||||
updateMetrics(context, manager.idsFor(context, MetricWidgetProvider::class.java), refreshNetwork)
|
||||
updateGraphs(context, manager.idsFor(context, GraphWidgetProvider::class.java), refreshNetwork)
|
||||
updateIncidents(context, manager.idsFor(context, IncidentsWidgetProvider::class.java), refreshNetwork)
|
||||
}
|
||||
|
||||
fun updateFleet(context: Context, appWidgetIds: IntArray, refreshNetwork: Boolean) {
|
||||
if (appWidgetIds.isEmpty()) return
|
||||
val repository = MonitorRepository(context)
|
||||
val servers = repository.servers()
|
||||
if (refreshNetwork) {
|
||||
servers.filter { it.enabled }.forEach { repository.refreshServer(it, forcePersist = true) }
|
||||
}
|
||||
val summaries = repository.latestSummaries(servers.map { it.id })
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
manager.updateAppWidget(appWidgetId, fleetViews(context, servers, summaries, appWidgetId))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateServers(context: Context, appWidgetIds: IntArray, refreshNetwork: Boolean) {
|
||||
if (appWidgetIds.isEmpty()) return
|
||||
val repository = MonitorRepository(context)
|
||||
val servers = repository.servers()
|
||||
val summaries = mutableMapOf<String, MetricSummary>().apply {
|
||||
putAll(repository.latestSummaries(servers.map { it.id }))
|
||||
}
|
||||
val refreshed = mutableSetOf<String>()
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
val server = configuredServer(context, appWidgetId, servers)
|
||||
if (refreshNetwork && server != null && refreshed.add(server.id)) {
|
||||
summaries[server.id] = repository.refreshServer(server, forcePersist = true).summary
|
||||
}
|
||||
manager.updateAppWidget(appWidgetId, serverViews(context, appWidgetId, server, server?.let { summaries[it.id] }))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMetrics(context: Context, appWidgetIds: IntArray, refreshNetwork: Boolean) {
|
||||
if (appWidgetIds.isEmpty()) return
|
||||
val repository = MonitorRepository(context)
|
||||
val servers = repository.servers()
|
||||
val summaries = mutableMapOf<String, MetricSummary>().apply {
|
||||
putAll(repository.latestSummaries(servers.map { it.id }))
|
||||
}
|
||||
val refreshed = mutableSetOf<String>()
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
val server = configuredServer(context, appWidgetId, servers)
|
||||
if (refreshNetwork && server != null && refreshed.add(server.id)) {
|
||||
summaries[server.id] = repository.refreshServer(server, forcePersist = true).summary
|
||||
}
|
||||
val history = server?.let { repository.history(it.id).asReversed().takeLast(48) }.orEmpty()
|
||||
manager.updateAppWidget(
|
||||
appWidgetId,
|
||||
metricViews(
|
||||
context = context,
|
||||
appWidgetId = appWidgetId,
|
||||
server = server,
|
||||
summary = server?.let { summaries[it.id] },
|
||||
history = history,
|
||||
metric = WidgetPreferences.metricKind(context, appWidgetId),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGraphs(context: Context, appWidgetIds: IntArray, refreshNetwork: Boolean) {
|
||||
if (appWidgetIds.isEmpty()) return
|
||||
val repository = MonitorRepository(context)
|
||||
val servers = repository.servers()
|
||||
val summaries = mutableMapOf<String, MetricSummary>().apply {
|
||||
putAll(repository.latestSummaries(servers.map { it.id }))
|
||||
}
|
||||
val refreshed = mutableSetOf<String>()
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
val server = configuredServer(context, appWidgetId, servers)
|
||||
if (refreshNetwork && server != null && refreshed.add(server.id)) {
|
||||
summaries[server.id] = repository.refreshServer(server, forcePersist = true).summary
|
||||
}
|
||||
val history = server?.let { repository.history(it.id).asReversed().takeLast(60) }.orEmpty()
|
||||
manager.updateAppWidget(
|
||||
appWidgetId,
|
||||
graphViews(
|
||||
context = context,
|
||||
appWidgetId = appWidgetId,
|
||||
server = server,
|
||||
summary = server?.let { summaries[it.id] },
|
||||
history = history,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateIncidents(context: Context, appWidgetIds: IntArray, refreshNetwork: Boolean) {
|
||||
if (appWidgetIds.isEmpty()) return
|
||||
val repository = MonitorRepository(context)
|
||||
val servers = repository.servers()
|
||||
if (refreshNetwork) {
|
||||
servers.filter { it.enabled }.forEach { repository.refreshServer(it, forcePersist = true) }
|
||||
}
|
||||
val summaries = repository.latestSummaries(servers.map { it.id })
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
manager.updateAppWidget(appWidgetId, incidentsViews(context, servers, summaries, appWidgetId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun fleetViews(
|
||||
context: Context,
|
||||
servers: List<ServerProfile>,
|
||||
summaries: Map<String, MetricSummary>,
|
||||
appWidgetId: Int,
|
||||
): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_fleet)
|
||||
val counters = counters(servers, summaries)
|
||||
val headline = fleetHeadline(counters)
|
||||
views.setTextViewText(R.id.widget_title, "Fleet Pulse")
|
||||
views.setTextViewText(R.id.widget_subtitle, "${servers.size} servers - ${formatTime(System.currentTimeMillis())}")
|
||||
views.setTextViewText(R.id.widget_status, headline)
|
||||
views.setTextColor(R.id.widget_status, colorForFleet(counters))
|
||||
views.setImageViewBitmap(R.id.widget_graph, fleetStatusBitmap(servers, summaries))
|
||||
views.setTextViewText(R.id.widget_online, "${counters.online} OK")
|
||||
views.setTextViewText(R.id.widget_watch, "${counters.watch} WATCH")
|
||||
views.setTextViewText(R.id.widget_down, "${counters.down} DOWN")
|
||||
views.setTextViewText(R.id.widget_priority, fleetPriority(servers, summaries))
|
||||
views.bindCommon(context, FleetWidgetProvider::class.java, ActionRefreshFleet, appWidgetId)
|
||||
return views
|
||||
}
|
||||
|
||||
private fun serverViews(
|
||||
context: Context,
|
||||
appWidgetId: Int,
|
||||
server: ServerProfile?,
|
||||
summary: MetricSummary?,
|
||||
): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_server)
|
||||
if (server == null) {
|
||||
views.setTextViewText(R.id.widget_title, "Choose server")
|
||||
views.setTextViewText(R.id.widget_status, "Create a server in the app first")
|
||||
views.setTextViewText(R.id.widget_health, "NO SERVER")
|
||||
views.setTextColor(R.id.widget_health, Muted)
|
||||
clearServerMetrics(views)
|
||||
} else {
|
||||
val status = summary?.status ?: ServerStatus.Unknown
|
||||
views.setTextViewText(R.id.widget_title, server.name)
|
||||
views.setTextViewText(R.id.widget_status, summary?.message ?: "Waiting for first sample")
|
||||
views.setTextColor(R.id.widget_status, colorForStatus(status))
|
||||
views.setTextViewText(R.id.widget_health, statusLabel(status).uppercase(Locale.US))
|
||||
views.setTextColor(R.id.widget_health, colorForStatus(status))
|
||||
setPercentBar(views, R.id.widget_cpu_bar, R.id.widget_cpu_value, summary?.cpuPercent)
|
||||
setPercentBar(views, R.id.widget_memory_bar, R.id.widget_memory_value, summary?.memoryPercent)
|
||||
setPercentBar(views, R.id.widget_disk_bar, R.id.widget_disk_value, summary?.diskPercent)
|
||||
views.setTextViewText(R.id.widget_rps, "RPS ${summary?.requestRate.number()}")
|
||||
views.setTextViewText(R.id.widget_p95, "P95 ${summary?.latencyP95Millis.ms()}")
|
||||
views.setTextViewText(R.id.widget_5xx, "5XX ${summary?.errors5xx?.toString() ?: "--"}")
|
||||
views.setTextColor(R.id.widget_5xx, if ((summary?.errors5xx ?: 0) > 0) Red else Green)
|
||||
}
|
||||
views.bindCommon(context, ServerWidgetProvider::class.java, ActionRefreshServer, appWidgetId)
|
||||
return views
|
||||
}
|
||||
|
||||
private fun metricViews(
|
||||
context: Context,
|
||||
appWidgetId: Int,
|
||||
server: ServerProfile?,
|
||||
summary: MetricSummary?,
|
||||
history: List<MetricSummary>,
|
||||
metric: WidgetMetricKind,
|
||||
): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_metric)
|
||||
if (server == null) {
|
||||
views.setTextViewText(R.id.widget_title, "Choose server")
|
||||
views.setTextViewText(R.id.widget_value, "--")
|
||||
views.setTextViewText(R.id.widget_status, "Add a server, then add this widget again")
|
||||
views.setImageViewBitmap(R.id.widget_graph, emptyBitmap("No samples"))
|
||||
} else {
|
||||
val color = colorForMetric(metric, summary)
|
||||
views.setTextViewText(R.id.widget_title, server.name)
|
||||
views.setTextViewText(R.id.widget_value, valueFor(metric, summary))
|
||||
views.setTextColor(R.id.widget_value, color)
|
||||
views.setTextViewText(
|
||||
R.id.widget_status,
|
||||
"${metric.label} - ${statusLabel(summary?.status ?: ServerStatus.Unknown)} - ${formatTime(summary?.timestampMillis)}",
|
||||
)
|
||||
views.setImageViewBitmap(
|
||||
R.id.widget_graph,
|
||||
sparklineBitmap(
|
||||
values = metricValues(metric, history, summary),
|
||||
metric = metric,
|
||||
color = color,
|
||||
),
|
||||
)
|
||||
}
|
||||
views.bindCommon(context, MetricWidgetProvider::class.java, ActionRefreshMetric, appWidgetId)
|
||||
return views
|
||||
}
|
||||
|
||||
private fun graphViews(
|
||||
context: Context,
|
||||
appWidgetId: Int,
|
||||
server: ServerProfile?,
|
||||
summary: MetricSummary?,
|
||||
history: List<MetricSummary>,
|
||||
): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_graph)
|
||||
if (server == null) {
|
||||
views.setTextViewText(R.id.widget_title, "Choose server")
|
||||
views.setTextViewText(R.id.widget_status, "Create a server in the app first")
|
||||
views.setTextViewText(R.id.widget_cpu, "CPU --")
|
||||
views.setTextViewText(R.id.widget_memory, "MEM --")
|
||||
views.setTextViewText(R.id.widget_disk, "STORAGE --")
|
||||
views.setImageViewBitmap(R.id.widget_graph, emptyBitmap("No samples"))
|
||||
} else {
|
||||
val status = summary?.status ?: ServerStatus.Unknown
|
||||
views.setTextViewText(R.id.widget_title, "${server.name} flow")
|
||||
views.setTextViewText(R.id.widget_status, "${statusLabel(status)} - ${formatTime(summary?.timestampMillis)}")
|
||||
views.setTextColor(R.id.widget_status, colorForStatus(status))
|
||||
views.setTextViewText(R.id.widget_cpu, "CPU ${summary?.cpuPercent.percent()}")
|
||||
views.setTextViewText(R.id.widget_memory, "MEM ${summary?.memoryPercent.percent()}")
|
||||
views.setTextViewText(R.id.widget_disk, "STORAGE ${summary?.diskPercent.percent()}")
|
||||
views.setImageViewBitmap(R.id.widget_graph, multilineBitmap(history, summary))
|
||||
}
|
||||
views.bindCommon(context, GraphWidgetProvider::class.java, ActionRefreshGraph, appWidgetId)
|
||||
return views
|
||||
}
|
||||
|
||||
private fun incidentsViews(
|
||||
context: Context,
|
||||
servers: List<ServerProfile>,
|
||||
summaries: Map<String, MetricSummary>,
|
||||
appWidgetId: Int,
|
||||
): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_incidents)
|
||||
views.setTextViewText(R.id.widget_title, "Incident Watch")
|
||||
views.setTextViewText(R.id.widget_subtitle, "${servers.size} servers - ${formatTime(System.currentTimeMillis())}")
|
||||
if (servers.isEmpty()) {
|
||||
views.setTextViewText(R.id.widget_issue_count, "--")
|
||||
views.setTextViewText(R.id.widget_issue_label, "SETUP")
|
||||
views.setTextColor(R.id.widget_issue_count, Muted)
|
||||
views.setTextViewText(R.id.widget_issue_one, "No servers configured")
|
||||
views.setTextViewText(R.id.widget_issue_two, "Open the app and add endpoints")
|
||||
views.setTextViewText(R.id.widget_issue_three, "Then place this widget again")
|
||||
} else {
|
||||
val issues = servers
|
||||
.map { it to summaries[it.id] }
|
||||
.filter { (_, summary) -> isIssueStatus(summary?.status ?: ServerStatus.Unknown) }
|
||||
.sortedWith(compareBy<Pair<ServerProfile, MetricSummary?>> { statusRank(it.second?.status) }.thenBy { it.first.name.lowercase() })
|
||||
if (issues.isEmpty()) {
|
||||
views.setTextViewText(R.id.widget_issue_count, "OK")
|
||||
views.setTextViewText(R.id.widget_issue_label, "CLEAR")
|
||||
views.setTextColor(R.id.widget_issue_count, Green)
|
||||
views.setTextColor(R.id.widget_issue_label, Green)
|
||||
views.setTextViewText(R.id.widget_issue_one, "All monitored servers look healthy")
|
||||
views.setTextColor(R.id.widget_issue_one, Green)
|
||||
views.setTextViewText(R.id.widget_issue_two, "No unreachable or degraded nodes")
|
||||
views.setTextColor(R.id.widget_issue_two, Muted)
|
||||
views.setTextViewText(R.id.widget_issue_three, "Last update ${formatTime(System.currentTimeMillis())}")
|
||||
views.setTextColor(R.id.widget_issue_three, Muted)
|
||||
} else {
|
||||
views.setTextViewText(R.id.widget_issue_count, issues.size.toString())
|
||||
views.setTextViewText(R.id.widget_issue_label, "ISSUES")
|
||||
views.setTextColor(R.id.widget_issue_count, if (issues.any { isDownStatus(it.second?.status) }) Red else Amber)
|
||||
views.setTextColor(R.id.widget_issue_label, if (issues.any { isDownStatus(it.second?.status) }) Red else Amber)
|
||||
val rows = issues.take(3).map { (server, summary) ->
|
||||
"${statusLabel(summary?.status ?: ServerStatus.Unknown)} - ${server.name}"
|
||||
}
|
||||
views.setTextViewText(R.id.widget_issue_one, rows.getOrNull(0) ?: "")
|
||||
views.setTextViewText(R.id.widget_issue_two, rows.getOrNull(1) ?: "")
|
||||
views.setTextViewText(R.id.widget_issue_three, rows.getOrNull(2) ?: "Open app for the full queue")
|
||||
views.setTextColor(R.id.widget_issue_one, colorForStatus(issues.getOrNull(0)?.second?.status ?: ServerStatus.Unknown))
|
||||
views.setTextColor(R.id.widget_issue_two, colorForStatus(issues.getOrNull(1)?.second?.status ?: ServerStatus.Unknown))
|
||||
views.setTextColor(R.id.widget_issue_three, colorForStatus(issues.getOrNull(2)?.second?.status ?: ServerStatus.Unknown))
|
||||
}
|
||||
}
|
||||
views.bindCommon(context, IncidentsWidgetProvider::class.java, ActionRefreshIncidents, appWidgetId)
|
||||
return views
|
||||
}
|
||||
|
||||
private fun setPercentBar(views: RemoteViews, barId: Int, valueId: Int, value: Double?) {
|
||||
views.setProgressBar(barId, 100, value.progressValue(), false)
|
||||
views.setTextViewText(valueId, value.percent())
|
||||
views.setTextColor(valueId, colorForPercent(value))
|
||||
}
|
||||
|
||||
private fun clearServerMetrics(views: RemoteViews) {
|
||||
setPercentBar(views, R.id.widget_cpu_bar, R.id.widget_cpu_value, null)
|
||||
setPercentBar(views, R.id.widget_memory_bar, R.id.widget_memory_value, null)
|
||||
setPercentBar(views, R.id.widget_disk_bar, R.id.widget_disk_value, null)
|
||||
views.setTextViewText(R.id.widget_rps, "RPS --")
|
||||
views.setTextViewText(R.id.widget_p95, "P95 --")
|
||||
views.setTextViewText(R.id.widget_5xx, "5XX --")
|
||||
views.setTextColor(R.id.widget_5xx, Muted)
|
||||
}
|
||||
|
||||
private fun configuredServer(context: Context, appWidgetId: Int, servers: List<ServerProfile>): ServerProfile? {
|
||||
val configured = WidgetPreferences.serverId(context, appWidgetId)
|
||||
return servers.firstOrNull { it.id == configured } ?: servers.firstOrNull()
|
||||
}
|
||||
|
||||
private fun RemoteViews.bindCommon(
|
||||
context: Context,
|
||||
providerClass: Class<*>,
|
||||
refreshAction: String,
|
||||
appWidgetId: Int,
|
||||
) {
|
||||
setOnClickPendingIntent(R.id.widget_root, openAppIntent(context, appWidgetId))
|
||||
setOnClickPendingIntent(R.id.widget_refresh, refreshIntent(context, providerClass, refreshAction, appWidgetId))
|
||||
}
|
||||
|
||||
private fun openAppIntent(context: Context, appWidgetId: Int): PendingIntent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
50_000 + appWidgetId,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun refreshIntent(
|
||||
context: Context,
|
||||
providerClass: Class<*>,
|
||||
action: String,
|
||||
appWidgetId: Int,
|
||||
): PendingIntent {
|
||||
val intent = Intent(context, providerClass)
|
||||
.setAction(action)
|
||||
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
70_000 + appWidgetId,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun AppWidgetManager.idsFor(context: Context, providerClass: Class<*>): IntArray {
|
||||
return getAppWidgetIds(ComponentName(context, providerClass))
|
||||
}
|
||||
|
||||
private fun counters(servers: List<ServerProfile>, summaries: Map<String, MetricSummary>): Counters {
|
||||
var online = 0
|
||||
var watch = 0
|
||||
var down = 0
|
||||
var unknown = 0
|
||||
servers.forEach { server ->
|
||||
when (summaries[server.id]?.status ?: ServerStatus.Unknown) {
|
||||
ServerStatus.Online -> online += 1
|
||||
ServerStatus.Degraded, ServerStatus.RateLimited, ServerStatus.Error, ServerStatus.Insecure -> watch += 1
|
||||
ServerStatus.Offline, ServerStatus.AuthFailed, ServerStatus.Forbidden, ServerStatus.Missing -> down += 1
|
||||
ServerStatus.Unknown -> unknown += 1
|
||||
}
|
||||
}
|
||||
return Counters(online, watch, down, unknown, servers.size)
|
||||
}
|
||||
|
||||
private fun fleetHeadline(counters: Counters): String = when {
|
||||
counters.total == 0 -> "Add servers"
|
||||
counters.down > 0 -> "${counters.down} down"
|
||||
counters.watch > 0 -> "${counters.watch} on watch"
|
||||
counters.unknown == counters.total -> "Waiting"
|
||||
counters.unknown > 0 -> "${counters.unknown} waiting"
|
||||
else -> "All clear"
|
||||
}
|
||||
|
||||
private fun colorForFleet(counters: Counters): Int = when {
|
||||
counters.down > 0 -> Red
|
||||
counters.watch > 0 -> Amber
|
||||
counters.unknown > 0 -> Muted
|
||||
else -> Green
|
||||
}
|
||||
|
||||
private fun fleetPriority(servers: List<ServerProfile>, summaries: Map<String, MetricSummary>): String {
|
||||
if (servers.isEmpty()) return "Open the app and add your first monitor endpoint"
|
||||
val priority = servers
|
||||
.sortedWith(compareBy<ServerProfile> { statusRank(summaries[it.id]?.status) }.thenBy { it.name.lowercase() })
|
||||
.filter { isIssueStatus(summaries[it.id]?.status ?: ServerStatus.Unknown) }
|
||||
.take(2)
|
||||
if (priority.isEmpty()) return "No active incidents"
|
||||
return priority.joinToString(" ") { server ->
|
||||
"${statusLabel(summaries[server.id]?.status ?: ServerStatus.Unknown)} - ${server.name}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun isIssueStatus(status: ServerStatus): Boolean = when (status) {
|
||||
ServerStatus.Degraded,
|
||||
ServerStatus.Offline,
|
||||
ServerStatus.AuthFailed,
|
||||
ServerStatus.Forbidden,
|
||||
ServerStatus.Missing,
|
||||
ServerStatus.RateLimited,
|
||||
ServerStatus.Insecure,
|
||||
ServerStatus.Error,
|
||||
-> true
|
||||
ServerStatus.Online, ServerStatus.Unknown -> false
|
||||
}
|
||||
|
||||
private fun isDownStatus(status: ServerStatus?): Boolean = status in setOf(
|
||||
ServerStatus.Offline,
|
||||
ServerStatus.AuthFailed,
|
||||
ServerStatus.Forbidden,
|
||||
ServerStatus.Missing,
|
||||
)
|
||||
|
||||
private fun statusRank(status: ServerStatus?): Int = when (status) {
|
||||
ServerStatus.Offline, ServerStatus.AuthFailed, ServerStatus.Forbidden, ServerStatus.Missing -> 0
|
||||
ServerStatus.Degraded, ServerStatus.RateLimited, ServerStatus.Error -> 1
|
||||
ServerStatus.Insecure -> 2
|
||||
ServerStatus.Unknown, null -> 3
|
||||
ServerStatus.Online -> 4
|
||||
}
|
||||
|
||||
private fun statusLabel(status: ServerStatus): String = when (status) {
|
||||
ServerStatus.Online -> "Online"
|
||||
ServerStatus.Degraded -> "Watch"
|
||||
ServerStatus.Offline -> "Offline"
|
||||
ServerStatus.AuthFailed -> "Auth"
|
||||
ServerStatus.Forbidden -> "Blocked"
|
||||
ServerStatus.Missing -> "Missing"
|
||||
ServerStatus.RateLimited -> "Limited"
|
||||
ServerStatus.Insecure -> "HTTP"
|
||||
ServerStatus.Error -> "Error"
|
||||
ServerStatus.Unknown -> "Waiting"
|
||||
}
|
||||
|
||||
private fun valueFor(metric: WidgetMetricKind, summary: MetricSummary?): String = when (metric) {
|
||||
WidgetMetricKind.Cpu -> summary?.cpuPercent.percent()
|
||||
WidgetMetricKind.Memory -> summary?.memoryPercent.percent()
|
||||
WidgetMetricKind.Disk -> summary?.diskPercent.percent()
|
||||
WidgetMetricKind.RequestRate -> summary?.requestRate.number()
|
||||
WidgetMetricKind.LatencyP95 -> summary?.latencyP95Millis.ms()
|
||||
WidgetMetricKind.Errors5xx -> summary?.errors5xx?.toString() ?: "--"
|
||||
WidgetMetricKind.Connections -> summary?.activeConnections?.toString() ?: "--"
|
||||
}
|
||||
|
||||
private fun metricValues(metric: WidgetMetricKind, history: List<MetricSummary>, summary: MetricSummary?): List<Double?> {
|
||||
val samples = if (history.isEmpty()) listOfNotNull(summary) else history
|
||||
return samples.takeLast(48).map { metricValue(metric, it) }
|
||||
}
|
||||
|
||||
private fun metricValue(metric: WidgetMetricKind, summary: MetricSummary): Double? = when (metric) {
|
||||
WidgetMetricKind.Cpu -> summary.cpuPercent
|
||||
WidgetMetricKind.Memory -> summary.memoryPercent
|
||||
WidgetMetricKind.Disk -> summary.diskPercent
|
||||
WidgetMetricKind.RequestRate -> summary.requestRate
|
||||
WidgetMetricKind.LatencyP95 -> summary.latencyP95Millis
|
||||
WidgetMetricKind.Errors5xx -> summary.errors5xx?.toDouble()
|
||||
WidgetMetricKind.Connections -> summary.activeConnections?.toDouble()
|
||||
}
|
||||
|
||||
private fun colorForMetric(metric: WidgetMetricKind, summary: MetricSummary?): Int = when (metric) {
|
||||
WidgetMetricKind.Cpu -> colorForPercent(summary?.cpuPercent)
|
||||
WidgetMetricKind.Memory -> colorForPercent(summary?.memoryPercent)
|
||||
WidgetMetricKind.Disk -> colorForPercent(summary?.diskPercent)
|
||||
WidgetMetricKind.Errors5xx -> if ((summary?.errors5xx ?: 0) > 0) Red else Green
|
||||
WidgetMetricKind.LatencyP95 -> Amber
|
||||
WidgetMetricKind.RequestRate, WidgetMetricKind.Connections -> Cyan
|
||||
}
|
||||
|
||||
private fun colorForStatus(status: ServerStatus): Int = when (status) {
|
||||
ServerStatus.Online -> Green
|
||||
ServerStatus.Degraded, ServerStatus.RateLimited, ServerStatus.Error -> Amber
|
||||
ServerStatus.Offline, ServerStatus.AuthFailed, ServerStatus.Forbidden, ServerStatus.Missing -> Red
|
||||
ServerStatus.Insecure -> Cyan
|
||||
ServerStatus.Unknown -> Muted
|
||||
}
|
||||
|
||||
private fun colorForPercent(value: Double?): Int = when {
|
||||
value == null -> Muted
|
||||
value >= 90.0 -> Red
|
||||
value >= 75.0 -> Amber
|
||||
else -> Green
|
||||
}
|
||||
|
||||
private fun fleetStatusBitmap(
|
||||
servers: List<ServerProfile>,
|
||||
summaries: Map<String, MetricSummary>,
|
||||
width: Int = 640,
|
||||
height: Int = 88,
|
||||
): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
paint.color = Panel
|
||||
canvas.drawRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), 24f, 24f, paint)
|
||||
if (servers.isEmpty()) {
|
||||
paint.color = Muted
|
||||
repeat(6) { index ->
|
||||
val left = 22f + index * 98f
|
||||
canvas.drawRoundRect(RectF(left, 26f, left + 60f, 62f), 18f, 18f, paint)
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
val statuses = servers
|
||||
.map { summaries[it.id]?.status ?: ServerStatus.Unknown }
|
||||
.sortedBy { statusRank(it) }
|
||||
val count = statuses.size.coerceAtLeast(1)
|
||||
val outer = 14f
|
||||
val gap = if (count > 38) 2f else 5f
|
||||
val cellWidth = ((width - outer * 2 - gap * (count - 1)) / count).coerceAtLeast(3f)
|
||||
statuses.forEachIndexed { index, status ->
|
||||
val left = outer + index * (cellWidth + gap)
|
||||
val right = min(width - outer, left + cellWidth)
|
||||
paint.color = colorForStatus(status)
|
||||
canvas.drawRoundRect(RectF(left, 18f, right, height - 18f), 18f, 18f, paint)
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun sparklineBitmap(
|
||||
values: List<Double?>,
|
||||
metric: WidgetMetricKind,
|
||||
color: Int,
|
||||
width: Int = 520,
|
||||
height: Int = 150,
|
||||
): Bitmap {
|
||||
val clean = values.filterNotNull().takeLast(48)
|
||||
if (clean.isEmpty()) return emptyBitmap("No samples", width, height)
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val graph = RectF(10f, 12f, width - 10f, height - 16f)
|
||||
drawGraphBackground(canvas, graph)
|
||||
val maxValue = if (metric.isPercentMetric()) {
|
||||
100.0
|
||||
} else {
|
||||
max(1.0, clean.maxOrNull().orZero() * 1.25)
|
||||
}
|
||||
drawLine(
|
||||
canvas = canvas,
|
||||
values = clean,
|
||||
graph = graph,
|
||||
color = color,
|
||||
minValue = 0.0,
|
||||
maxValue = maxValue,
|
||||
stroke = 7f,
|
||||
fill = true,
|
||||
)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun multilineBitmap(
|
||||
history: List<MetricSummary>,
|
||||
summary: MetricSummary?,
|
||||
width: Int = 720,
|
||||
height: Int = 230,
|
||||
): Bitmap {
|
||||
val samples = (if (history.isEmpty()) listOfNotNull(summary) else history).takeLast(60)
|
||||
if (samples.isEmpty()) return emptyBitmap("No samples", width, height)
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val graph = RectF(12f, 10f, width - 12f, height - 14f)
|
||||
drawGraphBackground(canvas, graph)
|
||||
drawLine(canvas, samples.map { it.cpuPercent }, graph, Green, minValue = 0.0, maxValue = 100.0, stroke = 5f)
|
||||
drawLine(canvas, samples.map { it.memoryPercent }, graph, Cyan, minValue = 0.0, maxValue = 100.0, stroke = 5f)
|
||||
drawLine(canvas, samples.map { it.diskPercent }, graph, Amber, minValue = 0.0, maxValue = 100.0, stroke = 5f)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun emptyBitmap(label: String, width: Int = 520, height: Int = 150): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val graph = RectF(10f, 10f, width - 10f, height - 10f)
|
||||
drawGraphBackground(canvas, graph)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Muted
|
||||
textSize = 32f
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
canvas.drawText(label, width / 2f, height / 2f + 12f, paint)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun drawGraphBackground(canvas: Canvas, graph: RectF) {
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
paint.color = Panel
|
||||
canvas.drawRoundRect(graph, 24f, 24f, paint)
|
||||
paint.color = withAlpha(Grid, 120)
|
||||
paint.strokeWidth = 2f
|
||||
repeat(4) { index ->
|
||||
val y = graph.top + graph.height() * (index + 1) / 5f
|
||||
canvas.drawLine(graph.left + 12f, y, graph.right - 12f, y, paint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawLine(
|
||||
canvas: Canvas,
|
||||
values: List<Double?>,
|
||||
graph: RectF,
|
||||
color: Int,
|
||||
minValue: Double,
|
||||
maxValue: Double,
|
||||
stroke: Float,
|
||||
fill: Boolean = false,
|
||||
) {
|
||||
val clean = values.filterNotNull()
|
||||
if (clean.isEmpty()) return
|
||||
val plotted = if (clean.size == 1) listOf(clean.first(), clean.first()) else clean
|
||||
val path = Path()
|
||||
plotted.forEachIndexed { index, value ->
|
||||
val x = graph.left + 18f + index * ((graph.width() - 36f) / (plotted.size - 1).coerceAtLeast(1))
|
||||
val normalized = ((value - minValue) / (maxValue - minValue).coerceAtLeast(1.0)).coerceIn(0.0, 1.0)
|
||||
val y = graph.bottom - 16f - (graph.height() - 32f) * normalized.toFloat()
|
||||
if (index == 0) path.moveTo(x, y) else path.lineTo(x, y)
|
||||
}
|
||||
if (fill) {
|
||||
val fillPath = Path(path)
|
||||
fillPath.lineTo(graph.right - 18f, graph.bottom - 16f)
|
||||
fillPath.lineTo(graph.left + 18f, graph.bottom - 16f)
|
||||
fillPath.close()
|
||||
val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
this.color = withAlpha(color, 45)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
}
|
||||
val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
this.color = color
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
strokeWidth = stroke
|
||||
}
|
||||
canvas.drawPath(path, linePaint)
|
||||
val last = plotted.last()
|
||||
val lastX = graph.right - 18f
|
||||
val lastY = graph.bottom - 16f - (graph.height() - 32f) *
|
||||
((last - minValue) / (maxValue - minValue).coerceAtLeast(1.0)).coerceIn(0.0, 1.0).toFloat()
|
||||
val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
this.color = color
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
canvas.drawCircle(lastX, lastY, stroke * 1.25f, dotPaint)
|
||||
}
|
||||
|
||||
private fun WidgetMetricKind.isPercentMetric(): Boolean = when (this) {
|
||||
WidgetMetricKind.Cpu, WidgetMetricKind.Memory, WidgetMetricKind.Disk -> true
|
||||
WidgetMetricKind.RequestRate,
|
||||
WidgetMetricKind.LatencyP95,
|
||||
WidgetMetricKind.Errors5xx,
|
||||
WidgetMetricKind.Connections,
|
||||
-> false
|
||||
}
|
||||
|
||||
private fun withAlpha(color: Int, alpha: Int): Int {
|
||||
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
|
||||
}
|
||||
|
||||
private fun Double?.progressValue(): Int = this?.coerceIn(0.0, 100.0)?.roundToInt() ?: 0
|
||||
private fun Double?.percent(): String = this?.let { "${it.coerceIn(0.0, 999.0).roundToInt()}%" } ?: "--"
|
||||
private fun Double?.ms(): String = this?.let { "${it.roundToInt()}ms" } ?: "--"
|
||||
private fun Double?.number(): String = this?.let {
|
||||
when {
|
||||
it >= 1000.0 -> String.format(Locale.US, "%.1fk", it / 1000.0)
|
||||
it >= 10.0 -> it.roundToInt().toString()
|
||||
else -> String.format(Locale.US, "%.1f", it)
|
||||
}
|
||||
} ?: "--"
|
||||
|
||||
private fun Double?.orZero(): Double = this ?: 0.0
|
||||
|
||||
private fun formatTime(timestampMillis: Long?): String {
|
||||
return timestampMillis?.let { DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(it)) } ?: "--"
|
||||
}
|
||||
|
||||
private data class Counters(
|
||||
val online: Int,
|
||||
val watch: Int,
|
||||
val down: Int,
|
||||
val unknown: Int,
|
||||
val total: Int,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package net.rodakot.ngxhttpmonitoringclient.widget
|
||||
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import net.rodakot.ngxhttpmonitoringclient.data.MonitorRepository
|
||||
|
||||
class ServerWidgetConfigureActivity : BaseWidgetConfigureActivity() {
|
||||
override val mode: WidgetConfigureMode = WidgetConfigureMode.Server
|
||||
}
|
||||
|
||||
class MetricWidgetConfigureActivity : BaseWidgetConfigureActivity() {
|
||||
override val mode: WidgetConfigureMode = WidgetConfigureMode.Metric
|
||||
}
|
||||
|
||||
class GraphWidgetConfigureActivity : BaseWidgetConfigureActivity() {
|
||||
override val mode: WidgetConfigureMode = WidgetConfigureMode.Graph
|
||||
}
|
||||
|
||||
abstract class BaseWidgetConfigureActivity : Activity() {
|
||||
protected abstract val mode: WidgetConfigureMode
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setResult(RESULT_CANCELED)
|
||||
appWidgetId = intent?.extras?.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID,
|
||||
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val repository = MonitorRepository(applicationContext)
|
||||
val servers = repository.servers()
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(32, 32, 32, 32)
|
||||
setBackgroundColor(Color.rgb(14, 17, 22))
|
||||
}
|
||||
root.addView(title(mode.title))
|
||||
root.addView(subtitle(mode.description))
|
||||
|
||||
if (servers.isEmpty()) {
|
||||
root.addView(subtitle("No servers exist yet. Open NGX Monitor and add a server first."))
|
||||
root.addView(actionButton("Close") { finish() })
|
||||
setContentView(root)
|
||||
return
|
||||
}
|
||||
|
||||
val serverGroup = RadioGroup(this).apply {
|
||||
orientation = RadioGroup.VERTICAL
|
||||
servers.forEachIndexed { index, server ->
|
||||
addView(RadioButton(this@BaseWidgetConfigureActivity).apply {
|
||||
id = index + 1
|
||||
text = server.name
|
||||
setTextColor(Color.rgb(234, 240, 247))
|
||||
textSize = 16f
|
||||
setPadding(0, 10, 0, 10)
|
||||
})
|
||||
}
|
||||
check(1)
|
||||
}
|
||||
val scroll = ScrollView(this).apply {
|
||||
addView(serverGroup)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1f,
|
||||
)
|
||||
}
|
||||
root.addView(scroll)
|
||||
|
||||
val metricSpinner = if (mode == WidgetConfigureMode.Metric) {
|
||||
Spinner(this).apply {
|
||||
adapter = ArrayAdapter(
|
||||
this@BaseWidgetConfigureActivity,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
WidgetMetricKind.entries.map { it.label },
|
||||
)
|
||||
}.also { root.addView(it) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
root.addView(actionButton("Add ${mode.buttonLabel}") {
|
||||
val serverIndex = (serverGroup.checkedRadioButtonId - 1).coerceAtLeast(0)
|
||||
val server = servers[serverIndex]
|
||||
when (mode) {
|
||||
WidgetConfigureMode.Server -> {
|
||||
WidgetPreferences.saveServerWidget(this, appWidgetId, server.id)
|
||||
MonitorWidgetUpdater.updateServers(this, intArrayOf(appWidgetId), refreshNetwork = false)
|
||||
}
|
||||
WidgetConfigureMode.Metric -> {
|
||||
val metric = WidgetMetricKind.entries[metricSpinner?.selectedItemPosition ?: 0]
|
||||
WidgetPreferences.saveMetricWidget(this, appWidgetId, server.id, metric)
|
||||
MonitorWidgetUpdater.updateMetrics(this, intArrayOf(appWidgetId), refreshNetwork = false)
|
||||
}
|
||||
WidgetConfigureMode.Graph -> {
|
||||
WidgetPreferences.saveServerWidget(this, appWidgetId, server.id)
|
||||
MonitorWidgetUpdater.updateGraphs(this, intArrayOf(appWidgetId), refreshNetwork = false)
|
||||
}
|
||||
}
|
||||
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
finish()
|
||||
})
|
||||
setContentView(root)
|
||||
}
|
||||
|
||||
private fun title(text: String): TextView = TextView(this).apply {
|
||||
this.text = text
|
||||
setTextColor(Color.rgb(234, 240, 247))
|
||||
textSize = 24f
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setPadding(0, 0, 0, 8)
|
||||
}
|
||||
|
||||
private fun subtitle(text: String): TextView = TextView(this).apply {
|
||||
this.text = text
|
||||
setTextColor(Color.rgb(154, 166, 182))
|
||||
textSize = 14f
|
||||
setPadding(0, 0, 0, 20)
|
||||
}
|
||||
|
||||
private fun actionButton(text: String, onClick: () -> Unit): Button = Button(this).apply {
|
||||
this.text = text
|
||||
gravity = Gravity.CENTER
|
||||
setOnClickListener { onClick() }
|
||||
}
|
||||
}
|
||||
|
||||
enum class WidgetConfigureMode(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val buttonLabel: String,
|
||||
) {
|
||||
Server(
|
||||
title = "Server Bars",
|
||||
description = "Choose one server for a large health state with CPU, memory, and storage bars.",
|
||||
buttonLabel = "server widget",
|
||||
),
|
||||
Metric(
|
||||
title = "Metric Sparkline",
|
||||
description = "Choose one server and one signal for a focused history graph.",
|
||||
buttonLabel = "metric widget",
|
||||
),
|
||||
Graph(
|
||||
title = "Telemetry Graph",
|
||||
description = "Choose one server for CPU, memory, and storage trend lines.",
|
||||
buttonLabel = "graph widget",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.rodakot.ngxhttpmonitoringclient.widget
|
||||
|
||||
import android.content.Context
|
||||
|
||||
enum class WidgetMetricKind(val label: String) {
|
||||
Cpu("CPU"),
|
||||
Memory("Memory"),
|
||||
Disk("Storage"),
|
||||
RequestRate("Requests/s"),
|
||||
LatencyP95("p95 latency"),
|
||||
Errors5xx("5xx errors"),
|
||||
Connections("Connections"),
|
||||
}
|
||||
|
||||
object WidgetPreferences {
|
||||
private const val Name = "ngx_monitor_widgets"
|
||||
|
||||
fun saveServerWidget(context: Context, appWidgetId: Int, serverId: String) {
|
||||
prefs(context).edit().putString(serverKey(appWidgetId), serverId).apply()
|
||||
}
|
||||
|
||||
fun serverId(context: Context, appWidgetId: Int): String? {
|
||||
return prefs(context).getString(serverKey(appWidgetId), null)
|
||||
}
|
||||
|
||||
fun saveMetricWidget(context: Context, appWidgetId: Int, serverId: String, metric: WidgetMetricKind) {
|
||||
prefs(context).edit()
|
||||
.putString(serverKey(appWidgetId), serverId)
|
||||
.putString(metricKey(appWidgetId), metric.name)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun metricKind(context: Context, appWidgetId: Int): WidgetMetricKind {
|
||||
val raw = prefs(context).getString(metricKey(appWidgetId), null)
|
||||
return runCatching { WidgetMetricKind.valueOf(raw ?: "") }.getOrDefault(WidgetMetricKind.Cpu)
|
||||
}
|
||||
|
||||
fun delete(context: Context, appWidgetIds: IntArray) {
|
||||
prefs(context).edit().apply {
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
remove(serverKey(appWidgetId))
|
||||
remove(metricKey(appWidgetId))
|
||||
}
|
||||
}.apply()
|
||||
}
|
||||
|
||||
private fun prefs(context: Context) = context.getSharedPreferences(Name, Context.MODE_PRIVATE)
|
||||
private fun serverKey(appWidgetId: Int) = "server_$appWidgetId"
|
||||
private fun metricKey(appWidgetId: Int) = "metric_$appWidgetId"
|
||||
}
|
||||
27
app/src/main/res/drawable/ic_widget_refresh.xml
Normal file
27
app/src/main/res/drawable/ic_widget_refresh.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M20,12a8,8 0,0 1,-13.7 5.6"
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4,12a8,8 0,0 1,13.7 -5.6"
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M17,3l3,3l-4,1z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M7,21l-3,-3l4,-1z" />
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/widget_background.xml
Normal file
13
app/src/main/res/drawable/widget_background.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="22dp" />
|
||||
<solid android:color="#F0171C23" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#3335D38B" />
|
||||
<padding
|
||||
android:bottom="12dp"
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="12dp" />
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/widget_button.xml
Normal file
8
app/src/main/res/drawable/widget_button.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="999dp" />
|
||||
<solid android:color="#2635D38B" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#6635D38B" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/widget_panel.xml
Normal file
5
app/src/main/res/drawable/widget_panel.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="14dp" />
|
||||
<solid android:color="#FF202733" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/widget_panel_amber.xml
Normal file
5
app/src/main/res/drawable/widget_panel_amber.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="14dp" />
|
||||
<solid android:color="#2CFFB84D" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/widget_panel_blue.xml
Normal file
5
app/src/main/res/drawable/widget_panel_blue.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="14dp" />
|
||||
<solid android:color="#264CC9F0" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/widget_panel_green.xml
Normal file
5
app/src/main/res/drawable/widget_panel_green.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="14dp" />
|
||||
<solid android:color="#2635D38B" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/widget_panel_red.xml
Normal file
5
app/src/main/res/drawable/widget_panel_red.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="14dp" />
|
||||
<solid android:color="#2CFF5A52" />
|
||||
</shape>
|
||||
15
app/src/main/res/drawable/widget_preview_fleet.xml
Normal file
15
app/src/main/res/drawable/widget_preview_fleet.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="360dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="360"
|
||||
android:viewportHeight="180">
|
||||
<path android:fillColor="#FF171C23" android:pathData="M0,0h360v180h-360z" />
|
||||
<path android:fillColor="#FF26313D" android:pathData="M24,24h198v18h-198z" />
|
||||
<path android:fillColor="#FF35D38B" android:pathData="M24,62h34v34h-34zM64,62h34v34h-34zM104,62h34v34h-34zM144,62h34v34h-34zM184,62h34v34h-34zM224,62h34v34h-34z" />
|
||||
<path android:fillColor="#FFFFB84D" android:pathData="M264,62h34v34h-34z" />
|
||||
<path android:fillColor="#FFFF5A52" android:pathData="M304,62h34v34h-34z" />
|
||||
<path android:fillColor="#2635D38B" android:pathData="M24,118h88v34h-88z" />
|
||||
<path android:fillColor="#26FFB84D" android:pathData="M136,118h88v34h-88z" />
|
||||
<path android:fillColor="#26FF5A52" android:pathData="M248,118h88v34h-88z" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/widget_preview_graph.xml
Normal file
30
app/src/main/res/drawable/widget_preview_graph.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="360dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="360"
|
||||
android:viewportHeight="180">
|
||||
<path android:fillColor="#FF171C23" android:pathData="M0,0h360v180h-360z" />
|
||||
<path android:fillColor="#FF27313D" android:pathData="M24,48h312v2h-312zM24,84h312v2h-312zM24,120h312v2h-312zM24,156h312v2h-312z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M24,138 C72,118 96,130 132,96 C172,58 214,82 248,54 C284,24 304,46 336,32"
|
||||
android:strokeColor="#FF35D38B"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="6" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M24,118 C72,98 102,112 142,84 C182,60 212,92 250,76 C292,58 310,82 336,68"
|
||||
android:strokeColor="#FF4CC9F0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="6" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M24,152 C66,138 98,144 136,126 C178,108 210,116 250,98 C288,82 312,104 336,92"
|
||||
android:strokeColor="#FFFFB84D"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="6" />
|
||||
</vector>
|
||||
14
app/src/main/res/drawable/widget_preview_incidents.xml
Normal file
14
app/src/main/res/drawable/widget_preview_incidents.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="360dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="360"
|
||||
android:viewportHeight="180">
|
||||
<path android:fillColor="#FF171C23" android:pathData="M0,0h360v180h-360z" />
|
||||
<path android:fillColor="#26FF5A52" android:pathData="M24,48h82v104h-82z" />
|
||||
<path android:fillColor="#FFFF5A52" android:pathData="M46,72h38v50h-38z" />
|
||||
<path android:fillColor="#FF202733" android:pathData="M126,48h210v28h-210zM126,88h210v28h-210zM126,128h210v28h-210z" />
|
||||
<path android:fillColor="#FFFF5A52" android:pathData="M138,58h22v8h-22z" />
|
||||
<path android:fillColor="#FFFFB84D" android:pathData="M138,98h22v8h-22z" />
|
||||
<path android:fillColor="#FF35D38B" android:pathData="M138,138h22v8h-22z" />
|
||||
</vector>
|
||||
18
app/src/main/res/drawable/widget_preview_metric.xml
Normal file
18
app/src/main/res/drawable/widget_preview_metric.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="360dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="360"
|
||||
android:viewportHeight="180">
|
||||
<path android:fillColor="#FF171C23" android:pathData="M0,0h360v180h-360z" />
|
||||
<path android:fillColor="#FF4CC9F0" android:pathData="M24,28h96v46h-96z" />
|
||||
<path android:fillColor="#FF27313D" android:pathData="M24,102h312v2h-312zM24,132h312v2h-312z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M24,142 C64,96 92,128 130,88 C166,50 196,82 226,64 C264,40 292,74 336,36"
|
||||
android:strokeColor="#FF4CC9F0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="8" />
|
||||
<path android:fillColor="#2635D38B" android:pathData="M300,24h36v36h-36z" />
|
||||
</vector>
|
||||
14
app/src/main/res/drawable/widget_preview_server.xml
Normal file
14
app/src/main/res/drawable/widget_preview_server.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="360dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="360"
|
||||
android:viewportHeight="180">
|
||||
<path android:fillColor="#FF171C23" android:pathData="M0,0h360v180h-360z" />
|
||||
<path android:fillColor="#FF35D38B" android:pathData="M24,24h150v24h-150z" />
|
||||
<path android:fillColor="#FF27313D" android:pathData="M24,68h250v18h-250zM24,100h250v18h-250zM24,132h250v18h-250z" />
|
||||
<path android:fillColor="#FF35D38B" android:pathData="M24,68h176v18h-176z" />
|
||||
<path android:fillColor="#FF4CC9F0" android:pathData="M24,100h214v18h-214z" />
|
||||
<path android:fillColor="#FFFFB84D" android:pathData="M24,132h142v18h-142z" />
|
||||
<path android:fillColor="#2635D38B" android:pathData="M300,24h36v36h-36z" />
|
||||
</vector>
|
||||
17
app/src/main/res/drawable/widget_progress_amber.xml
Normal file
17
app/src/main/res/drawable/widget_progress_amber.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="999dp" />
|
||||
<solid android:color="#FF27313D" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="999dp" />
|
||||
<solid android:color="#FFFFB84D" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
||||
17
app/src/main/res/drawable/widget_progress_cyan.xml
Normal file
17
app/src/main/res/drawable/widget_progress_cyan.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="999dp" />
|
||||
<solid android:color="#FF27313D" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="999dp" />
|
||||
<solid android:color="#FF4CC9F0" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
||||
17
app/src/main/res/drawable/widget_progress_green.xml
Normal file
17
app/src/main/res/drawable/widget_progress_green.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="999dp" />
|
||||
<solid android:color="#FF27313D" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="999dp" />
|
||||
<solid android:color="#FF35D38B" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
||||
129
app/src/main/res/layout/widget_fleet.xml
Normal file
129
app/src/main/res/layout/widget_fleet.xml
Normal file
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Fleet Pulse"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Waiting for samples"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_refresh"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/widget_button"
|
||||
android:contentDescription="@string/widget_refresh"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/ic_widget_refresh"
|
||||
android:tint="#FF35D38B" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="All clear"
|
||||
android:textColor="#FF35D38B"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_graph"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="42dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/widget_fleet_graph"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_online"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel_green"
|
||||
android:gravity="center"
|
||||
android:text="0 OK"
|
||||
android:textColor="#FF35D38B"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_watch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel_amber"
|
||||
android:gravity="center"
|
||||
android:text="0 WATCH"
|
||||
android:textColor="#FFFFB84D"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_down"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel_red"
|
||||
android:gravity="center"
|
||||
android:text="0 DOWN"
|
||||
android:textColor="#FFFF5A52"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_priority"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="No priority servers"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
107
app/src/main/res/layout/widget_graph.xml
Normal file
107
app/src/main/res/layout/widget_graph.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Telemetry Flow"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="CPU / MEM / STORAGE"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_refresh"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/widget_button"
|
||||
android:contentDescription="@string/widget_refresh"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/ic_widget_refresh"
|
||||
android:tint="#FF35D38B" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_graph"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/widget_graph_chart"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_cpu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel_green"
|
||||
android:gravity="center"
|
||||
android:text="CPU"
|
||||
android:textColor="#FF35D38B"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_memory"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel_blue"
|
||||
android:gravity="center"
|
||||
android:text="MEM"
|
||||
android:textColor="#FF4CC9F0"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_disk"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel_amber"
|
||||
android:gravity="center"
|
||||
android:text="STORAGE"
|
||||
android:textColor="#FFFFB84D"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
145
app/src/main/res/layout/widget_incidents.xml
Normal file
145
app/src/main/res/layout/widget_incidents.xml
Normal file
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Incident Watch"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Highest priority first"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_refresh"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/widget_button"
|
||||
android:contentDescription="@string/widget_refresh"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/ic_widget_refresh"
|
||||
android:tint="#FF35D38B" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="9dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="86dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="@drawable/widget_panel"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_issue_count"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="0"
|
||||
android:textColor="#FFFF5A52"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_issue_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="ISSUES"
|
||||
android:textColor="#FFFFB5B1"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_issue_one"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:paddingLeft="9dp"
|
||||
android:paddingRight="9dp"
|
||||
android:text="No active incidents"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_issue_two"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:paddingLeft="9dp"
|
||||
android:paddingRight="9dp"
|
||||
android:text="Routes are healthy"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_issue_three"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:paddingLeft="9dp"
|
||||
android:paddingRight="9dp"
|
||||
android:text="Last update --"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
70
app/src/main/res/layout/widget_metric.xml
Normal file
70
app/src/main/res/layout/widget_metric.xml
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Metric"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_refresh"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/widget_button"
|
||||
android:contentDescription="@string/widget_refresh"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/ic_widget_refresh"
|
||||
android:tint="#FF35D38B" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start"
|
||||
android:maxLines="1"
|
||||
android:text="--"
|
||||
android:textColor="#FF4CC9F0"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_graph"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/widget_metric_graph"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="Waiting"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
227
app/src/main/res/layout/widget_server.xml
Normal file
227
app/src/main/res/layout/widget_server.xml
Normal file
@@ -0,0 +1,227 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Server"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Waiting"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_refresh"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/widget_button"
|
||||
android:contentDescription="@string/widget_refresh"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/ic_widget_refresh"
|
||||
android:tint="#FF35D38B" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_health"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="9dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="ONLINE"
|
||||
android:textColor="#FF35D38B"
|
||||
android:textSize="25sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_cpu_label"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="22dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="CPU"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/widget_cpu_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_weight="1"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressDrawable="@drawable/widget_progress_green" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_cpu_value"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="22dp"
|
||||
android:gravity="end|center_vertical"
|
||||
android:text="--"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_memory_label"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="22dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="MEM"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/widget_memory_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_weight="1"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressDrawable="@drawable/widget_progress_cyan" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_memory_value"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="22dp"
|
||||
android:gravity="end|center_vertical"
|
||||
android:text="--"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_disk_label"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="22dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="STORAGE"
|
||||
android:textColor="#FF9AA6B6"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/widget_disk_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_weight="1"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressDrawable="@drawable/widget_progress_amber" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_disk_value"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="22dp"
|
||||
android:gravity="end|center_vertical"
|
||||
android:text="--"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_rps"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel"
|
||||
android:gravity="center"
|
||||
android:text="RPS --"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_p95"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel"
|
||||
android:gravity="center"
|
||||
android:text="P95 --"
|
||||
android:textColor="#FFEAF0F7"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_5xx"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_panel_red"
|
||||
android:gravity="center"
|
||||
android:text="5XX --"
|
||||
android:textColor="#FFFF5A52"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,3 +1,17 @@
|
||||
<resources>
|
||||
<string name="app_name">NGX Monitor</string>
|
||||
<string name="widget_fleet_label">NGX Fleet Pulse</string>
|
||||
<string name="widget_server_label">NGX Server Bars</string>
|
||||
<string name="widget_metric_label">NGX Metric Sparkline</string>
|
||||
<string name="widget_graph_label">NGX Telemetry Graph</string>
|
||||
<string name="widget_incidents_label">NGX Incident Watch</string>
|
||||
<string name="widget_fleet_description">Visual all-server status strip with online, watch, and down groups.</string>
|
||||
<string name="widget_server_description">One selected server with large health state and CPU, memory, and storage bars.</string>
|
||||
<string name="widget_metric_description">One focused metric with a large value and history sparkline.</string>
|
||||
<string name="widget_graph_description">One selected server with CPU, memory, and storage trend lines.</string>
|
||||
<string name="widget_incidents_description">Fleet incident queue with the highest-priority unreachable or degraded servers.</string>
|
||||
<string name="widget_refresh">Refresh widget</string>
|
||||
<string name="widget_fleet_graph">Fleet status strip</string>
|
||||
<string name="widget_metric_graph">Metric history graph</string>
|
||||
<string name="widget_graph_chart">Telemetry trend graph</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,4 +15,15 @@
|
||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||
<item name="android:windowDisablePreview">false</item>
|
||||
</style>
|
||||
|
||||
<style name="WidgetMetricCell">
|
||||
<item name="android:layout_width">0dp</item>
|
||||
<item name="android:layout_height">match_parent</item>
|
||||
<item name="android:layout_weight">1</item>
|
||||
<item name="android:background">@drawable/widget_panel</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:textColor">#FFEAF0F7</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
12
app/src/main/res/xml/widget_fleet_info.xml
Normal file
12
app/src/main/res/xml/widget_fleet_info.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_fleet_description"
|
||||
android:initialLayout="@layout/widget_fleet"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="140dp"
|
||||
android:previewImage="@drawable/widget_preview_fleet"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="2"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
13
app/src/main/res/xml/widget_graph_info.xml
Normal file
13
app/src/main/res/xml/widget_graph_info.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:configure="net.rodakot.ngxhttpmonitoringclient.widget.GraphWidgetConfigureActivity"
|
||||
android:description="@string/widget_graph_description"
|
||||
android:initialLayout="@layout/widget_graph"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="140dp"
|
||||
android:previewImage="@drawable/widget_preview_graph"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="2"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
12
app/src/main/res/xml/widget_incidents_info.xml
Normal file
12
app/src/main/res/xml/widget_incidents_info.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_incidents_description"
|
||||
android:initialLayout="@layout/widget_incidents"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="140dp"
|
||||
android:previewImage="@drawable/widget_preview_incidents"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="2"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
13
app/src/main/res/xml/widget_metric_info.xml
Normal file
13
app/src/main/res/xml/widget_metric_info.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:configure="net.rodakot.ngxhttpmonitoringclient.widget.MetricWidgetConfigureActivity"
|
||||
android:description="@string/widget_metric_description"
|
||||
android:initialLayout="@layout/widget_metric"
|
||||
android:minWidth="120dp"
|
||||
android:minHeight="120dp"
|
||||
android:previewImage="@drawable/widget_preview_metric"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:targetCellWidth="2"
|
||||
android:targetCellHeight="2"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
13
app/src/main/res/xml/widget_server_info.xml
Normal file
13
app/src/main/res/xml/widget_server_info.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:configure="net.rodakot.ngxhttpmonitoringclient.widget.ServerWidgetConfigureActivity"
|
||||
android:description="@string/widget_server_description"
|
||||
android:initialLayout="@layout/widget_server"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="140dp"
|
||||
android:previewImage="@drawable/widget_preview_server"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="2"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
Reference in New Issue
Block a user