diff --git a/app/build.gradle b/app/build.gradle index 5a68bbb7..71c57dbc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt index 36a25729..a17815da 100644 --- a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt @@ -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(R.id.update_app) + val updateAppSwitch = updateAppLayout.findViewById(R.id.preference_switch) + updateAppLayout.findViewById(R.id.preference_icon).apply { + icon = ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_update_app) + isVisible = true + } + updateAppLayout.findViewById(R.id.preference_title).text = getString(R.string.update_app) + updateAppLayout.findViewById(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(R.id.update_ytdl) + val updateYTDLSwitch = updateYTDLLayout.findViewById(R.id.preference_switch) + updateYTDLLayout.findViewById(R.id.preference_icon).apply { + icon = ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_update) + isVisible = true + } + updateYTDLLayout.findViewById(R.id.preference_title).text = getString(R.string.auto_update_ytdlp) + updateYTDLLayout.findViewById(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" } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsModule.kt index d8f1fadf..95ed4bb7 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsModule.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsModule.kt @@ -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 }) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsModule.kt index e4680731..a34fa76f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsModule.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsModule.kt @@ -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 -> diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt index 5dbec3b0..6c626582 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt @@ -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 diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 1db4e0d2..14ed1e56 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -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(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) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt index 0bfa8ccd..e0ed3063 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt @@ -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 { + 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 diff --git a/app/src/main/res/layout/dialog_ask_update_preferences.xml b/app/src/main/res/layout/dialog_ask_update_preferences.xml new file mode 100644 index 00000000..cf173361 --- /dev/null +++ b/app/src/main/res/layout/dialog_ask_update_preferences.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_search_result_regular.xml b/app/src/main/res/layout/preference_search_result_regular.xml index 84e7202a..d9777b3a 100644 --- a/app/src/main/res/layout/preference_search_result_regular.xml +++ b/app/src/main/res/layout/preference_search_result_regular.xml @@ -7,7 +7,7 @@ android:background="?android:attr/selectableItemBackground" android:paddingEnd="16dp" android:paddingStart="0dp" - android:paddingVertical="16dp" + android:paddingVertical="8dp" android:minHeight="48dp"> @@ -41,7 +42,8 @@ android:textSize="16sp" android:ellipsize="end" android:textStyle="bold" - android:maxLines="2" /> + android:maxLines="2" + tools:text="Preference Title" /> + android:visibility="gone" + tools:text="Preference Description" + tools:visibility="visible" + /> Bundled If enabled, metadata fields will follow your app language and even affect format selection by preferring dubbed versions instead of original Installed + 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. diff --git a/app/src/main/res/xml/updating_preferences.xml b/app/src/main/res/xml/updating_preferences.xml index 418e2c5a..e19ef85e 100644 --- a/app/src/main/res/xml/updating_preferences.xml +++ b/app/src/main/res/xml/updating_preferences.xml @@ -51,7 +51,7 @@