mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
An APK download with no external storage went nowhere, silently (#266)
getExternalFilesDir() returns null whenever external storage is unavailable, and
on a signage panel that is not exotic: no emulated volume, a vendor ROM that never
mounts one, an ejected card, storage still unmounted early in boot.
Both APK download paths did:
File(context.getExternalFilesDir(DIRECTORY_DOWNLOADS), name)
Java's File(File, String) treats a null parent as "no parent" and silently yields
a RELATIVE path, so the download targeted `ScreenTinker-x.y.z.apk` in the process
working directory — `/` — which is not writable. The write threw, the generic
catch swallowed it, and the caller reported only "failed to download or failed
signature verification".
That message is why this was expensive to find. The HTTP request SUCCEEDS (the
server logs a served download at the exact moment of each failure), so it does not
look like a download problem; the signing key is fine, so it does not look like a
verification problem; and because nothing is ever written there is no partial file
to find. It also never recovers — every attempt fails the same way, forever.
installFromUrl (the dashboard "Push an APK" button) carried the identical line, so
the obvious workaround for a panel in this state was broken by the same bug.
Both now use apkDir(), which falls back to internal storage. filesDir cannot be
unmounted: if it is gone the app is not running.
file_paths.xml gains a <files-path> for the same directory. The silent
PackageInstaller path streams the file itself and needs nothing there, but the
intent-based install FALLBACK resolves it through FileProvider and would throw
"Failed to find configured root" — turning an already-degraded panel into one that
cannot install at all.
⚠️ This cannot reach an affected panel over the air: the broken download path IS
the delivery mechanism, and Push APK shares the bug. A panel already in this state
needs one manual install to escape it.
Root cause is inferred from converging evidence on an Android 9 panel (HTTP served
at each failure, no APK anywhere on the device, the app's own external files dir
denied to its own UID, both paths failing identically, signing verified at parity
against the release it already installed). Handling a null return is correct
regardless — it must never become a relative path.
This commit is contained in:
parent
1fc50ec263
commit
3234c923a3
|
|
@ -311,10 +311,35 @@ class UpdateChecker(private val context: Context) {
|
|||
// Returns TRUE only when a verified APK is in hand and an install has been launched (the
|
||||
// caller may then count an attempt); FALSE on any download/verify failure — the caller must
|
||||
// NOT count those, so a transient network problem can't burn a healthy device's budget. #139
|
||||
/*
|
||||
* Where a downloaded APK is staged.
|
||||
*
|
||||
* getExternalFilesDir() returns NULL whenever external storage is not mounted/available — and
|
||||
* on a signage panel that is not exotic: no emulated volume, a vendor ROM that never mounts one,
|
||||
* an SD card ejected, storage still unmounted early in boot.
|
||||
*
|
||||
* The bug this replaces: `File(context.getExternalFilesDir(...), name)`. Java's File(File,String)
|
||||
* treats a NULL parent as "no parent" and silently produces a RELATIVE path, so the download
|
||||
* targeted `ScreenTinker-x.y.z.apk` in the process working directory — `/` — which is not
|
||||
* writable. The write threw, the generic catch swallowed it, and the caller reported only
|
||||
* "failed to download or failed signature verification". Nothing was ever written, so there was
|
||||
* no partial file to find and nothing in the message pointed at storage. It fails on EVERY
|
||||
* attempt, forever, on an affected panel — and identically for the pushed-APK path, which had
|
||||
* the same line.
|
||||
*
|
||||
* Internal storage always exists, so fall back to it. It costs nothing when external is present.
|
||||
* NOTE: the intent-based install fallback resolves this file through FileProvider, so
|
||||
* res/xml/file_paths.xml must expose this directory too — see the <files-path> entry there.
|
||||
*/
|
||||
private fun apkDir(): File {
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.let { return it }
|
||||
Log.w(TAG, "External storage unavailable — staging APKs in internal storage instead")
|
||||
return File(context.filesDir, "Download").apply { mkdirs() }
|
||||
}
|
||||
|
||||
private fun downloadAndInstall(url: String, version: String): Boolean {
|
||||
try {
|
||||
val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"ScreenTinker-$version.apk")
|
||||
val apkFile = File(apkDir(), "ScreenTinker-$version.apk")
|
||||
|
||||
// #139: reuse a previously-downloaded, verified APK for this version instead of
|
||||
// re-pulling ~8.7 MB every cycle. The file also stays on disk as the artifact for a
|
||||
|
|
@ -378,7 +403,7 @@ class UpdateChecker(private val context: Context) {
|
|||
try {
|
||||
val base = url.substringAfterLast('/').substringBefore('?').ifBlank { "app.apk" }
|
||||
val fileName = "pushed-" + (if (base.endsWith(".apk")) base else "$base.apk")
|
||||
val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)
|
||||
val apkFile = File(apkDir(), fileName)
|
||||
if (apkFile.exists()) apkFile.delete()
|
||||
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
||||
if (!response.isSuccessful) { Log.e(TAG, "installFromUrl: download failed ${response.code}"); return@Thread }
|
||||
|
|
|
|||
|
|
@ -2,4 +2,11 @@
|
|||
<paths>
|
||||
<external-files-path name="downloads" path="Download/" />
|
||||
<external-files-path name="apk" path="." />
|
||||
<!-- UpdateChecker.apkDir() stages APKs in internal storage when external storage is not
|
||||
available (getExternalFilesDir returns null). The silent PackageInstaller path streams the
|
||||
file itself and needs nothing here, but the intent-based install FALLBACK resolves it
|
||||
through FileProvider — without this entry that fallback throws
|
||||
IllegalArgumentException ("Failed to find configured root"), turning an already-degraded
|
||||
panel into one that cannot install at all. -->
|
||||
<files-path name="internal_downloads" path="Download/" />
|
||||
</paths>
|
||||
|
|
|
|||
Loading…
Reference in a new issue