Implemented the Play Store readiness pass.

This commit is contained in:
2026-05-11 19:06:10 +03:30
parent 33fa8744e1
commit ad05e9da35
17 changed files with 312 additions and 46 deletions

View File

@@ -1,8 +1,32 @@
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
}
val localProperties = Properties().apply {
val file = rootProject.file("local.properties")
if (file.isFile) {
file.inputStream().use { load(it) }
}
}
fun releaseSecret(name: String): String? {
return providers.environmentVariable(name).orNull ?: localProperties.getProperty(name)
}
val releaseStoreFile = releaseSecret("NGX_RELEASE_STORE_FILE")
val releaseStorePassword = releaseSecret("NGX_RELEASE_STORE_PASSWORD")
val releaseKeyAlias = releaseSecret("NGX_RELEASE_KEY_ALIAS")
val releaseKeyPassword = releaseSecret("NGX_RELEASE_KEY_PASSWORD")
val hasReleaseSigning = listOf(
releaseStoreFile,
releaseStorePassword,
releaseKeyAlias,
releaseKeyPassword,
).all { !it.isNullOrBlank() }
android {
namespace = "net.rodakot.ngxhttpmonitoringclient"
compileSdk {
@@ -15,15 +39,30 @@ android {
applicationId = "net.rodakot.ngxhttpmonitoringclient"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionCode = (providers.gradleProperty("VERSION_CODE").orNull ?: "1").toInt()
versionName = providers.gradleProperty("VERSION_NAME").orNull ?: "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
if (hasReleaseSigning) {
create("release") {
storeFile = file(releaseStoreFile!!)
storePassword = releaseStorePassword
keyAlias = releaseKeyAlias
keyPassword = releaseKeyPassword
}
}
}
buildTypes {
release {
isMinifyEnabled = false
isMinifyEnabled = true
isShrinkResources = true
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"

View File

@@ -8,7 +8,8 @@
<application
android:name=".NgxMonitorApplication"
android:allowBackup="true"
android:allowBackup="false"
android:appCategory="productivity"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"

View File

@@ -587,17 +587,27 @@ private fun HistoryCockpit(history: List<MetricSummary>) {
@Composable
private fun SettingsCockpit(server: ServerProfile, controller: MonitorController) {
DataPanel("Alert Rules") {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
RuleRow("CPU limit", "${server.alertOverrides.cpuPercent ?: DefaultCpuThreshold}%")
RuleRow("Memory limit", "${server.alertOverrides.memoryPercent ?: DefaultMemoryThreshold}%")
RuleRow("Disk limit", "${server.alertOverrides.diskPercent ?: DefaultDiskThreshold}%")
RuleRow("p95 latency", "${(server.alertOverrides.latencyP95Millis ?: DefaultLatencyP95ThresholdMs).toInt()} ms")
RuleRow("5xx limit", "${server.alertOverrides.errors5xx ?: DefaultErrors5xxThreshold}")
RuleRow("Fallback IPs", server.fallbackIpAddresses.ifEmpty { listOf("None") }.joinToString(", "))
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Button(onClick = controller::showEditSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Edit") }
OutlinedButton(onClick = controller::deleteSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Delete") }
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
DataPanel("Alert Rules") {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
RuleRow("CPU limit", "${server.alertOverrides.cpuPercent ?: DefaultCpuThreshold}%")
RuleRow("Memory limit", "${server.alertOverrides.memoryPercent ?: DefaultMemoryThreshold}%")
RuleRow("Disk limit", "${server.alertOverrides.diskPercent ?: DefaultDiskThreshold}%")
RuleRow("p95 latency", "${(server.alertOverrides.latencyP95Millis ?: DefaultLatencyP95ThresholdMs).toInt()} ms")
RuleRow("5xx limit", "${server.alertOverrides.errors5xx ?: DefaultErrors5xxThreshold}")
RuleRow("Fallback IPs", server.fallbackIpAddresses.ifEmpty { listOf("None") }.joinToString(", "))
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Button(onClick = controller::showEditSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Edit") }
OutlinedButton(onClick = controller::deleteSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Delete") }
}
}
}
DataPanel("Privacy") {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
RuleRow("Storage", "Server profiles, credentials, samples, alerts, and widget choices stay on this device.")
RuleRow("Credentials", "Monitor tokens and Basic Auth passwords are encrypted with Android Keystore.")
RuleRow("Network", "The app connects only to monitor endpoints configured by the user.")
RuleRow("Backups", "Android cloud backup and device transfer are disabled for app data.")
}
}
}

View File

@@ -6,6 +6,7 @@ import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
@@ -66,14 +67,14 @@ abstract class BaseWidgetConfigureActivity : Activity() {
orientation = RadioGroup.VERTICAL
servers.forEachIndexed { index, server ->
addView(RadioButton(this@BaseWidgetConfigureActivity).apply {
id = index + 1
id = View.generateViewId()
text = server.name
setTextColor(Color.rgb(234, 240, 247))
textSize = 16f
setPadding(0, 10, 0, 10)
})
}
check(1)
check(getChildAt(0).id)
}
val scroll = ScrollView(this).apply {
addView(serverGroup)
@@ -98,7 +99,8 @@ abstract class BaseWidgetConfigureActivity : Activity() {
}
root.addView(actionButton("Add ${mode.buttonLabel}") {
val serverIndex = (serverGroup.checkedRadioButtonId - 1).coerceAtLeast(0)
val checkedView = serverGroup.findViewById<RadioButton>(serverGroup.checkedRadioButtonId)
val serverIndex = serverGroup.indexOfChild(checkedView).coerceAtLeast(0)
val server = servers[serverIndex]
when (mode) {
WidgetConfigureMode.Server -> {

View File

@@ -5,7 +5,6 @@
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:windowBackground">@color/command_black</item>
<item name="android:fontFamily">sans</item>
<item name="android:colorAccent">@color/signal_green</item>

View File

@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
<exclude
domain="database"
path="." />
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
</full-backup-content>

View File

@@ -1,19 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
<exclude
domain="database"
path="." />
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
<exclude
domain="database"
path="." />
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
</device-transfer>
-->
</data-extraction-rules>
</data-extraction-rules>