Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Test build

# Build de test signé par la clé plateforme, SANS publication dans les Releases.
# Les APK sortent en artifacts du run (onglet Actions → run → section Artifacts).
# L'OTA interroge /releases/latest, qui ignore artifacts, drafts et prereleases :
# ce workflow ne peut donc PAS perturber les mises à jour des utilisateurs.
on:
workflow_dispatch:
inputs:
variant:
description: "Variante à construire"
type: choice
options: [both, online, offline]
default: both
build_type:
description: "Type de build"
type: choice
options: [release, debug]
default: release

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
env:
MG4_KEYSTORE_BASE64: ${{ secrets.MG4_KEYSTORE_BASE64 }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Decode platform keystore
if: ${{ env.MG4_KEYSTORE_BASE64 != '' }}
run: |
printf '%s' "$MG4_KEYSTORE_BASE64" | tr -d '[:space:]' | base64 -d > "$RUNNER_TEMP/platform.keystore"
if [ "$(head -c1 "$RUNNER_TEMP/platform.keystore" | od -An -tx1 | tr -d ' ')" != "30" ]; then
echo "::error::MG4_KEYSTORE_BASE64 est invalide. Recréer le secret avec : base64 -w0 platform.keystore"
exit 1
fi
echo "MG4_KEYSTORE=$RUNNER_TEMP/platform.keystore" >> "$GITHUB_ENV"

- name: Build APK(s)
env:
MG4_KEYSTORE_PASSWORD: ${{ secrets.MG4_KEYSTORE_PASSWORD }}
MG4_KEY_ALIAS: ${{ secrets.MG4_KEY_ALIAS }}
MG4_KEY_PASSWORD: ${{ secrets.MG4_KEY_PASSWORD }}
run: |
chmod +x gradlew
# "release" → assembleOnlineRelease ; "debug" → assembleOnlineDebug
case "${{ inputs.build_type }}" in
release) SUFFIX="Release" ;;
debug) SUFFIX="Debug" ;;
esac
case "${{ inputs.variant }}" in
both) TASKS="assembleOnline$SUFFIX assembleOffline$SUFFIX" ;;
online) TASKS="assembleOnline$SUFFIX" ;;
offline) TASKS="assembleOffline$SUFFIX" ;;
esac
echo "Tâches : $TASKS"
./gradlew $TASKS --no-daemon

- name: Collect APKs
run: |
mkdir -p dist
find app/build/outputs/apk -name "*.apk" -exec cp {} dist/ \;
ls -la dist/

