feat: clean up the 1.x app's leftover data on upgrade

The React Native 1.x app shipped as com.subtracks and left an AsyncStorage
database (RKStorage), an older `servers` dump and a downloaded/cached
artwork+audio tree in app-specific storage. v3 reads none of those paths, so
an update would otherwise leave the data behind, reachable only by wiping the
whole app.

Remove them once at startup, gated on one of those 1.x paths existing, so a
fresh install does nothing and an upgrade is cleaned exactly once. The cached
trees can be large, so the delete runs off the main thread. Kept in a single
file so it can be deleted once no installs can come from 1.x.
This commit is contained in:
austinried
2026-10-09 17:05:19 +00:00
parent e0a09b724b
commit 0a3e839da0
3 changed files with 110 additions and 0 deletions
@@ -5,6 +5,7 @@ import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.subtracks.data.LegacyDataCleanup
import com.subtracks.data.prefs.UserPreferences
import com.subtracks.data.repo.ArtworkSeedStore
import com.subtracks.di.appModule
@@ -25,6 +26,7 @@ class SubtracksApp : Application() {
override fun onCreate() {
super.onCreate()
Log.init(this)
LegacyDataCleanup.run(this)
stopKoin()
SingletonImageLoader.reset()
val koin =
@@ -0,0 +1,41 @@
package com.subtracks.data
import android.content.Context
import com.subtracks.log.Log
import java.io.File
// One-shot cleanup of the 1.x React Native app, which shipped as com.subtracks before this rewrite.
// It left an AsyncStorage database (RKStorage), an older `servers` dump and a downloaded/cached
// artwork+audio tree (under the app's external files dir) that v3 never reads. Only a real 1.x upgrade
// has those paths, so a fresh install does nothing and an upgrade is cleaned exactly once.
//
// Delete this file and its call in SubtracksApp once installs can no longer be coming from 1.x.
internal object LegacyDataCleanup {
fun run(context: Context) {
// The cached trees can be large, so keep the walk off the main thread.
Thread { run(context.dataDir, context.getExternalFilesDir(null)) }.start()
}
internal fun run(
dataDir: File,
externalFilesDir: File?,
) {
val database = File(dataDir, "databases/RKStorage")
val servers = File(dataDir, "files/servers")
val cache = externalFilesDir?.let { File(it, "s") }
if (!database.exists() && !servers.exists() && cache?.exists() != true) return
deleteDatabase(database)
deleteTree(servers)
cache?.let(::deleteTree)
}
private fun deleteDatabase(database: File) {
listOf("", "-wal", "-shm", "-journal").forEach { deleteTree(File(database.path + it)) }
}
private fun deleteTree(target: File) {
if (!target.exists()) return
val removed = target.deleteRecursively()
Log.i("legacy", "removed ${target.path} ($removed)")
}
}
@@ -0,0 +1,67 @@
package com.subtracks.data
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
class LegacyDataCleanupTest {
@get:Rule
val tmp = TemporaryFolder()
@Test
fun removesThe1xLeftoversAndLeavesTheNativeData() {
val dataDir = tmp.newFolder("data")
val external = tmp.newFolder("external")
val legacyDatabase = file(dataDir, "databases/RKStorage")
val legacyDatabaseWal = file(dataDir, "databases/RKStorage-wal")
val legacyServers = file(dataDir, "files/servers/0.json")
val legacyCache = file(external, "s/0/coverArt/cover")
val nativeDatabase = file(dataDir, "databases/subtracks.db")
val nativeDownload = file(external, "Music/downloads/song.mp3")
LegacyDataCleanup.run(dataDir, external)
assertFalse(legacyDatabase.exists())
assertFalse(legacyDatabaseWal.exists())
assertFalse(legacyServers.exists())
assertFalse(legacyCache.exists())
assertTrue(nativeDatabase.exists())
assertTrue(nativeDownload.exists())
}
@Test
fun leavesTheNativeDataAloneOnAFreshInstall() {
val dataDir = tmp.newFolder("data2")
val external = tmp.newFolder("external2")
val nativeDatabase = file(dataDir, "databases/subtracks.db")
val nativeDownload = file(external, "Music/downloads/song.mp3")
LegacyDataCleanup.run(dataDir, external)
assertTrue(nativeDatabase.exists())
assertTrue(nativeDownload.exists())
}
@Test
fun cleansUpEvenWhenExternalStorageIsUnavailable() {
val dataDir = tmp.newFolder("data3")
val legacyDatabase = file(dataDir, "databases/RKStorage")
LegacyDataCleanup.run(dataDir, null)
assertFalse(legacyDatabase.exists())
}
private fun file(
dir: File,
path: String,
): File =
File(dir, path).apply {
parentFile?.mkdirs()
createNewFile()
}
}