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
This commit is contained in:
@@ -23,6 +23,9 @@
|
||||
<!-- Move task to front (bring kiosk back after gateway launch) -->
|
||||
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||
|
||||
<!-- Self-update: install APK downloaded at runtime -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<!-- Query gateway app visibility (required Android 11+) -->
|
||||
<queries>
|
||||
<package android:name="it.dadaloop.evershelf.scalegate" />
|
||||
@@ -54,6 +57,17 @@
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" />
|
||||
|
||||
<!-- FileProvider for serving the downloaded APK to the installer -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -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
|
||||
@@ -97,6 +96,7 @@ class KioskActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||
if (id != downloadId) return
|
||||
unregisterReceiver(this)
|
||||
installApk()
|
||||
// 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,9 +834,11 @@ class KioskActivity : AppCompatActivity() {
|
||||
}
|
||||
startActivity(install)
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error Page ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<!-- App-private external dir: no storage permission needed -->
|
||||
<external-files-path name="apk_downloads" path="." />
|
||||
</paths>
|
||||
@@ -24,6 +24,9 @@
|
||||
<!-- Keep screen on while gateway is active -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Self-update: install APK downloaded at runtime -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
||||
|
||||
<application
|
||||
@@ -45,6 +48,17 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- FileProvider for serving the downloaded APK to the installer -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
+36
-10
@@ -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) {
|
||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||
if (id != downloadId) return
|
||||
unregisterReceiver(this)
|
||||
installApk("evershelf-scale-update.apk")
|
||||
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,9 +523,11 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener
|
||||
}
|
||||
startActivity(install)
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "Errore installazione: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── RecyclerView adapter ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<!-- App-private external dir: no storage permission needed -->
|
||||
<external-files-path name="apk_downloads" path="." />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user