fix(train): nothing of Train's goes to Android's backup or to another phone (ADR 0030, D53)

What Train keeps unsealed on the phone (a recording while it runs, the Loop's days before they are sealed, the body's figures the band was set up with, the maps along routes) went to the backup and to a new phone by default, outside the sealing. allowBackup and fullBackupContent are off, and from Android 12 on the extraction rules exclude every folder from both. The second WRITE_EXERCISE goes: a ride's cadence is written under the first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-30 12:55:59 +01:00
commit 071899ce44
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 112 additions and 3 deletions

View file

@ -38,8 +38,8 @@
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
<uses-permission android:name="android.permission.health.WRITE_SPEED"/>
<uses-permission android:name="android.permission.health.WRITE_POWER"/>
<!-- A ride's and a run's cadence (ADR 0030, D51), asked for with writing. -->
<uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
<!-- A run's cadence (ADR 0030, D51), asked for with writing; a ride's is
written under WRITE_EXERCISE above. -->
<uses-permission android:name="android.permission.health.WRITE_STEPS"/>
<uses-permission android:name="android.permission.health.WRITE_DISTANCE"/>
<uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED"/>
@ -51,10 +51,17 @@
<uses-feature android:name="android.hardware.location.gps" android:required="false"/>
<uses-feature android:name="android.hardware.sensor.stepdetector" android:required="false"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
<!-- No backup and no transfer to another phone (ADR 0030, D53): through
Android 11 allowBackup and fullBackupContent stop both, from 12 on
the extraction rules stop the transfer. -->
<application
android:label="Halkyon Train"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:fullBackupContent="false"
android:dataExtractionRules="@xml/data_extraction_rules"
tools:targetApi="s">
<activity
android:name=".MainActivity"
android:exported="true"

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Nothing of Train's goes to a backup or to another phone, from Android 12
on (ADR 0030, D53): allowBackup="false" stops the backup, and only these
rules stop the transfer. What Train keeps unsealed stays on this phone;
what is sealed comes back from the instance on the next. -->
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" path="."/>
<exclude domain="file" path="."/>
<exclude domain="database" path="."/>
<exclude domain="sharedpref" path="."/>
<exclude domain="external" path="."/>
<exclude domain="device_root" path="."/>
<exclude domain="device_file" path="."/>
<exclude domain="device_database" path="."/>
<exclude domain="device_sharedpref" path="."/>
</cloud-backup>
<device-transfer>
<exclude domain="root" path="."/>
<exclude domain="file" path="."/>
<exclude domain="database" path="."/>
<exclude domain="sharedpref" path="."/>
<exclude domain="external" path="."/>
<exclude domain="device_root" path="."/>
<exclude domain="device_file" path="."/>
<exclude domain="device_database" path="."/>
<exclude domain="device_sharedpref" path="."/>
</device-transfer>
</data-extraction-rules>

View file

@ -0,0 +1,73 @@
package app.halkyon.train
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.w3c.dom.Element
/**
* Nothing of Train's leaves the phone by Android's backup or a transfer to
* another phone (ADR 0030, D53): what the app keeps unsealed (a recording,
* the Loop's days, the body's figures, the maps along routes) would go
* there unsealed, and what is sealed comes back from the instance anyway.
*/
class BackupRulesTest {
private val android = "http://schemas.android.com/apk/res/android"
private fun parse(path: String) =
DocumentBuilderFactory.newInstance()
.apply { isNamespaceAware = true }
.newDocumentBuilder()
.parse(File(path))
.documentElement
private val application =
parse("src/main/AndroidManifest.xml").getElementsByTagName("application").item(0) as Element
@Test
fun `backup is off, through Android 11 as from 12 on`() {
assertEquals("false", application.getAttributeNS(android, "allowBackup"))
assertEquals("false", application.getAttributeNS(android, "fullBackupContent"))
assertEquals(
"@xml/data_extraction_rules",
application.getAttributeNS(android, "dataExtractionRules"),
)
}
@Test
fun `from Android 12 on, neither a backup nor a transfer takes any of the app's folders`() {
// allowBackup="false" leaves a transfer to another phone on from
// Android 12 on: the rules have to exclude it too, folder by folder.
val rules = parse("src/main/res/xml/data_extraction_rules.xml")
for (kind in listOf("cloud-backup", "device-transfer")) {
val section = rules.getElementsByTagName(kind).item(0) as Element?
assertTrue("$kind is missing", section != null)
val excluded =
section!!.getElementsByTagName("exclude").let { list ->
(0 until list.length)
.map { list.item(it) as Element }
.filter { it.getAttribute("path") == "." }
.map { it.getAttribute("domain") }
.toSet()
}
assertEquals(
"$kind excludes",
setOf(
"root",
"file",
"database",
"sharedpref",
"external",
"device_root",
"device_file",
"device_database",
"device_sharedpref",
),
excluded,
)
assertEquals("$kind includes nothing", 0, section.getElementsByTagName("include").length)
}
}
}