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