mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
merge: in-app apk updater (#12)
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".AngApplication"
|
||||
|
||||
@@ -70,35 +70,41 @@ object UpdateCheckerManager {
|
||||
}
|
||||
|
||||
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
|
||||
?: throw IllegalStateException("Failed to create connection")
|
||||
// Try a direct connection first, then fall back to the local proxy
|
||||
// (mirrors checkForUpdate). The proxy only listens while the VPN is up,
|
||||
// so a direct attempt is what makes updating work when disconnected.
|
||||
downloadApkVia(context, downloadUrl, 0)
|
||||
?: downloadApkVia(context, downloadUrl, SettingsManager.getHttpPort())
|
||||
}
|
||||
|
||||
try {
|
||||
val apkFile = File(context.cacheDir, "update.apk")
|
||||
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
|
||||
private fun downloadApkVia(context: Context, downloadUrl: String, httpPort: Int): File? {
|
||||
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
|
||||
?: return null
|
||||
return try {
|
||||
val code = connection.responseCode
|
||||
if (code !in 200..299) {
|
||||
Log.e(AppConfig.TAG, "APK download failed, http $code (port=$httpPort)")
|
||||
return null
|
||||
}
|
||||
val apkFile = File(context.cacheDir, "update.apk")
|
||||
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath} (port=$httpPort)")
|
||||
|
||||
FileOutputStream(apkFile).use { outputStream ->
|
||||
connection.inputStream.use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
Log.i(AppConfig.TAG, "APK download completed")
|
||||
return@withContext apkFile
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
|
||||
return@withContext null
|
||||
} finally {
|
||||
try {
|
||||
connection.disconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
|
||||
FileOutputStream(apkFile).use { outputStream ->
|
||||
connection.inputStream.use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
Log.i(AppConfig.TAG, "APK download completed")
|
||||
apkFile
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
|
||||
return@withContext null
|
||||
Log.e(AppConfig.TAG, "Failed to download APK (port=$httpPort): ${e.message}")
|
||||
null
|
||||
} finally {
|
||||
try {
|
||||
connection.disconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package xyz.zarazaex.olc.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
@@ -63,6 +66,40 @@ class CheckUpdateActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadAndInstall(downloadUrl: String) {
|
||||
if (!Utils.canInstallApk(this)) {
|
||||
toast(R.string.update_install_permission_required)
|
||||
try {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||
"package:$packageName".toUri()
|
||||
)
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to open install-sources settings: ${e.message}")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
toast(R.string.update_downloading)
|
||||
showLoading()
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val apk = UpdateCheckerManager.downloadApk(this@CheckUpdateActivity, downloadUrl)
|
||||
if (apk != null && Utils.installApk(this@CheckUpdateActivity, apk)) {
|
||||
return@launch
|
||||
}
|
||||
toastError(R.string.update_download_failed)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to download/install update: ${e.message}")
|
||||
toastError(e.message ?: getString(R.string.update_download_failed))
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUpdateDialog(result: CheckUpdateResult) {
|
||||
val message = result.releaseNotes?.let { MarkdownUtil.parseBasic(it) } ?: ""
|
||||
val titleStr = getString(R.string.update_new_version_found, result.latestVersion)
|
||||
@@ -70,9 +107,7 @@ class CheckUpdateActivity : BaseActivity() {
|
||||
.setTitle(titleStr)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.update_now) { _, _ ->
|
||||
result.downloadUrl?.let {
|
||||
Utils.openUri(this, it)
|
||||
}
|
||||
result.downloadUrl?.let { downloadAndInstall(it) }
|
||||
}
|
||||
.create()
|
||||
dialog.show()
|
||||
|
||||
@@ -15,10 +15,12 @@ import android.util.Log
|
||||
import android.util.Patterns
|
||||
import android.webkit.URLUtil
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.AppConfig.LOOPBACK
|
||||
import xyz.zarazaex.olc.BuildConfig
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
@@ -291,6 +293,40 @@ object Utils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the system package installer for a downloaded APK file.
|
||||
*
|
||||
* @param context The context to use.
|
||||
* @param apkFile The downloaded APK file (must live under the FileProvider paths).
|
||||
* @return true if the installer intent was started, false otherwise.
|
||||
*/
|
||||
fun installApk(context: Context, apkFile: File): Boolean {
|
||||
return try {
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.cache",
|
||||
apkFile
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to install APK", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the app is allowed to install APKs from unknown sources.
|
||||
*/
|
||||
fun canInstallApk(context: Context): Boolean {
|
||||
return context.packageManager.canRequestPackageInstalls()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a UUID.
|
||||
*
|
||||
|
||||
@@ -575,6 +575,9 @@
|
||||
<string name="update_now">Update now</string>
|
||||
<string name="update_check_pre_release">Check Pre-release</string>
|
||||
<string name="update_checking_for_update">Checking for update…</string>
|
||||
<string name="update_downloading">Downloading update…</string>
|
||||
<string name="update_download_failed">Download failed</string>
|
||||
<string name="update_install_permission_required">Allow installing unknown apps to update, then tap Update now again</string>
|
||||
|
||||
<string name="title_policy_group_type">Policy group type</string>
|
||||
<string
|
||||
|
||||
Reference in New Issue
Block a user