ask permission for auto update preferences on fresh install

pull/1153/head
deniscerri 4 months ago
parent e61a644bd0
commit b22bc0b155
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -12,7 +12,7 @@ def properties = new Properties()
def versionMajor = 1
def versionMinor = 8
def versionPatch = 8
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionBuild = 1 // bump for dogfood builds, public betas, etc.
//WHEN RELEASING BETA KEEP THE SAME VERSION AS LATEST STABLE, SO USERS CAN DOWNGRADE
def isBeta = false

@ -12,13 +12,22 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowInsets
import android.view.inputmethod.InputMethodManager
import android.widget.CheckBox
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
@ -31,6 +40,8 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.afollestad.materialdialogs.utils.MDUtil.textChanged
import com.anggrayudi.storage.file.getAbsolutePath
import com.deniscerri.ytdl.core.RuntimeManager
import com.deniscerri.ytdl.database.DBManager
@ -53,12 +64,17 @@ import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigation.NavigationView
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -72,6 +88,8 @@ import java.io.InputStreamReader
import java.io.Reader
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.Locale
import kotlin.sequences.forEach
import kotlin.system.exitProcess
@ -112,7 +130,6 @@ class MainActivity : BaseActivity() {
}
askPermissions()
checkUpdate()
navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
navController = navHostFragment.findNavController()
@ -270,26 +287,7 @@ class MainActivity : BaseActivity() {
val intent = intent
handleIntents(intent)
if (preferences.getBoolean("auto_update_ytdlp", false)){
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
kotlin.runCatching {
if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){
if (UpdateUtil(this@MainActivity).updateYTDL().status == UpdateUtil.YTDLPUpdateStatus.DONE) {
val version = RuntimeManager.getInstance().version(context)
val snack = Snackbar.make(findViewById(R.id.frame_layout),
this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]",
Snackbar.LENGTH_LONG)
navigationBarView?.apply {
snack.setAnchorView(this)
}
snack.show()
}
}
}
}
}
askAutoUpdatePreferences()
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
@ -485,8 +483,67 @@ class MainActivity : BaseActivity() {
}
}
private fun askAutoUpdatePreferences() {
if (preferences.getBoolean("asked_auto_update_preferences", false)) {
callAutoUpdates()
return
}
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(context.getString(R.string.update))
builder.setIcon(R.drawable.ic_info)
val view = layoutInflater.inflate(R.layout.dialog_ask_update_preferences, null)
val updateAppLayout = view.findViewById<View>(R.id.update_app)
val updateAppSwitch = updateAppLayout.findViewById<MaterialSwitch>(R.id.preference_switch)
updateAppLayout.findViewById<MaterialButton>(R.id.preference_icon).apply {
icon = ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_update_app)
isVisible = true
}
updateAppLayout.findViewById<TextView>(R.id.preference_title).text = getString(R.string.update_app)
updateAppLayout.findViewById<TextView>(R.id.preference_summary).apply {
text = getString(R.string.update_app_summary)
isVisible = true
}
updateAppSwitch.isChecked = true
updateAppLayout.setOnClickListener {
updateAppSwitch.isChecked = !updateAppSwitch.isChecked
}
val updateYTDLLayout = view.findViewById<View>(R.id.update_ytdl)
val updateYTDLSwitch = updateYTDLLayout.findViewById<MaterialSwitch>(R.id.preference_switch)
updateYTDLLayout.findViewById<MaterialButton>(R.id.preference_icon).apply {
icon = ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_update)
isVisible = true
}
updateYTDLLayout.findViewById<TextView>(R.id.preference_title).text = getString(R.string.auto_update_ytdlp)
updateYTDLLayout.findViewById<TextView>(R.id.preference_summary).apply {
text = getString(R.string.auto_update_ytdlp_summary)
isVisible = true
}
updateYTDLSwitch.isChecked = true
updateYTDLLayout.setOnClickListener {
updateYTDLSwitch.isChecked = !updateYTDLSwitch.isChecked
}
builder.setView(view)
builder.setCancelable(false)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
preferences.edit(commit = true) {
putBoolean("update_app", updateAppSwitch.isChecked)
putBoolean("auto_update_ytdlp", updateYTDLSwitch.isChecked)
putBoolean("asked_auto_update_preferences", true)
}
callAutoUpdates()
}
val dialog = builder.create()
dialog.show()
}
private fun checkUpdate() {
private fun callAutoUpdates() {
if (preferences.getBoolean("update_app", false)) {
val updateUtil = UpdateUtil(this)
CoroutineScope(Dispatchers.IO).launch {
@ -496,14 +553,36 @@ class MainActivity : BaseActivity() {
settingsViewModel.backup()
}
withContext(Dispatchers.Main) {
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, this@MainActivity, preferences)
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, this@MainActivity, updateUtil, this@MainActivity, preferences)
}
}
}
}
if (preferences.getBoolean("auto_update_ytdlp", false)){
CoroutineScope(SupervisorJob()).launch {
try {
val hasActiveQueuedDownloads = DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) > 0
if (hasActiveQueuedDownloads) return@launch
val updateRes = withContext(Dispatchers.IO) {
UpdateUtil(this@MainActivity).updateYTDL()
}
if (updateRes.status == UpdateUtil.YTDLPUpdateStatus.DONE) {
val version = RuntimeManager.getInstance().version(context)
val message = this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]"
Snackbar.make(findViewById(R.id.frame_layout), message, Snackbar.LENGTH_LONG).apply {
anchorView = navigationBarView
show()
}
}
} catch (err: Exception) {}
}
}
}
companion object {
private const val TAG = "MainActivity"
}

