diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml
index ae32e2cb..83ef99b4 100644
--- a/V2rayNG/app/src/main/AndroidManifest.xml
+++ b/V2rayNG/app/src/main/AndroidManifest.xml
@@ -41,6 +41,7 @@
+
- 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}")
+ }
}
}
diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt
index 2bc5473f..93d35092 100644
--- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt
+++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt
@@ -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()
diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/util/Utils.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/util/Utils.kt
index d81e7749..ef2850c0 100644
--- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/util/Utils.kt
+++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/util/Utils.kt
@@ -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.
*
diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml
index 85a49543..2d6affab 100644
--- a/V2rayNG/app/src/main/res/values/strings.xml
+++ b/V2rayNG/app/src/main/res/values/strings.xml
@@ -575,6 +575,9 @@
Update now
Check Pre-release
Checking for update…
+ Downloading update…
+ Download failed
+ Allow installing unknown apps to update, then tap Update now again
Policy group type