support for silent updates

This commit is contained in:
Bnyro 2022-07-18 19:15:35 +02:00
parent e48da1109a
commit ebcd11d00c
9 changed files with 137 additions and 50 deletions

View File

@ -277,8 +277,12 @@
<service <service
android:name=".services.ClosingService" android:name=".services.ClosingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false" />
android:stopWithTask="false" />
<service
android:name=".services.UpdateService"
android:enabled="true"
android:exported="false" />
</application> </application>
</manifest> </manifest>

View File

@ -4,6 +4,8 @@ import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.os.Build import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
class MyApp : Application() { class MyApp : Application() {
@ -19,6 +21,12 @@ class MyApp : Application() {
* set the applicationContext as context for the [PreferenceHelper] * set the applicationContext as context for the [PreferenceHelper]
*/ */
PreferenceHelper.setContext(applicationContext) PreferenceHelper.setContext(applicationContext)
/**
* bypassing fileUriExposedException, see https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed
*/
val builder = VmPolicy.Builder()
StrictMode.setVmPolicy(builder.build())
} }
/** /**

View File

@ -3,43 +3,51 @@ package com.github.libretube.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.services.UpdateService
import com.github.libretube.update.UpdateInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
class UpdateAvailableDialog( class UpdateDialog(
private val versionTag: String, private val updateInfo: UpdateInfo
private val updateLink: String
) : DialogFragment() { ) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { return activity?.let {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(context?.getString(R.string.update_available, versionTag)) .setTitle(context?.getString(R.string.update_available, updateInfo.name))
.setMessage(context?.getString(R.string.update_available_text)) .setMessage(context?.getString(R.string.update_now))
.setNegativeButton(context?.getString(R.string.cancel)) { _, _ -> .setNegativeButton(context?.getString(R.string.cancel)) { _, _ ->
dismiss() dismiss()
} }
.setPositiveButton(context?.getString(R.string.okay)) { _, _ -> .setPositiveButton(context?.getString(R.string.okay)) { _, _ ->
val uri = Uri.parse(updateLink) val downloadUrl = getDownloadUrl(updateInfo)
Log.i("downloadUrl", downloadUrl.toString())
if (downloadUrl != null) {
val intent = Intent(context, UpdateService::class.java)
intent.putExtra("downloadUrl", downloadUrl)
context?.startService(intent)
} else {
val uri = Uri.parse(updateInfo.html_url)
val intent = Intent(Intent.ACTION_VIEW).setData(uri) val intent = Intent(Intent.ACTION_VIEW).setData(uri)
startActivity(intent) startActivity(intent)
} }
}
.create() .create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
} }
}
class NoUpdateAvailableDialog : DialogFragment() { private fun getDownloadUrl(updateInfo: UpdateInfo): String? {
val supportedArchitectures = Build.SUPPORTED_ABIS
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { supportedArchitectures.forEach { arch ->
return activity?.let { updateInfo.assets.forEach {
MaterialAlertDialogBuilder(requireContext()) if (it.browser_download_url.contains(arch)) return it.browser_download_url
.setTitle(context?.getString(R.string.app_uptodate)) }
.setMessage(context?.getString(R.string.no_update_available)) }
.setPositiveButton(context?.getString(R.string.okay)) { _, _ -> } return null
.create()
} ?: throw IllegalStateException("Activity cannot be null")
} }
} }

View File

@ -1,7 +0,0 @@
package com.github.libretube.obj
// data class for the update info, required to return the data
data class VersionInfo(
val updateUrl: String,
val tagName: String
)

View File

@ -10,9 +10,9 @@ import com.github.libretube.BuildConfig
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity import com.github.libretube.activities.SettingsActivity
import com.github.libretube.dialogs.RequireRestartDialog import com.github.libretube.dialogs.RequireRestartDialog
import com.github.libretube.dialogs.UpdateAvailableDialog import com.github.libretube.dialogs.UpdateDialog
import com.github.libretube.update.checkUpdate
import com.github.libretube.util.ThemeHelper import com.github.libretube.util.ThemeHelper
import com.github.libretube.util.checkUpdate
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -93,13 +93,10 @@ class MainSettings : PreferenceFragmentCompat() {
update?.setOnPreferenceClickListener { update?.setOnPreferenceClickListener {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
// check for update // check for update
val versionInfo = checkUpdate() val updateInfo = checkUpdate()
if (versionInfo?.tagName != "" && BuildConfig.VERSION_NAME != versionInfo?.tagName) { if (updateInfo?.name != "" && BuildConfig.VERSION_NAME != updateInfo?.name) {
// show the UpdateAvailableDialog if there's an update available // show the UpdateAvailableDialog if there's an update available
val updateAvailableDialog = UpdateAvailableDialog( val updateAvailableDialog = UpdateDialog(updateInfo!!)
versionInfo!!.tagName,
versionInfo.updateUrl
)
updateAvailableDialog.show(childFragmentManager, "UpdateAvailableDialog") updateAvailableDialog.show(childFragmentManager, "UpdateAvailableDialog")
} else { } else {
// otherwise show the no update available snackBar // otherwise show the no update available snackBar

View File

@ -164,7 +164,10 @@ class DownloadService : Service() {
onDestroy() onDestroy()
} }
} else { } else {
try {
muxDownloadedMedia() muxDownloadedMedia()
} catch (e: Exception) {
}
} }
} }
} }

View File

@ -0,0 +1,79 @@
package com.github.libretube.services
import android.app.DownloadManager
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import android.os.IBinder
import java.io.File
class UpdateService : Service() {
private val TAG = "UpdateService"
private lateinit var downloadUrl: String
private var downloadId: Long = -1
private lateinit var file: File
private lateinit var downloadManager: DownloadManager
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
downloadUrl = intent?.getStringExtra("downloadUrl")!!
downloadApk(downloadUrl)
return super.onStartCommand(intent, flags, startId)
}
private fun downloadApk(downloadUrl: String) {
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
// val dir = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
file = File(dir, "release.apk")
val request: DownloadManager.Request =
DownloadManager.Request(Uri.parse(downloadUrl))
.setTitle("Downloading APK ...")
.setDescription("")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
.setDestinationUri(Uri.fromFile(file))
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
downloadManager =
applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
downloadId = downloadManager.enqueue(request)
// listener for the download to end
registerReceiver(
onDownloadComplete,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
}
private val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (downloadId == id) {
// install the apk after download finished
val installIntent = Intent(Intent.ACTION_VIEW)
installIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
installIntent.setDataAndType(
Uri.fromFile(file),
downloadManager.getMimeTypeForDownloadedFile(downloadId)
)
startActivity(installIntent)
}
}
}
override fun onDestroy() {
unregisterReceiver(onDownloadComplete)
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {
TODO("Not yet implemented")
}
}

View File

@ -1,16 +1,14 @@
package com.github.libretube.util package com.github.libretube.update
import com.github.libretube.GITHUB_API_URL import com.github.libretube.GITHUB_API_URL
import com.github.libretube.obj.VersionInfo
import com.github.libretube.update.UpdateInfo
import com.google.gson.Gson import com.google.gson.Gson
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
fun checkUpdate(): VersionInfo? { fun checkUpdate(): UpdateInfo? {
var versionInfo: VersionInfo? = null var versionInfo: UpdateInfo? = null
// run http request as thread to make it async // run http request as thread to make it async
val thread = Thread { val thread = Thread {
// otherwise crashes without internet // otherwise crashes without internet
@ -27,22 +25,18 @@ fun checkUpdate(): VersionInfo? {
return versionInfo return versionInfo
} }
fun getUpdateInfo(): VersionInfo? { fun getUpdateInfo(): UpdateInfo? {
val latest = URL(GITHUB_API_URL) val latest = URL(GITHUB_API_URL)
val json = StringBuilder() val json = StringBuilder()
val urlConnection: HttpsURLConnection? val urlConnection: HttpsURLConnection?
urlConnection = latest.openConnection() as HttpsURLConnection urlConnection = latest.openConnection() as HttpsURLConnection
val br = BufferedReader(InputStreamReader(urlConnection.inputStream))
// read json
val br = BufferedReader(InputStreamReader(urlConnection.inputStream))
var line: String? var line: String?
while (br.readLine().also { line = it } != null) json.append(line) while (br.readLine().also { line = it } != null) json.append(line)
// Parse and return the json data // Parse and return the json data
val gson = Gson() val gson = Gson()
val updateInfo = gson.fromJson(json.toString(), UpdateInfo::class.java) return gson.fromJson(json.toString(), UpdateInfo::class.java)
return VersionInfo(
updateInfo.html_url,
updateInfo.name
)
} }

View File

@ -252,4 +252,5 @@
<string name="system_caption_style">System caption style</string> <string name="system_caption_style">System caption style</string>
<string name="captions">Captions</string> <string name="captions">Captions</string>
<string name="none">None</string> <string name="none">None</string>
<string name="update_now">Do you want to update the app now?</string>
</resources> </resources>