mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'bgupdate' into 'master'
BG update initial impl. See merge request videostreaming/grayjay!159
This commit is contained in:
@@ -252,5 +252,14 @@
|
|||||||
android:name=".activities.QRCodeFullscreenActivity"
|
android:name=".activities.QRCodeFullscreenActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<service
|
||||||
|
android:name=".UpdateDownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".UpdateActionReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -403,13 +403,6 @@ class UIDialogs {
|
|||||||
dialog.setMaxVersion(lastVersion);
|
dialog.setMaxVersion(lastVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showInstallDownloadedUpdateDialog(context: Context, apkFile: File) {
|
|
||||||
val dialog = AutoUpdateDialog(context);
|
|
||||||
registerDialogOpened(dialog);
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
|
||||||
dialog.showPredownloaded(apkFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
|
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
|
||||||
if(!store.hasMissingReconstructions())
|
if(!store.hasMissingReconstructions())
|
||||||
onConcluded();
|
onConcluded();
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class UpdateActionReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
UpdateNotificationManager.ACTION_UPDATE_YES -> handleUpdateYes(context, intent)
|
||||||
|
UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
|
||||||
|
UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
|
||||||
|
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
||||||
|
UpdateNotificationManager.ACTION_INSTALL_NOW -> handleInstallNow(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateYes(context: Context, intent: Intent) {
|
||||||
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
if (version == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
||||||
|
|
||||||
|
if (Settings.instance.autoUpdate.backgroundDownload == 1) {
|
||||||
|
val serviceIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(context, serviceIntent)
|
||||||
|
} else {
|
||||||
|
if (StateApp.instance.isMainActive) {
|
||||||
|
StateApp.withContext { ctx ->
|
||||||
|
UIDialogs.showUpdateAvailableDialog(ctx, version, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val startIntent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
putExtra("SHOW_UPDATE_DIALOG_VERSION", version)
|
||||||
|
}
|
||||||
|
context.startActivity(startIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateNo(context: Context) {
|
||||||
|
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateNever(context: Context) {
|
||||||
|
Settings.instance.autoUpdate.check = 1
|
||||||
|
Settings.instance.save()
|
||||||
|
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDownloadCancel(context: Context, intent: Intent) {
|
||||||
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
|
||||||
|
val cancelIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_CANCEL, true)
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(context, cancelIntent)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_DOWNLOADING)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInstallNow(context: Context, intent: Intent) {
|
||||||
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
val apkPath = intent.getStringExtra(UpdateNotificationManager.EXTRA_APK_PATH)
|
||||||
|
|
||||||
|
if (version == 0 || apkPath.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val apkFile = File(apkPath)
|
||||||
|
if (!apkFile.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
|
UpdateInstaller.startInstall(context, apkFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
if (!Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||||
|
Logger.i(TAG, "Auto-update disabled, skipping worker run")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient()
|
||||||
|
val latestVersion = StateUpdate.Companion.instance.downloadVersionCode(client)
|
||||||
|
|
||||||
|
if (latestVersion == null) {
|
||||||
|
Logger.w(TAG, "Failed to fetch latest version in worker")
|
||||||
|
return@withContext Result.retry()
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentVersion = BuildConfig.VERSION_CODE
|
||||||
|
Logger.i(TAG, "Worker check: current=$currentVersion, latest=$latestVersion")
|
||||||
|
|
||||||
|
if (latestVersion <= currentVersion) {
|
||||||
|
return@withContext Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateNotificationManager.showUpdateAvailableNotification(applicationContext, latestVersion)
|
||||||
|
|
||||||
|
if (StateApp.instance.isMainActive) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
StateApp.withContext { ctx ->
|
||||||
|
try {
|
||||||
|
UIDialogs.showUpdateAvailableDialog(ctx, latestVersion, false)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to show in-app update dialog from worker", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.success()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Exception in UpdateCheckWorker", t)
|
||||||
|
Result.retry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UpdateCheckWorker"
|
||||||
|
const val UNIQUE_WORK_NAME = "updateCheck"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class UpdateDownloadService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UpdateDownloadService"
|
||||||
|
const val EXTRA_VERSION = "version"
|
||||||
|
const val EXTRA_CANCEL = "cancel"
|
||||||
|
private const val MAX_RETRIES = 5
|
||||||
|
private const val INITIAL_BACKOFF_MS = 5_000L
|
||||||
|
private const val BUFFER_SIZE = 8 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isDownloading: Boolean = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var cancelRequested: Boolean = false
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) {
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
|
||||||
|
cancelRequested = true
|
||||||
|
Logger.i(TAG, "Download cancel requested")
|
||||||
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
val version = intent.getIntExtra(EXTRA_VERSION, 0)
|
||||||
|
if (version == 0) {
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDownloading) {
|
||||||
|
Logger.i(TAG, "Download already in progress, ignoring new start")
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
isDownloading = true
|
||||||
|
cancelRequested = false
|
||||||
|
|
||||||
|
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
|
||||||
|
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
downloadApk(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun downloadApk(version: Int) {
|
||||||
|
val apkFile = StateUpdate.getApkFile(this, version)
|
||||||
|
val partialFile = StateUpdate.getPartialApkFile(this, version)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (apkFile.exists() && apkFile.length() > 0L) {
|
||||||
|
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
|
||||||
|
onDownloadComplete(version, apkFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var backoffMs = INITIAL_BACKOFF_MS
|
||||||
|
|
||||||
|
for (attempt in 0 until MAX_RETRIES) {
|
||||||
|
if (cancelRequested) {
|
||||||
|
Logger.i(TAG, "Download cancelled before attempt ${attempt + 1}")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
performDownload(StateUpdate.APK_URL, partialFile, version)
|
||||||
|
|
||||||
|
if (!cancelRequested) {
|
||||||
|
if (apkFile.exists()) {
|
||||||
|
apkFile.delete()
|
||||||
|
}
|
||||||
|
if (!partialFile.renameTo(apkFile)) {
|
||||||
|
throw IllegalStateException("Failed to rename partial APK file")
|
||||||
|
}
|
||||||
|
onDownloadComplete(version, apkFile)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
if (cancelRequested) {
|
||||||
|
Logger.i(TAG, "Download cancelled by user", t)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt == MAX_RETRIES - 1) {
|
||||||
|
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
|
||||||
|
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
|
||||||
|
delay(backoffMs)
|
||||||
|
backoffMs *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isDownloading = false
|
||||||
|
cancelRequested = false
|
||||||
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performDownload(url: String, partialFile: File, version: Int) {
|
||||||
|
var startOffset = if (partialFile.exists()) partialFile.length() else 0L
|
||||||
|
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
|
||||||
|
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 15_000
|
||||||
|
readTimeout = 30_000
|
||||||
|
if (startOffset > 0L) {
|
||||||
|
setRequestProperty("Range", "bytes=$startOffset-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.connect()
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK && startOffset > 0L) {
|
||||||
|
Logger.w(TAG, "Server ignored Range header, restarting download from scratch")
|
||||||
|
partialFile.delete()
|
||||||
|
startOffset = 0L
|
||||||
|
} else if (responseCode != HttpURLConnection.HTTP_OK &&
|
||||||
|
responseCode != HttpURLConnection.HTTP_PARTIAL) {
|
||||||
|
throw IllegalStateException("Unexpected HTTP response code $responseCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentLength = connection.contentLengthLong
|
||||||
|
val totalBytes = if (contentLength > 0L) startOffset + contentLength else -1L
|
||||||
|
|
||||||
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
|
var downloaded = 0L
|
||||||
|
var lastProgress = -1
|
||||||
|
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
FileOutputStream(partialFile, startOffset > 0L).use { output ->
|
||||||
|
while (!cancelRequested) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
downloaded += read
|
||||||
|
|
||||||
|
if (totalBytes > 0L) {
|
||||||
|
val progress = (((startOffset + downloaded) * 100L) / totalBytes).toInt()
|
||||||
|
if (progress != lastProgress) {
|
||||||
|
lastProgress = progress
|
||||||
|
val safeProgress = when {
|
||||||
|
progress < 0 -> 0
|
||||||
|
progress > 100 -> 100
|
||||||
|
else -> progress
|
||||||
|
}
|
||||||
|
UpdateNotificationManager.updateDownloadProgress(this, version, safeProgress, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UpdateNotificationManager.updateDownloadProgress(this, version, 0, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelRequested) {
|
||||||
|
throw CancellationException("Download cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalBytes > 0L && startOffset + downloaded < totalBytes) {
|
||||||
|
throw IllegalStateException("Download incomplete: expected=$totalBytes, got=${startOffset + downloaded}")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDownloadComplete(version: Int, apkFile: File) {
|
||||||
|
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
|
||||||
|
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
|
||||||
|
|
||||||
|
if (StateApp.instance.isMainActive) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
StateApp.withContext { ctx ->
|
||||||
|
try {
|
||||||
|
UIDialogs.showConfirmationDialog(ctx, "Update downloaded, press confirm to install", {
|
||||||
|
UpdateNotificationManager.cancelAll(ctx)
|
||||||
|
UpdateInstaller.startInstall(ctx, apkFile)
|
||||||
|
}, {})
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent.FLAG_MUTABLE
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
import android.app.PendingIntent.getBroadcast
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.provider.Settings
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.receivers.InstallReceiver
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
|
object UpdateInstaller {
|
||||||
|
private const val TAG = "UpdateInstaller"
|
||||||
|
|
||||||
|
@SuppressLint("RequestInstallPackagesPolicy")
|
||||||
|
fun startInstall(context: Context, apkFile: File) {
|
||||||
|
if (!apkFile.exists()) {
|
||||||
|
Logger.w(TAG, "APK file does not exist: ${apkFile.absolutePath}")
|
||||||
|
UIDialogs.toast(context, "Update file missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
|
UIDialogs.toast(context, "Updates are managed by the Play Store")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val pm = context.packageManager
|
||||||
|
if (!pm.canRequestPackageInstalls()) {
|
||||||
|
UIDialogs.toast(context, "Allow this app to install updates, then try again")
|
||||||
|
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||||
|
data = "package:${context.packageName}".toUri()
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to check unknown sources permission", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
var inputStream: InputStream? = null
|
||||||
|
var session: PackageInstaller.Session? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
||||||
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
|
val sessionId = packageInstaller.createSession(params)
|
||||||
|
session = packageInstaller.openSession(sessionId)
|
||||||
|
|
||||||
|
inputStream = apkFile.inputStream()
|
||||||
|
val dataLength = apkFile.length()
|
||||||
|
|
||||||
|
session.openWrite("package", 0, dataLength).use { sessionStream ->
|
||||||
|
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
|
||||||
|
session.fsync(sessionStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(context, InstallReceiver::class.java)
|
||||||
|
val pendingIntent = getBroadcast(context, 0, intent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val statusReceiver = pendingIntent.intentSender
|
||||||
|
|
||||||
|
Logger.i(TAG, "Committing install session for ${apkFile.absolutePath}")
|
||||||
|
session.commit(statusReceiver)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Exception while installing update", e)
|
||||||
|
session?.abandon()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, "Failed to install update: ${e.message}")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
session?.close()
|
||||||
|
inputStream?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent.FLAG_MUTABLE
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
import android.app.PendingIntent.getBroadcast
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object UpdateNotificationManager {
|
||||||
|
private const val CHANNEL_ID = "app_updates"
|
||||||
|
private const val CHANNEL_NAME = "App updates"
|
||||||
|
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
|
||||||
|
|
||||||
|
const val ACTION_UPDATE_YES = "com.futo.platformplayer.UPDATE_YES"
|
||||||
|
const val ACTION_UPDATE_NO = "com.futo.platformplayer.UPDATE_NO"
|
||||||
|
const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
|
||||||
|
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
||||||
|
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
||||||
|
|
||||||
|
const val EXTRA_VERSION = "version"
|
||||||
|
const val EXTRA_APK_PATH = "apk_path"
|
||||||
|
|
||||||
|
const val NOTIF_ID_AVAILABLE = 2001
|
||||||
|
const val NOTIF_ID_DOWNLOADING = 2002
|
||||||
|
const val NOTIF_ID_READY = 2003
|
||||||
|
|
||||||
|
fun ensureChannel(context: Context) {
|
||||||
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
|
channel.description = CHANNEL_DESCRIPTION
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showUpdateAvailableNotification(context: Context, version: Int) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val yesIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_UPDATE_YES
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val yesPendingIntent = getBroadcast(context, 0, yesIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val noIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_UPDATE_NO
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val noPendingIntent = getBroadcast(context, 1, noIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val neverIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_UPDATE_NEVER
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val neverPendingIntent = getBroadcast(context, 2, neverIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Update available")
|
||||||
|
.setContentText("A new version ($version) is available.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.addAction(0, "Download", yesPendingIntent)
|
||||||
|
.addAction(0, "Not now", noPendingIntent)
|
||||||
|
.addAction(0, "Never", neverPendingIntent)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_AVAILABLE, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val cancelIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_DOWNLOAD_CANCEL
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val cancelPendingIntent = getBroadcast(
|
||||||
|
context,
|
||||||
|
3,
|
||||||
|
cancelIntent,
|
||||||
|
FLAG_MUTABLE or FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Downloading update")
|
||||||
|
.setContentText("Downloading version $version")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setOngoing(true)
|
||||||
|
.addAction(0, "Cancel", cancelPendingIntent)
|
||||||
|
|
||||||
|
if (indeterminate) {
|
||||||
|
builder.setProgress(0, 0, true)
|
||||||
|
} else {
|
||||||
|
builder.setProgress(100, progress, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloadProgress(context: Context, version: Int, progress: Int, indeterminate: Boolean) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val notification = buildDownloadProgressNotification(context, version, progress, indeterminate)
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_DOWNLOADING, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun showDownloadCompleteNotification(context: Context, version: Int, apkFile: File) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val installIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_INSTALL_NOW
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
putExtra(EXTRA_APK_PATH, apkFile.absolutePath)
|
||||||
|
}
|
||||||
|
val installPendingIntent = getBroadcast(
|
||||||
|
context,
|
||||||
|
4,
|
||||||
|
installIntent,
|
||||||
|
FLAG_MUTABLE or FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Update downloaded")
|
||||||
|
.setContentText("Tap to install version $version.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.addAction(0, "Install", installPendingIntent)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun showDownloadFailedNotification(context: Context, version: Int, error: Throwable?) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Failed to download update")
|
||||||
|
.setContentText(error?.message ?: "Unknown error")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelAll(context: Context) {
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_AVAILABLE)
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -245,19 +245,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val _notifPermission = "android.permission.POST_NOTIFICATIONS";
|
|
||||||
private val _notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
|
||||||
if (isGranted)
|
|
||||||
UIDialogs.toast(this, "Notification permission granted");
|
|
||||||
else
|
|
||||||
UIDialogs.toast(this, "Notification permission denied");
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun requestNotificationPermissions() {
|
|
||||||
_notificationPermissionLauncher?.launch(_notifPermission);
|
|
||||||
}
|
|
||||||
|
|
||||||
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
||||||
|
|
||||||
@@ -631,6 +618,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||||
|
requestNotificationPermissions("Grayjay uses notifications to inform you when a new app update is available.");
|
||||||
|
}
|
||||||
|
|
||||||
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
||||||
|
|
||||||
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import android.widget.Button
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UpdateDownloadService
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.copyToOutputStream
|
import com.futo.platformplayer.copyToOutputStream
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -46,7 +48,6 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private var _maxVersion: Int = 0;
|
private var _maxVersion: Int = 0;
|
||||||
|
|
||||||
private var _updating: Boolean = false;
|
private var _updating: Boolean = false;
|
||||||
private var _apkFile: File? = null;
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -80,14 +81,19 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updating = true;
|
if (Settings.instance.autoUpdate.backgroundDownload == 1) {
|
||||||
update();
|
val ctx = context.applicationContext;
|
||||||
|
val intent = Intent(ctx, UpdateDownloadService::class.java);
|
||||||
|
intent.putExtra(UpdateDownloadService.EXTRA_VERSION, _maxVersion);
|
||||||
|
ContextCompat.startForegroundService(ctx, intent);
|
||||||
|
UIDialogs.toast(context, "Downloading update in background");
|
||||||
|
dismiss();
|
||||||
|
} else {
|
||||||
|
_updating = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fun showPredownloaded(apkFile: File) {
|
|
||||||
_apkFile = apkFile;
|
|
||||||
super.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
@@ -118,21 +124,14 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var inputStream: InputStream? = null;
|
var inputStream: InputStream? = null;
|
||||||
try {
|
try {
|
||||||
val apkFile = _apkFile;
|
val client = ManagedHttpClient();
|
||||||
if (apkFile != null) {
|
val response = client.get(StateUpdate.APK_URL);
|
||||||
inputStream = apkFile.inputStream();
|
if (response.isOk && response.body != null) {
|
||||||
val dataLength = apkFile.length();
|
inputStream = response.body.byteStream();
|
||||||
|
val dataLength = response.body.contentLength();
|
||||||
install(inputStream, dataLength);
|
install(inputStream, dataLength);
|
||||||
} else {
|
} else {
|
||||||
val client = ManagedHttpClient();
|
throw Exception("Failed to download latest version of app.");
|
||||||
val response = client.get(StateUpdate.APK_URL);
|
|
||||||
if (response.isOk && response.body != null) {
|
|
||||||
inputStream = response.body.byteStream();
|
|
||||||
val dataLength = response.body.contentLength();
|
|
||||||
install(inputStream, dataLength);
|
|
||||||
} else {
|
|
||||||
throw Exception("Failed to download latest version of app.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Exception thrown while downloading and installing latest version of app.", e);
|
Logger.w(TAG, "Exception thrown while downloading and installing latest version of app.", e);
|
||||||
|
|||||||
@@ -572,30 +572,39 @@ class StateApp {
|
|||||||
DownloadService.getOrCreateService(context);
|
DownloadService.getOrCreateService(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
|
if (Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||||
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
if (Settings.instance.autoUpdate.backgroundDownload == 1) {
|
||||||
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate Background]");
|
||||||
val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
|
val constraints = Constraints.Builder()
|
||||||
when {
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
//Background download
|
.build();
|
||||||
autoUpdateEnabled && shouldDownload && backgroundDownload -> {
|
|
||||||
StateUpdate.instance.setShouldBackgroundUpdate(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdateEnabled && !shouldDownload && backgroundDownload -> {
|
val periodicRequest = PeriodicWorkRequest.Builder(
|
||||||
Logger.i(TAG, "Auto update skipped due to wrong network state");
|
UpdateCheckWorker::class.java,
|
||||||
}
|
12, TimeUnit.HOURS
|
||||||
|
)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build();
|
||||||
|
|
||||||
//Foreground download
|
val wm = WorkManager.getInstance(context);
|
||||||
autoUpdateEnabled -> {
|
wm.enqueueUniquePeriodicWork(
|
||||||
|
UpdateCheckWorker.UNIQUE_WORK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
|
periodicRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
val oneTimeRequest = OneTimeWorkRequest.Builder(UpdateCheckWorker::class.java)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build();
|
||||||
|
wm.enqueue(oneTimeRequest);
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
|
||||||
scopeOrNull?.launch(Dispatchers.IO) {
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
StateUpdate.instance.checkForUpdates(context, false)
|
StateUpdate.instance.checkForUpdates(context, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
else -> {
|
Logger.i(TAG, "AutoUpdate disabled");
|
||||||
Logger.i(TAG, "Auto update disabled");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||||
@@ -781,24 +790,20 @@ class StateApp {
|
|||||||
Logger.i("StateApp", "No AutoBackup configured");
|
Logger.i("StateApp", "No AutoBackup configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun scheduleBackgroundWork(context: Context, active: Boolean = true, intervalMinutes: Int = 60 * 12) {
|
fun scheduleBackgroundWork(context: Context, active: Boolean = true, intervalMinutes: Int = 60 * 12) {
|
||||||
try {
|
try {
|
||||||
val wm = WorkManager.getInstance(context);
|
val wm = WorkManager.getInstance(context);
|
||||||
|
|
||||||
if(active) {
|
if (active) {
|
||||||
if(BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
UIDialogs.toast(context, "Scheduling background every ${intervalMinutes} minutes");
|
UIDialogs.toast(context, "Scheduling background every ${intervalMinutes} minutes");
|
||||||
|
|
||||||
val req = PeriodicWorkRequest.Builder(BackgroundWorker::class.java, intervalMinutes.toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES)
|
val req = PeriodicWorkRequest.Builder(BackgroundWorker::class.java, intervalMinutes.toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES)
|
||||||
.setConstraints(Constraints.Builder()
|
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED).build()).build();
|
||||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
wm.enqueueUniquePeriodicWork("backgroundSubscriptions", ExistingPeriodicWorkPolicy.UPDATE, req);
|
wm.enqueueUniquePeriodicWork("backgroundSubscriptions", ExistingPeriodicWorkPolicy.UPDATE, req);
|
||||||
|
} else {
|
||||||
|
wm.cancelUniqueWork("backgroundSubscriptions");
|
||||||
}
|
}
|
||||||
else
|
|
||||||
wm.cancelAllWork();
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to schedule background subscription updates.", e)
|
Logger.e(TAG, "Failed to schedule background subscription updates.", e)
|
||||||
UIDialogs.toast(context, "Background subscription update failed: " + e.message)
|
UIDialogs.toast(context, "Background subscription update failed: " + e.message)
|
||||||
@@ -806,6 +811,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun migrateStores(context: Context, managedStores: List<ManagedStore<*>>, index: Int) {
|
private suspend fun migrateStores(context: Context, managedStores: List<ManagedStore<*>>, index: Int) {
|
||||||
if(managedStores.size <= index)
|
if(managedStores.size <= index)
|
||||||
return;
|
return;
|
||||||
@@ -903,15 +909,6 @@ class StateApp {
|
|||||||
try {
|
try {
|
||||||
if(FragmentedStorage.isInitialized && Settings.instance.downloads.shouldDownload())
|
if(FragmentedStorage.isInitialized && Settings.instance.downloads.shouldDownload())
|
||||||
StateDownloads.instance.checkForDownloadsTodos();
|
StateDownloads.instance.checkForDownloadsTodos();
|
||||||
|
|
||||||
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
|
||||||
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
|
||||||
val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
|
|
||||||
if (autoUpdateEnabled && shouldDownload && backgroundDownload) {
|
|
||||||
StateUpdate.instance.setShouldBackgroundUpdate(true);
|
|
||||||
} else {
|
|
||||||
StateUpdate.instance.setShouldBackgroundUpdate(false);
|
|
||||||
}
|
|
||||||
} catch(ex: Throwable) {
|
} catch(ex: Throwable) {
|
||||||
Logger.w(TAG, "Failed to handle capabilities changed event", ex);
|
Logger.w(TAG, "Failed to handle capabilities changed event", ex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,146 +15,6 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
class StateUpdate {
|
class StateUpdate {
|
||||||
private var _backgroundUpdateFinished = false;
|
|
||||||
private var _gettingOrDownloadingLastApk = false;
|
|
||||||
private var _shouldBackgroundUpdate = false;
|
|
||||||
private val _lockObject = Object();
|
|
||||||
|
|
||||||
private fun getOrDownloadLastApkFile(filesDir: File): File? {
|
|
||||||
try {
|
|
||||||
Logger.i(TAG, "Started getting or downloading latest APK file.");
|
|
||||||
|
|
||||||
if (!_shouldBackgroundUpdate) {
|
|
||||||
Logger.i(TAG, "Update download cancelled 1.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Started background update download.");
|
|
||||||
val client = ManagedHttpClient();
|
|
||||||
val latestVersion = downloadVersionCode(client);
|
|
||||||
if (!_shouldBackgroundUpdate) {
|
|
||||||
Logger.i(TAG, "Update download cancelled 2.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latestVersion != null) {
|
|
||||||
val currentVersion = BuildConfig.VERSION_CODE;
|
|
||||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
|
||||||
|
|
||||||
if (latestVersion <= currentVersion) {
|
|
||||||
Logger.i(TAG, "Already up to date.");
|
|
||||||
_backgroundUpdateFinished = true;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
val outputDirectory = File(filesDir, "autoupdate");
|
|
||||||
if (!outputDirectory.exists()) {
|
|
||||||
outputDirectory.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_shouldBackgroundUpdate) {
|
|
||||||
Logger.i(TAG, "Update download cancelled 3.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
val apkOutputFile = File(outputDirectory, "last_version.apk");
|
|
||||||
val versionOutputFile = File(outputDirectory, "last_version.txt");
|
|
||||||
|
|
||||||
var cachedVersionInvalid = false;
|
|
||||||
if (!versionOutputFile.exists() || !apkOutputFile.exists()) {
|
|
||||||
Logger.i(TAG, "No downloaded version exists.");
|
|
||||||
cachedVersionInvalid = true;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
val downloadedVersion = versionOutputFile.readText().toInt();
|
|
||||||
Logger.i(TAG, "Downloaded version is $downloadedVersion.");
|
|
||||||
if (downloadedVersion != latestVersion) {
|
|
||||||
Logger.i(TAG, "Downloaded version is not newest version.");
|
|
||||||
cachedVersionInvalid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.w(TAG, "Deleted version file as it was inaccessible");
|
|
||||||
versionOutputFile.delete();
|
|
||||||
cachedVersionInvalid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_shouldBackgroundUpdate) {
|
|
||||||
Logger.i(TAG, "Update download cancelled 4.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedVersionInvalid) {
|
|
||||||
Logger.i(TAG, "Downloading new APK to '${apkOutputFile.path}'...");
|
|
||||||
downloadApkToFile(client, apkOutputFile) { !_shouldBackgroundUpdate };
|
|
||||||
versionOutputFile.writeText(latestVersion.toString());
|
|
||||||
|
|
||||||
Logger.i(TAG, "Downloaded APK to '${apkOutputFile.path}'.");
|
|
||||||
} else {
|
|
||||||
Logger.i(TAG, "Latest APK is already downloaded in '${apkOutputFile.path}'...");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_shouldBackgroundUpdate) {
|
|
||||||
Logger.i(TAG, "Update download cancelled 5.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return apkOutputFile;
|
|
||||||
} else {
|
|
||||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to download APK.", e);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
_gettingOrDownloadingLastApk = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldBackgroundUpdate(shouldBackgroundUpdate: Boolean) {
|
|
||||||
synchronized (_lockObject) {
|
|
||||||
if (_backgroundUpdateFinished) {
|
|
||||||
_shouldBackgroundUpdate = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_shouldBackgroundUpdate = shouldBackgroundUpdate;
|
|
||||||
if (shouldBackgroundUpdate && !_gettingOrDownloadingLastApk) {
|
|
||||||
Logger.i(TAG, "Auto Updating in Background");
|
|
||||||
|
|
||||||
_gettingOrDownloadingLastApk = true;
|
|
||||||
StateApp.withContext { context ->
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val file = getOrDownloadLastApkFile(context.filesDir);
|
|
||||||
if (file == null) {
|
|
||||||
Logger.i(TAG, "Failed to get or download update.");
|
|
||||||
return@launch;
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
try {
|
|
||||||
context.let { c ->
|
|
||||||
_backgroundUpdateFinished = true;
|
|
||||||
UIDialogs.showInstallDownloadedUpdateDialog(c, file);
|
|
||||||
};
|
|
||||||
Logger.i(TAG, "Showing install dialog for '${file.path}'.");
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
context.let { c -> UIDialogs.toast(c, "Failed to show update dialog"); };
|
|
||||||
Logger.w(TAG, "Error occurred in update dialog.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to get last downloaded APK file.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val client = ManagedHttpClient();
|
val client = ManagedHttpClient();
|
||||||
@@ -196,25 +56,6 @@ class StateUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) {
|
|
||||||
var apkStream: InputStream? = null;
|
|
||||||
var outputStream: OutputStream? = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
val response = client.get(APK_URL);
|
|
||||||
if (response.isOk && response.body != null) {
|
|
||||||
apkStream = response.body.byteStream();
|
|
||||||
outputStream = destinationFile.outputStream();
|
|
||||||
apkStream.copyToOutputStream(outputStream, isCancelled);
|
|
||||||
apkStream.close();
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
apkStream?.close();
|
|
||||||
outputStream?.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadVersionCode(client: ManagedHttpClient): Int? {
|
fun downloadVersionCode(client: ManagedHttpClient): Int? {
|
||||||
val response = client.get(VERSION_URL);
|
val response = client.get(VERSION_URL);
|
||||||
if (!response.isOk || response.body == null) {
|
if (!response.isOk || response.body == null) {
|
||||||
@@ -267,6 +108,22 @@ class StateUpdate {
|
|||||||
}
|
}
|
||||||
val CHANGELOG_BASE_URL = "https://releases.grayjay.app/changelogs";
|
val CHANGELOG_BASE_URL = "https://releases.grayjay.app/changelogs";
|
||||||
|
|
||||||
|
fun getApkFile(context: Context, version: Int): File {
|
||||||
|
val dir = File(context.filesDir, "updates");
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs();
|
||||||
|
}
|
||||||
|
return File(dir, "app-${DESIRED_ABI}-${version}.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPartialApkFile(context: Context, version: Int): File {
|
||||||
|
val dir = File(context.filesDir, "updates");
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs();
|
||||||
|
}
|
||||||
|
return File(dir, "app-${DESIRED_ABI}-${version}.apk.part");
|
||||||
|
}
|
||||||
|
|
||||||
fun finish() {
|
fun finish() {
|
||||||
_instance?.let {
|
_instance?.let {
|
||||||
_instance = null;
|
_instance = null;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/update"
|
android:text="@string/download"
|
||||||
android:textSize="14dp"
|
android:textSize="14dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:fontFamily="@font/inter_regular"
|
android:fontFamily="@font/inter_regular"
|
||||||
|
|||||||
Reference in New Issue
Block a user