Skip to content

Commit 58c5d33

Browse files
smalnuitsacclaude
andcommitted
feat(offline): offline hardened build flavor + dual-APK release
Add an `offline` product flavor alongside `online` so both APKs can be published in the same GitHub Release and installed side-by-side on the car. - offline flavor: applicationId suffix `.offline`, BuildConfig.OFFLINE flag - offline manifest overlay strips INTERNET / ACCESS_NETWORK_STATE / INSTALL_PACKAGES perms and the silent-install receiver (tools:node=remove) - self-update UI hidden in offline (MainActivity + SettingsFragment guards) - platform signing read from env/gradle props (no hardcoded passwords) - per-flavor release APK naming: MG4Control-<flavor>-<version>.apk - release.yml: build online+offline release APKs, attach both to the Release Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7c45160 commit 58c5d33

7 files changed

Lines changed: 150 additions & 4 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Release
2+
3+
# Build les deux variantes (online + offline) et les publie dans la même Release.
4+
# L'utilisateur choisit l'APK à installer depuis la page Releases GitHub.
5+
on:
6+
push:
7+
tags:
8+
- "v*"
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-java@v4
21+
with:
22+
distribution: temurin
23+
java-version: "17"
24+
25+
- name: Setup Gradle
26+
uses: gradle/actions/setup-gradle@v4
27+
28+
- name: Decode platform keystore
29+
if: ${{ secrets.MG4_KEYSTORE_BASE64 != '' }}
30+
env:
31+
MG4_KEYSTORE_BASE64: ${{ secrets.MG4_KEYSTORE_BASE64 }}
32+
run: |
33+
echo "$MG4_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/platform.keystore"
34+
echo "MG4_KEYSTORE=$RUNNER_TEMP/platform.keystore" >> "$GITHUB_ENV"
35+
36+
- name: Build online + offline release APKs
37+
env:
38+
MG4_KEYSTORE_PASSWORD: ${{ secrets.MG4_KEYSTORE_PASSWORD }}
39+
MG4_KEY_ALIAS: ${{ secrets.MG4_KEY_ALIAS }}
40+
MG4_KEY_PASSWORD: ${{ secrets.MG4_KEY_PASSWORD }}
41+
run: ./gradlew assembleOnlineRelease assembleOfflineRelease --no-daemon
42+
43+
- name: Collect APKs
44+
run: |
45+
mkdir -p dist
46+
find app/build/outputs/apk -name "MG4Control-*-*.apk" -exec cp {} dist/ \;
47+
ls -la dist/
48+
49+
- name: Upload APKs as workflow artifacts
50+
uses: actions/upload-artifact@v4
51+
with:
52+
name: mg4control-apks
53+
path: dist/*.apk
54+
55+
- name: Publish to GitHub Release
56+
if: startsWith(github.ref, 'refs/tags/')
57+
uses: softprops/action-gh-release@v2
58+
with:
59+
files: dist/*.apk
60+
body: |
61+
## MG4Control ${{ github.ref_name }}
62+
63+
Deux variantes sont fournies — **choisissez-en une** :
64+
65+
| Variante | Fichier | Réseau | Auto-update | Cible |
66+
|---|---|---|---|---|
67+
| **Online** | `MG4Control-online-*.apk` | oui | oui | usage standard |
68+
| **Offline** 🔒 | `MG4Control-offline-*.apk` | aucun (perms réseau retirées) | non | déploiement durci sur véhicule |
69+
70+
> L'APK offline a un applicationId distinct (`com.mg4.control.offline`) et peut cohabiter avec l'online.

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ local.properties
99
# mokup
1010
mockup/
1111

12+
# Analyse interne (non commitée) — docs de travail générés
13+
analysis/DIFF_2.7.0.md
14+
analysis/DIFF_2.7.0.full.diff
15+
analysis/SOH_feasibility.md
16+
analysis/FEATURE_BACKLOG.md
17+
1218
# Claude Code (config locale IDE)
1319
.claude/
1420

‎app/build.gradle.kts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,42 @@ android {
1515
versionName = "2.6.2"
1616
}
1717

18+
// Signature avec la clé plateforme de la ROM (requise par sharedUserId=android.uid.system).
19+
// Secrets lus depuis l'environnement (CI) ou gradle.properties local — JAMAIS commités.
20+
val keystorePath = System.getenv("MG4_KEYSTORE") ?: (project.findProperty("mg4.keystore") as String?)
21+
signingConfigs {
22+
if (keystorePath != null && file(keystorePath).exists()) {
23+
create("platform") {
24+
storeFile = file(keystorePath)
25+
storePassword = System.getenv("MG4_KEYSTORE_PASSWORD") ?: (project.findProperty("mg4.keystore.password") as String?)
26+
keyAlias = System.getenv("MG4_KEY_ALIAS") ?: (project.findProperty("mg4.key.alias") as String?) ?: "platform"
27+
keyPassword = System.getenv("MG4_KEY_PASSWORD") ?: (project.findProperty("mg4.key.password") as String?)
28+
}
29+
}
30+
}
31+
32+
flavorDimensions += "dist"
33+
productFlavors {
34+
create("online") {
35+
dimension = "dist"
36+
buildConfigField("boolean", "OFFLINE", "false")
37+
}
38+
create("offline") {
39+
dimension = "dist"
40+
applicationIdSuffix = ".offline"
41+
versionNameSuffix = "-offline"
42+
buildConfigField("boolean", "OFFLINE", "true")
43+
}
44+
}
45+
1846
buildTypes {
1947
release {
2048
isMinifyEnabled = false
2149
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
50+
signingConfigs.findByName("platform")?.let { signingConfig = it }
51+
}
52+
debug {
53+
signingConfigs.findByName("platform")?.let { signingConfig = it }
2254
}
2355
}
2456

@@ -33,6 +65,17 @@ android {
3365

3466
buildFeatures {
3567
viewBinding = true
68+
buildConfig = true
69+
}
70+
71+
applicationVariants.all {
72+
val variant = this
73+
outputs.all {
74+
val output = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
75+
if (variant.buildType.name == "release") {
76+
output.outputFileName = "MG4Control-${variant.flavorName}-${variant.versionName}.apk"
77+
}
78+
}
3679
}
3780
}
3881

‎app/src/main/java/com/mg4/control/MainActivity.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class MainActivity : AppCompatActivity() {
9696
// ── Vérification de mise à jour au démarrage ──────────────────────────────
9797

9898
private fun checkForUpdates() {
99+
if (BuildConfig.OFFLINE) return // build offline : aucune vérif réseau
99100
val prefs = getSharedPreferences("mg4_settings", android.content.Context.MODE_PRIVATE)
100101
if (!prefs.getBoolean("auto_check_update", true)) return
101102

‎app/src/main/java/com/mg4/control/ui/SettingsFragment.kt‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.Context
77
import android.content.Intent
88
import android.content.res.ColorStateList
99
import androidx.appcompat.app.AppCompatDelegate
10+
import com.mg4.control.BuildConfig
1011
import com.mg4.control.MainActivity
1112
import com.mg4.control.util.ThemeHelper
1213
import android.graphics.Bitmap
@@ -160,10 +161,16 @@ class SettingsFragment : Fragment() {
160161
}
161162

162163
// ── Vérification auto des mises à jour ───────────────────────────────
163-
val switchAutoUpdate = view.findViewById<Switch>(R.id.switch_auto_update)
164-
switchAutoUpdate.isChecked = prefs.getBoolean("auto_check_update", true)
165-
switchAutoUpdate.setOnCheckedChangeListener { _, checked ->
166-
prefs.edit().putBoolean("auto_check_update", checked).apply()
164+
// Build offline : pas de réseau → on masque toute l'UI de mise à jour.
165+
if (BuildConfig.OFFLINE) {
166+
view.findViewById<View>(R.id.row_auto_update).visibility = View.GONE
167+
view.findViewById<View>(R.id.row_update_buttons).visibility = View.GONE
168+
} else {
169+
val switchAutoUpdate = view.findViewById<Switch>(R.id.switch_auto_update)
170+
switchAutoUpdate.isChecked = prefs.getBoolean("auto_check_update", true)
171+
switchAutoUpdate.setOnCheckedChangeListener { _, checked ->
172+
prefs.edit().putBoolean("auto_check_update", checked).apply()
173+
}
167174
}
168175

169176
// ── Alimentation véhicule (SWI133) — éteint la voiture, garde l'écran ──

‎app/src/main/res/layout/fragment_settings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@
378378
android:background="@color/dash_border" />
379379

380380
<LinearLayout
381+
android:id="@+id/row_auto_update"
381382
android:layout_width="match_parent"
382383
android:layout_height="wrap_content"
383384
android:orientation="horizontal"
@@ -444,6 +445,7 @@
444445

445446
<!-- ── Boutons MAJ (2 colonnes : Vérifier | Nettoyer) ──────────── -->
446447
<LinearLayout
448+
android:id="@+id/row_update_buttons"
447449
android:layout_width="match_parent"
448450
android:layout_height="44dp"
449451
android:orientation="horizontal"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Offline flavor overlay.
4+
Durcissement : retire toute capacité réseau et le canal d'installation silencieuse
5+
du manifeste fusionné. L'APK offline ne peut ni télécharger ni installer de paquet.
6+
-->
7+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
8+
xmlns:tools="http://schemas.android.com/tools">
9+
10+
<uses-permission android:name="android.permission.INTERNET" tools:node="remove" />
11+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove" />
12+
<uses-permission android:name="android.permission.INSTALL_PACKAGES" tools:node="remove" />
13+
14+
<application>
15+
<receiver android:name=".update.InstallReceiver" tools:node="remove" />
16+
</application>
17+
</manifest>

0 commit comments

Comments
 (0)