@ -60,7 +60,7 @@ object AdvancedSettingsModule : SettingModule {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
showFormatImportanceDialog(context,title.toString(), itms) { new ->
showFormatImportanceDialog(host.getHostContext(),title.toString(), itms) { new ->
prefs.edit(commit = true) {
putString("format_importance_audio", new.joinToString(",") { it.first })
}

@ -158,7 +158,7 @@ object GeneralSettingsModule : SettingModule {
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(optionsRecycler)
MaterialAlertDialogBuilder(context)
MaterialAlertDialogBuilder(host.getHostContext())
.setTitle(R.string.navigation_bar)
.setView(binding)
.setPositiveButton(R.string.ok) { _, _ ->
@ -178,7 +178,7 @@ object GeneralSettingsModule : SettingModule {
(pref as ListPreference).apply {
summary = entry
setOnPreferenceChangeListener { _, newValue ->
val dialog = MaterialAlertDialogBuilder(context)
val dialog = MaterialAlertDialogBuilder(host.getHostContext())
dialog.setTitle(context.getString(R.string.app_icon_change))
dialog.setNegativeButton(context.getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dialog.setPositiveButton(context.getString(R.string.ok)) { _: DialogInterface?, _: Int ->

@ -108,7 +108,7 @@ object UpdateSettingsModule : SettingModule {
settingsViewModel.backup()
}
}
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, host.getHostContext(), preferences)
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, host.getHostContext(), updateUtil, host.hostLifecycleOwner, preferences)
}
}
true

@ -45,12 +45,15 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.FileProvider
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.afollestad.materialdialogs.utils.MDUtil.textChanged
@ -94,6 +97,7 @@ import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -2689,74 +2693,28 @@ object UiUtil {
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun showNewAppUpdateDialog(v: GithubRelease, context: Activity, preferences: SharedPreferences) {
fun showNewAppUpdateDialog(v: GithubRelease, context: Activity, updateUtil: UpdateUtil, lifecycleOwner: LifecycleOwner, preferences: SharedPreferences) {
if (context.isFinishing || context.isDestroyed) return
var positiveButton: Button? = null
var tmpDownloadJob: Job? = null
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val skippedVersions = preferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf()
val updateDialog = MaterialAlertDialogBuilder(context)
.setTitle(v.tag_name)
.setMessage(v.body)
.setCancelable(false)
.setIcon(R.drawable.ic_update_app)
.setNeutralButton(R.string.ignore){ d: DialogInterface?, _:Int ->
tmpDownloadJob?.cancel()
skippedVersions.add(v.tag_name)
preferences.edit().putString("skip_updates", skippedVersions.joinToString(",")).apply()
d?.dismiss()
}
.setNegativeButton(context.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setPositiveButton(context.getString(R.string.update)) { d: DialogInterface?, _: Int ->
runCatching {
val releaseVersion = v.assets.firstOrNull { it.name.contains(Build.SUPPORTED_ABIS[0]) }
if (releaseVersion == null){
Toast.makeText(context, R.string.couldnt_find_apk, Toast.LENGTH_SHORT).show()
return@runCatching
}
val uri = Uri.parse(releaseVersion.browser_download_url)
Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.mkdirs()
val downloadID = downloadManager.enqueue(
DownloadManager.Request(uri)
.setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE
)
.setAllowedOverRoaming(true)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setTitle(releaseVersion.name)
.setDescription(context.getString(R.string.downloading_update))
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, releaseVersion.name)
)
val onDownloadComplete: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
context?.unregisterReceiver(this)
FileUtil.openFileIntent(context!!,
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS)?.absolutePath +
File.separator + releaseVersion.name)
}
}
if (Build.VERSION.SDK_INT >= 26) {
if (Build.VERSION.SDK_INT >= 33) {
context.registerReceiver(onDownloadComplete, IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE),
Context.RECEIVER_NOT_EXPORTED
)
}else{
context.registerReceiver(onDownloadComplete, IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
}
}
d?.dismiss()
}
.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
tmpDownloadJob?.cancel()
}
.setPositiveButton(R.string.update, null)
val view = updateDialog.show()
val textView = view.findViewById<TextView>(android.R.id.message)
textView!!.movementMethod = LinkMovementMethod.getInstance()
@ -2770,5 +2728,41 @@ object UiUtil {
}
}).build()
mw.setMarkdown(textView, v.body)
positiveButton = view.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
positiveButton?.setOnClickListener {
positiveButton.isEnabled = false
positiveButton.text = "0%"
val lifecycleScope = lifecycleOwner.lifecycleScope
tmpDownloadJob = lifecycleScope.launch {
val fileResp = updateUtil.downloadReleaseApk(context, v) { progress ->
lifecycleScope.launch {
withContext(Dispatchers.Main) {
positiveButton.text = "$progress%"
}
}
}
fileResp.onFailure {
lifecycleScope.launch {
withContext(Dispatchers.Main) {
view.dismiss()
Snackbar.make(context.findViewById(R.id.frame_layout), it.message ?: context.getString(R.string.errored), Snackbar.LENGTH_LONG).show()
}
}
}
fileResp.onSuccess { file ->
view.dismiss()
val contentUri = FileProvider.getUriForFile(context, context.packageName + ".fileprovider", file)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(contentUri, "application/vnd.android.package-archive")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
}
}
}
}
}