# Affiche les schémas de signature réellement posés (v1 / v2 / v3) et l'empreinte du
# certificat. Permet de contrôler ce que produit VRAIMENT la CI, sans installer l'APK.
- name: Signature report
run: |
APKSIGNER=$(ls "$ANDROID_SDK_ROOT"/build-tools/*/apksigner | sort -V | tail -1)
echo "apksigner : $APKSIGNER"
for f in dist/*.apk; do
echo "── $(basename "$f") ──"
"$APKSIGNER" verify --print-certs -v "$f" 2>&1 \
| grep -E 'Verified using v[0-9]|certificate SHA-256 digest' || echo " (non vérifiable)"
echo
done

- name: Upload APKs (artifacts, PAS une release)
uses: actions/upload-artifact@v4
with:
name: mg4control-test-${{ inputs.variant }}-${{ inputs.build_type }}-${{ github.run_number }}
path: dist/*.apk
retention-days: 14
if-no-files-found: error
19 changes: 8 additions & 11 deletions app/src/main/java/com/mg4/control/update/ApkInstaller.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,14 @@ object ApkInstaller {
return "APK manquant ou vide : ${apkFile.absolutePath}"
}

// [DÉSACTIVÉ 2026-08-05] Contrôle de signature (T-901) mis en pause : il refusait
// TOUTE mise à jour OTA sur AAOS 9, y compris avec un APK signé de la même clé
// plateforme en v1+v2+v3. Cause non encore élucidée (getPackageArchiveInfo ne
// remonte probablement pas la signature de l'archive sur cette ROM).
// Garde-fous restants : Android refuse une MAJ signée d'une autre clé, et
// ApkUrlPolicy restreint toujours l'origine du téléchargement (github/gitlab, https).
// Pour réactiver : décommenter le bloc ci-dessous.
// if (!ApkSignatureVerifier.matchesRunningApp(context, apkFile)) {
// apkFile.runCatching { delete() }
// return "Signature non conforme — APK rejeté et supprimé"
// }
// [RÉACTIVÉ 2026-08-05] Contrôle de signature (T-901). Les échecs précédents ont eu
// lieu avec une app installée signée à la main (v1 seul) face à un APK CI (v2/v3) :
// on retente avec les DEUX côtés produits par la CI, donc signés à l'identique.
// L'app tourne en uid.system : on n'installe que ce qui est signé par notre clé.
if (!ApkSignatureVerifier.matchesRunningApp(context, apkFile)) {
apkFile.runCatching { delete() }
return "Signature non conforme — APK rejeté et supprimé"
}

// ── Stratégie 1 : pm install depuis le stockage externe ───────────────
val extDir = context.getExternalFilesDir(null)
Expand Down
39 changes: 26 additions & 13 deletions app/src/main/java/com/mg4/control/update/UpdateDialogManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@
.substringBefore('?')
.ifBlank { "MG4Control-${info.versionName}.apk" }

// DownloadManager n'écrase JAMAIS un fichier existant : si "…-2.6.5.apk" est déjà là
// (téléchargement précédent de la même version), il écrit "…-2.6.5-1.apk". Or le code
// relit ensuite le nom d'origine → il vérifierait/installerait l'ANCIEN fichier.
// On supprime donc la cible avant d'enfiler la requête, pour garantir que le fichier
// téléchargé, le fichier vérifié et le fichier installé soient bien le même.
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
if (target.exists()) {
val deleted = target.runCatching { delete() }.getOrDefault(false)
Log.i(TAG, "Ancien APK homonyme trouvé (${target.name}) → suppression=$deleted")
}

val request = DownloadManager.Request(Uri.parse(info.apkUrl)).apply {
setTitle("MG4Control ${info.versionName}")
setDescription(activity.getString(R.string.update_downloading))
Expand Down Expand Up @@ -210,19 +224,18 @@
when (status) {
DownloadManager.STATUS_SUCCESSFUL -> {
progressBar.progress = 100
// [DÉSACTIVÉ 2026-08-05] Contrôle de signature (T-901) mis en pause :
// il refusait TOUTE mise à jour OTA sur AAOS 9, même avec un APK
// signé de la même clé plateforme en v1+v2+v3. Voir ApkInstaller.
// Pour réactiver : décommenter le bloc ci-dessous.
// val downloaded = File(
// Environment.getExternalStoragePublicDirectory(
// Environment.DIRECTORY_DOWNLOADS), fileName)
// if (!ApkSignatureVerifier.matchesRunningApp(activity, downloaded)) {
// downloaded.runCatching { delete() }
// tvStatus.setText(R.string.update_error_signature)
// btnCancel.setText(R.string.update_close)
// break
// }
// [RÉACTIVÉ 2026-08-05] Contrôle de signature (T-901) — voir ApkInstaller.
// L'APK est dans un dossier public : on vérifie qu'il est signé
// par NOTRE clé avant d'inviter l'utilisateur à l'installer.
val downloaded = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), fileName)
if (!ApkSignatureVerifier.matchesRunningApp(activity, downloaded)) {
downloaded.runCatching { delete() }
tvStatus.setText(R.string.update_error_signature)
btnCancel.setText(R.string.update_close)
break
}
// Nettoie les anciens APK dans Téléchargements (garde les 5 plus récents)
ApkCleanup.cleanIfNeeded()
// Ouvre le dossier Téléchargements dans le gestionnaire AAOS
Expand Down
Loading