Add in-app update installer flow and menu tools
Some checks failed
Android CI / build (push) Has been cancelled
Some checks failed
Android CI / build (push) Has been cancelled
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
android/app/src/main/res/xml/file_paths.xml
Normal file
6
android/app/src/main/res/xml/file_paths.xml
Normal 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>
|
||||
Reference in New Issue
Block a user