From f9718fee6da63e6b6f39aca1294343c1ceea4288 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Sun, 3 May 2026 17:37:45 +0000 Subject: [PATCH] fix: APK self-update download+install in kiosk and scale gateway Root causes fixed: - REQUEST_INSTALL_PACKAGES permission missing from both manifests - FileProvider not declared in either manifest (FileProvider.getUriForFile() crashed) - res/xml/file_paths.xml missing (required by FileProvider) - setDestinationInExternalPublicDir() used public Downloads dir (needs storage permission + FileProvider can't serve it); replaced with getExternalFilesDir() which is app-private, needs no permission, and IS accessible by FileProvider - canRequestPackageInstalls() check returned early with startActivity (fire-and- forget); user could never retry. Now uses startActivityForResult/installPermLauncher so the download auto-retries when user returns from the Settings screen - Added download status check (COLUMN_STATUS == STATUS_SUCCESSFUL) before installing --- .../app/src/main/AndroidManifest.xml | 14 +++++ .../dadaloop/evershelf/kiosk/KioskActivity.kt | 62 ++++++++++++++----- .../app/src/main/res/xml/file_paths.xml | 5 ++ .../app/src/main/AndroidManifest.xml | 14 +++++ .../evershelf/scalegate/MainActivity.kt | 50 +++++++++++---- .../app/src/main/res/xml/file_paths.xml | 5 ++ 6 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 evershelf-kiosk/app/src/main/res/xml/file_paths.xml create mode 100644 evershelf-scale-gateway/app/src/main/res/xml/file_paths.xml diff --git a/evershelf-kiosk/app/src/main/AndroidManifest.xml b/evershelf-kiosk/app/src/main/AndroidManifest.xml index 4a15cd9..997c2b8 100644 --- a/evershelf-kiosk/app/src/main/AndroidManifest.xml +++ b/evershelf-kiosk/app/src/main/AndroidManifest.xml @@ -23,6 +23,9 @@ + + + @@ -54,6 +57,17 @@ android:exported="false" android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" /> + + + + + diff --git a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt index ece56ed..0ff8853 100644 --- a/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt +++ b/evershelf-kiosk/app/src/main/kotlin/it/dadaloop/evershelf/kiosk/KioskActivity.kt @@ -15,7 +15,6 @@ import android.net.Uri import android.net.http.SslError import android.os.Build import android.os.Bundle -import android.os.Environment import android.os.Handler import android.os.Looper import android.provider.Settings @@ -95,8 +94,9 @@ class KioskActivity : AppCompatActivity() { private var pendingWebPermission: PermissionRequest? = null companion object { - private const val FILE_CHOOSER_REQUEST = 1002 + private const val FILE_CHOOSER_REQUEST = 1002 private const val PERMISSION_REQUEST_CODE = 1003 + private const val INSTALL_PERM_REQUEST = 1004 // ACTION_MANAGE_UNKNOWN_APP_SOURCES private const val PREFS_NAME = "evershelf_kiosk" private const val KEY_URL = "evershelf_url" private const val KEY_SETUP_COMPLETE = "setup_complete" @@ -758,33 +758,52 @@ class KioskActivity : AppCompatActivity() { private fun triggerApkDownload(apkUrl: String) { if (apkUrl.isEmpty()) return try { - // On Android 8+ we need to check "install unknown apps" permission + // On Android 8+ check the "install unknown apps" source permission if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !packageManager.canRequestPackageInstalls()) { - val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, - Uri.parse("package:$packageName")) - startActivity(intent) - Toast.makeText(this, "Abilita 'Installa app sconosciute', poi ripremi Scarica", Toast.LENGTH_LONG).show() + pendingApkDownloadUrl = apkUrl // remember URL for the retry + @Suppress("DEPRECATION") + startActivityForResult( + Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, + Uri.parse("package:$packageName")), + INSTALL_PERM_REQUEST + ) + Toast.makeText(this, "Abilita 'Installa app sconosciute', poi torna qui", Toast.LENGTH_LONG).show() return } - val dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager + // Download to app-private external dir — no storage permission needed + val destDir = getExternalFilesDir(null) ?: filesDir + val destFile = java.io.File(destDir, "evershelf-update.apk") + + val dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager val req = DownloadManager.Request(Uri.parse(apkUrl)).apply { setTitle("EverShelf — Aggiornamento") setDescription("Scaricamento aggiornamento in corso…") setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "evershelf-update.apk") + setDestinationUri(Uri.fromFile(destFile)) setMimeType("application/vnd.android.package-archive") } val downloadId = dm.enqueue(req) Toast.makeText(this, "Download avviato…", Toast.LENGTH_SHORT).show() - // Listen for completion val receiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context?, intent: Intent?) { - if (intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) == downloadId) { - unregisterReceiver(this) - installApk() + val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) + if (id != downloadId) return + unregisterReceiver(this) + // Verify the download succeeded before trying to install + val q = DownloadManager.Query().setFilterById(downloadId) + val c = (getSystemService(DOWNLOAD_SERVICE) as DownloadManager).query(q) + var ok = false + if (c.moveToFirst()) { + val status = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + ok = (status == DownloadManager.STATUS_SUCCESSFUL) + } + c.close() + if (ok) installApk(destFile) + else runOnUiThread { + Toast.makeText(this@KioskActivity, "Download fallito, riprova", Toast.LENGTH_LONG).show() } } } @@ -799,11 +818,12 @@ class KioskActivity : AppCompatActivity() { } } - private fun installApk() { + private fun installApk(file: java.io.File) { try { - val file = java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "evershelf-update.apk") val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - androidx.core.content.FileProvider.getUriForFile(this, "$packageName.provider", file) + androidx.core.content.FileProvider.getUriForFile( + this, "$packageName.provider", file + ) } else { Uri.fromFile(file) } @@ -814,7 +834,9 @@ class KioskActivity : AppCompatActivity() { } startActivity(install) } catch (e: Exception) { - Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show() + runOnUiThread { + Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show() + } } } @@ -896,6 +918,12 @@ class KioskActivity : AppCompatActivity() { fileChooserCallback?.onReceiveValue(result) fileChooserCallback = null } + // Returned from ACTION_MANAGE_UNKNOWN_APP_SOURCES — retry the download + // regardless of resultCode (the system always returns RESULT_CANCELED here). + if (requestCode == INSTALL_PERM_REQUEST) { + val url = pendingApkDownloadUrl + if (url.isNotEmpty()) triggerApkDownload(url) + } } override fun onDestroy() { diff --git a/evershelf-kiosk/app/src/main/res/xml/file_paths.xml b/evershelf-kiosk/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..50599dc --- /dev/null +++ b/evershelf-kiosk/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/evershelf-scale-gateway/app/src/main/AndroidManifest.xml b/evershelf-scale-gateway/app/src/main/AndroidManifest.xml index d57f744..3584955 100644 --- a/evershelf-scale-gateway/app/src/main/AndroidManifest.xml +++ b/evershelf-scale-gateway/app/src/main/AndroidManifest.xml @@ -24,6 +24,9 @@ + + + + + + + + diff --git a/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt b/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt index fedf0b0..bf6e4bb 100644 --- a/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt +++ b/evershelf-scale-gateway/app/src/main/kotlin/it/dadaloop/evershelf/scalegate/MainActivity.kt @@ -12,7 +12,6 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Environment import android.provider.Settings import android.view.LayoutInflater import android.view.View @@ -81,6 +80,14 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener else showDialog("Bluetooth required", "Please enable Bluetooth to use the gateway.") } + /** Returns from ACTION_MANAGE_UNKNOWN_APP_SOURCES — retry the download. */ + private val installPermLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { _ -> + val url = pendingApkDownloadUrl + if (url.isNotEmpty()) triggerApkDownload(url) + } + // ─── Lifecycle ───────────────────────────────────────────────────────────── override fun onCreate(savedInstanceState: Bundle?) { @@ -453,26 +460,42 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !packageManager.canRequestPackageInstalls()) { - val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:$packageName")) - startActivity(intent) - Toast.makeText(this, "Abilita 'Installa app sconosciute', poi ripremi Scarica", Toast.LENGTH_LONG).show() + pendingApkDownloadUrl = apkUrl // remember for retry + installPermLauncher.launch( + Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:$packageName")) + ) + Toast.makeText(this, "Abilita 'Installa app sconosciute', poi torna qui", Toast.LENGTH_LONG).show() return } + // Download to app-private external dir — no storage permission needed + val destDir = getExternalFilesDir(null) ?: filesDir + val destFile = java.io.File(destDir, "evershelf-scale-update.apk") val dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager val req = DownloadManager.Request(Uri.parse(apkUrl)).apply { setTitle("EverShelf Scale Gateway — Aggiornamento") setDescription("Scaricamento aggiornamento…") setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "evershelf-scale-update.apk") + setDestinationUri(Uri.fromFile(destFile)) setMimeType("application/vnd.android.package-archive") } val downloadId = dm.enqueue(req) Toast.makeText(this, "Download avviato…", Toast.LENGTH_SHORT).show() val receiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context?, intent: Intent?) { - if (intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) == downloadId) { - unregisterReceiver(this) - installApk("evershelf-scale-update.apk") + val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) + if (id != downloadId) return + unregisterReceiver(this) + val q = DownloadManager.Query().setFilterById(downloadId) + val c = (getSystemService(DOWNLOAD_SERVICE) as DownloadManager).query(q) + var ok = false + if (c.moveToFirst()) { + val status = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + ok = (status == DownloadManager.STATUS_SUCCESSFUL) + } + c.close() + if (ok) installApk(destFile) + else runOnUiThread { + Toast.makeText(this@MainActivity, "Download fallito, riprova", Toast.LENGTH_LONG).show() } } } @@ -487,11 +510,12 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener } } - private fun installApk(fileName: String) { + private fun installApk(file: java.io.File) { try { - val file = java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName) val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - androidx.core.content.FileProvider.getUriForFile(this, "$packageName.provider", file) + androidx.core.content.FileProvider.getUriForFile( + this, "$packageName.provider", file + ) } else { Uri.fromFile(file) } val install = Intent(Intent.ACTION_VIEW).apply { setDataAndType(uri, "application/vnd.android.package-archive") @@ -499,7 +523,9 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener } startActivity(install) } catch (e: Exception) { - Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show() + runOnUiThread { + Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show() + } } } diff --git a/evershelf-scale-gateway/app/src/main/res/xml/file_paths.xml b/evershelf-scale-gateway/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..50599dc --- /dev/null +++ b/evershelf-scale-gateway/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + +