Add in-app update installer flow and menu tools
Some checks failed
Android CI / build (push) Has been cancelled

This commit is contained in:
dom4k
2026-03-17 02:43:13 +00:00
parent 909d1462f7
commit c158fd63b6
10 changed files with 156 additions and 3 deletions

View File

@@ -17,6 +17,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:allowBackup="true"
@@ -53,6 +54,16 @@
android:enabled="true"
android:exported="false"
android:foregroundServiceType="connectedDevice" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -156,6 +156,14 @@ class MainActivity : AppCompatActivity() {
menuInflater.inflate(R.menu.main_menu, menu)
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_map -> {
startActivity(Intent(this@MainActivity, PacketMapActivity::class.java))
true
}
R.id.menu_packets -> {
startActivity(Intent(this@MainActivity, PacketLogActivity::class.java))
true
}
R.id.menu_settings -> {
startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
true

View File

@@ -2,6 +2,7 @@ package pro.nnnteam.nnnet
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
import android.widget.TextView
@@ -16,6 +17,7 @@ import pro.nnnteam.nnnet.data.MeshRepository
import pro.nnnteam.nnnet.data.ProfileEntity
import pro.nnnteam.nnnet.mesh.MeshServiceContract
import pro.nnnteam.nnnet.update.UpdateInfo
import pro.nnnteam.nnnet.update.UpdateInstaller
import pro.nnnteam.nnnet.update.UpdateManager
import android.content.BroadcastReceiver
import android.content.Intent
@@ -33,6 +35,7 @@ class SettingsActivity : AppCompatActivity() {
private lateinit var resultUsernameText: TextView
private lateinit var resultDescriptionText: TextView
private lateinit var resultPeerIdText: TextView
private lateinit var updateProgressText: TextView
private var receiverRegistered = false
@@ -77,6 +80,7 @@ class SettingsActivity : AppCompatActivity() {
resultUsernameText = findViewById(R.id.resultUsernameText)
resultDescriptionText = findViewById(R.id.resultDescriptionText)
resultPeerIdText = findViewById(R.id.resultPeerIdText)
updateProgressText = findViewById(R.id.updateProgressText)
val autoUpdateSwitch = findViewById<SwitchMaterial>(R.id.autoUpdateSwitch)
val versionText = findViewById<TextView>(R.id.versionText)
@@ -198,14 +202,17 @@ class SettingsActivity : AppCompatActivity() {
private fun checkForUpdates() {
lifecycleScope.launch {
showUpdateProgress(getString(R.string.update_checking))
val updateInfo = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { UpdateManager.fetchUpdateInfo() }
if (updateInfo == null) {
hideUpdateProgress()
Toast.makeText(this@SettingsActivity, R.string.update_check_failed, Toast.LENGTH_SHORT).show()
return@launch
}
if (updateInfo.versionCode > currentVersionCode()) {
showUpdateDialog(updateInfo)
} else {
hideUpdateProgress()
Toast.makeText(this@SettingsActivity, R.string.latest_version_installed, Toast.LENGTH_SHORT).show()
}
}
@@ -228,14 +235,40 @@ class SettingsActivity : AppCompatActivity() {
}
)
.setPositiveButton(R.string.download_update) { _, _ ->
val url = UpdateManager.buildDownloadUrl(updateInfo.apkPath)
startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse(url)))
downloadAndInstallUpdate(updateInfo)
}
.setNegativeButton(R.string.later, null)
.show()
}
}
private fun downloadAndInstallUpdate(updateInfo: UpdateInfo) {
lifecycleScope.launch {
showUpdateProgress(getString(R.string.update_downloading))
val apkFile = UpdateInstaller.downloadToTempFile(this@SettingsActivity, updateInfo)
if (apkFile == null) {
hideUpdateProgress()
Toast.makeText(this@SettingsActivity, R.string.update_download_failed, Toast.LENGTH_SHORT).show()
return@launch
}
showUpdateProgress(getString(R.string.update_installing))
val started = UpdateInstaller.installDownloadedApk(this@SettingsActivity, apkFile)
if (!started) {
hideUpdateProgress()
}
}
}
private fun showUpdateProgress(message: String) {
updateProgressText.visibility = View.VISIBLE
updateProgressText.text = message
}
private fun hideUpdateProgress() {
updateProgressText.visibility = View.GONE
}
private fun currentVersionCode(): Int {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

View File

@@ -0,0 +1,72 @@
package pro.nnnteam.nnnet.update
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.core.content.FileProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pro.nnnteam.nnnet.R
import pro.nnnteam.nnnet.mesh.MeshForegroundService
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
object UpdateInstaller {
suspend fun downloadToTempFile(context: Context, updateInfo: UpdateInfo): File? {
return withContext(Dispatchers.IO) {
runCatching {
val updatesDir = File(context.cacheDir, "updates").apply { mkdirs() }
val apkFile = File(updatesDir, "nnnet-update-${updateInfo.versionName}.apk")
val connection = URL(UpdateManager.buildDownloadUrl(updateInfo.apkPath)).openConnection() as HttpURLConnection
connection.connectTimeout = 15_000
connection.readTimeout = 60_000
connection.inputStream.use { input ->
apkFile.outputStream().use { output ->
input.copyTo(output)
}
}
apkFile
}.getOrNull()
}
}
fun installDownloadedApk(context: Context, apkFile: File, stopMesh: Boolean = true): Boolean {
if (!apkFile.exists()) return false
if (stopMesh) {
MeshForegroundService.stop(context)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !context.packageManager.canRequestPackageInstalls()) {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
data = Uri.parse("package:${context.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
Toast.makeText(context, R.string.allow_unknown_apps, Toast.LENGTH_LONG).show()
return false
}
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
apkFile
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
return try {
context.startActivity(intent)
true
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.installer_not_found, Toast.LENGTH_SHORT).show()
false
}
}
}

View File

@@ -233,6 +233,14 @@
android:layout_marginTop="16dp"
android:text="@string/check_updates"
app:cornerRadius="18dp" />
<TextView
android:id="@+id/updateProgressText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textColor="@color/secondary_text"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_map"
android:title="@string/map_mode_title" />
<item
android:id="@+id/menu_packets"
android:title="@string/packet_log_title" />
<item
android:id="@+id/menu_settings"
android:title="@string/settings_title" />

View File

@@ -21,9 +21,15 @@
<string name="peer_id_required">Введите ID устройства</string>
<string name="profile_not_found_locally">Профиль не найден в локальном каталоге сети</string>
<string name="update_check_failed">Не удалось проверить обновления</string>
<string name="update_download_failed">Не удалось скачать обновление</string>
<string name="latest_version_installed">У вас уже установлена последняя версия</string>
<string name="update_available_message">Доступна версия %1$s.</string>
<string name="download_update">Скачать обновление</string>
<string name="update_checking">Проверяем версию…</string>
<string name="update_downloading">Скачиваем APK во временный каталог…</string>
<string name="update_installing">Останавливаем сеть и запускаем установку…</string>
<string name="allow_unknown_apps">Разрешите установку из этого приложения, затем повторите обновление.</string>
<string name="installer_not_found">Системный установщик не найден</string>
<string name="later">Позже</string>
<string name="back">Назад</string>
<string name="no_messages">Сообщений пока нет. Напишите первым.</string>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path
name="update_cache"
path="updates/" />
</paths>