@ -2,20 +2,27 @@ package com.deniscerri.ytdl.util
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.core.RuntimeManager
import com.deniscerri.ytdl.core.models.YTDLRequest
import com.deniscerri.ytdl.core.packages.PackageBase.Companion.sharedClient
import com.deniscerri.ytdl.core.packages.PackageBase.PackageRelease
import com.deniscerri.ytdl.database.models.GithubRelease
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import okhttp3.Request
import java.io.File
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import kotlin.coroutines.cancellation.CancellationException
class UpdateUtil(var context: Context) {
@ -151,6 +158,51 @@ class UpdateUtil(var context: Context) {
}
suspend fun downloadReleaseApk(context: Context, release: GithubRelease, onProgress: (Int) -> Unit) : Result<File> {
return withContext(Dispatchers.IO) {
try {
val tempApk = File(context.cacheDir, "YTDLnis-${release.tag_name}_interally_downloaded.apk")
val releaseVersion = release.assets.firstOrNull { it.name.contains(Build.SUPPORTED_ABIS[0]) }
//download
val request = Request.Builder()
.url(releaseVersion!!.browser_download_url)
.build()
val response = sharedClient.newCall(request).execute()
if (!response.isSuccessful) {
return@withContext Result.failure(Throwable(response.body.string()))
}
val body = response.body
val totalBytes = body.contentLength()
var bytesDownloaded = 0L
body.byteStream().use { inputStream ->
tempApk.outputStream().use { outputStream ->
val buffer = ByteArray(8192) // 8KB buffer
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
if (!isActive) throw CancellationException("Download cancelled by user")
outputStream.write(buffer, 0, bytesRead)
bytesDownloaded += bytesRead
// Calculate percentage
val progress = ((bytesDownloaded * 100) / totalBytes).toInt()
onProgress(progress)
}
}
}
Result.success(tempApk)
} catch (e: Exception) {
Result.failure(e)
}
}
}
companion object {
var updatingYTDL = false
var updatingApp = false

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:paddingTop="20dp"
android:layout_height="wrap_content">
<include
android:id="@+id/update_app"
layout="@layout/preference_search_result_switch" />
<include
android:id="@+id/update_ytdl"
layout="@layout/preference_search_result_switch" />
</LinearLayout>

@ -7,7 +7,7 @@
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="16dp"
android:paddingStart="0dp"
android:paddingVertical="16dp"
android:paddingVertical="8dp"
android:minHeight="48dp">
<com.google.android.material.button.MaterialButton

@ -7,7 +7,7 @@
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="16dp"
android:paddingStart="0dp"
android:paddingVertical="16dp"
android:paddingVertical="8dp"
android:minHeight="48dp">
<com.google.android.material.button.MaterialButton

@ -3,11 +3,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="16dp"
android:paddingStart="0dp"
android:paddingVertical="16dp"
android:paddingVertical="8dp"
android:minHeight="48dp"
android:gravity="center_vertical">
@ -41,7 +42,8 @@
android:textSize="16sp"
android:ellipsize="end"
android:textStyle="bold"
android:maxLines="2" />
android:maxLines="2"
tools:text="Preference Title" />
<TextView
android:id="@+id/preference_summary"
@ -52,7 +54,10 @@
android:textSize="14sp"
android:ellipsize="end"
android:maxLines="10"
android:visibility="gone" />
android:visibility="gone"
tools:text="Preference Description"
tools:visibility="visible"
/>
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch

@ -515,4 +515,5 @@
<string name="bundled">Bundled</string>
<string name="use_app_language_for_metadata_summary">If enabled, metadata fields will follow your app language and even affect format selection by preferring dubbed versions instead of original</string>
<string name="installed">Installed</string>
<string name="auto_update_ytdlp_summary">It\'s recommended to keep this enabled to keep the application working properly as yt-dlp updates pretty often! The data is extracted from yt-dlp\'s GitHub Repository.</string>
</resources>

@ -51,7 +51,7 @@
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:defaultValue="true"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"

Loading…
Cancel
Save